@workos/oagen 0.8.0 → 0.9.1

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.
@@ -4771,8 +4771,7 @@ function generateAllFiles(spec, emitter, ctx) {
4771
4771
  ...emitter.generateClient(spec, ctx),
4772
4772
  ...emitter.generateErrors(ctx),
4773
4773
  ...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
4774
- ...emitter.generateTests(reachableSpec, ctx),
4775
- ...emitter.generateManifest?.(spec, ctx) ?? []
4774
+ ...emitter.generateTests(reachableSpec, ctx)
4776
4775
  ];
4777
4776
  }
4778
4777
  /** Apply file header to generated files, respecting headerPlacement and JSON files. */
@@ -4786,11 +4785,13 @@ function applyFileHeaders(files, header) {
4786
4785
  function generateFiles(spec, emitter, options) {
4787
4786
  const ctx = buildEmitterContext(spec, options);
4788
4787
  const files = generateAllFiles(spec, emitter, ctx);
4788
+ const operations = emitter.buildOperationsMap?.(spec, ctx);
4789
4789
  const header = emitter.fileHeader();
4790
4790
  return {
4791
4791
  files: applyFileHeaders(files, header),
4792
4792
  ctx,
4793
- header
4793
+ header,
4794
+ operations
4794
4795
  };
4795
4796
  }
4796
4797
  //#endregion
@@ -5056,7 +5057,8 @@ async function writeManifest(dir, opts) {
5056
5057
  ...opts.emitterVersion !== void 0 ? { emitterVersion: opts.emitterVersion } : {},
5057
5058
  ...opts.configSha !== void 0 ? { configSha: opts.configSha } : {},
5058
5059
  ...opts.compatSchemaVersion !== void 0 ? { compatSchemaVersion: opts.compatSchemaVersion } : {},
5059
- files: [...new Set(opts.files)].sort()
5060
+ files: [...new Set(opts.files)].sort(),
5061
+ ...opts.operations !== void 0 ? { operations: opts.operations } : {}
5060
5062
  };
5061
5063
  const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
5062
5064
  await fs.mkdir(dir, { recursive: true });
@@ -5138,7 +5140,7 @@ async function removeEmptyParents(startDir, stopDir) {
5138
5140
  async function generate(spec, emitter, options) {
5139
5141
  const targetManifestForCtx = options.target && !options.noPrune ? await readManifest(options.target) : null;
5140
5142
  const priorTargetManifestPaths = targetManifestForCtx ? new Set(targetManifestForCtx.files) : void 0;
5141
- const { files: withHeaders, header } = generateFiles(spec, emitter, {
5143
+ const { files: withHeaders, header, operations } = generateFiles(spec, emitter, {
5142
5144
  ...options,
5143
5145
  priorTargetManifestPaths
5144
5146
  });
@@ -5164,7 +5166,8 @@ async function generate(spec, emitter, options) {
5164
5166
  currentPaths: outputEmittedPaths,
5165
5167
  language: emitter.language,
5166
5168
  header,
5167
- noPrune: options.noPrune
5169
+ noPrune: options.noPrune,
5170
+ operations
5168
5171
  });
5169
5172
  const allOutputFiles = [...writeResult.written, ...writeResult.merged];
5170
5173
  if (allOutputFiles.length > 0) await formatTargetFiles(emitter, options.outputDir, allOutputFiles);
@@ -5186,7 +5189,8 @@ async function generate(spec, emitter, options) {
5186
5189
  currentPaths: targetResult.emittedPaths,
5187
5190
  language: emitter.language,
5188
5191
  header,
5189
- noPrune: options.noPrune
5192
+ noPrune: options.noPrune,
5193
+ operations
5190
5194
  });
5191
5195
  const allTargetFiles = [
5192
5196
  ...targetResult.written,
@@ -5202,19 +5206,18 @@ async function generate(spec, emitter, options) {
5202
5206
  * then write the fresh manifest. Safe on first adoption (no previous manifest).
5203
5207
  */
5204
5208
  async function applyManifestPrune(opts) {
5209
+ const manifestOpts = {
5210
+ language: opts.language,
5211
+ files: opts.currentPaths,
5212
+ operations: opts.operations
5213
+ };
5205
5214
  if (opts.noPrune) {
5206
- await writeManifest(opts.dir, {
5207
- language: opts.language,
5208
- files: opts.currentPaths
5209
- });
5215
+ await writeManifest(opts.dir, manifestOpts);
5210
5216
  return;
5211
5217
  }
5212
5218
  if (!opts.prevManifest) {
5213
5219
  console.log(`${opts.label}: no ${MANIFEST_FILENAME} found — skipping prune (this baseline manifest will be written now).`);
5214
- await writeManifest(opts.dir, {
5215
- language: opts.language,
5216
- files: opts.currentPaths
5217
- });
5220
+ await writeManifest(opts.dir, manifestOpts);
5218
5221
  return;
5219
5222
  }
5220
5223
  const stale = computeStalePaths(opts.prevManifest, opts.currentPaths);
@@ -5226,10 +5229,7 @@ async function applyManifestPrune(opts) {
5226
5229
  for (const p of preserved) console.log(` ${p}`);
5227
5230
  }
5228
5231
  }
5229
- await writeManifest(opts.dir, {
5230
- language: opts.language,
5231
- files: opts.currentPaths
5232
- });
5232
+ await writeManifest(opts.dir, manifestOpts);
5233
5233
  }
5234
5234
  //#endregion
5235
5235
  //#region src/engine/registry.ts
@@ -5740,7 +5740,7 @@ function patchOverlay(overlay, changes, baseline) {
5740
5740
  if (baseClass && baseClass.methods[methodName]?.length > 0) {
5741
5741
  const httpKey = overlay.httpKeyByMethod.get(`${className}.${methodName}`);
5742
5742
  if (!httpKey && overlay.httpKeyByMethod.size === 0 && !warnedManifest) {
5743
- 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.");
5743
+ console.warn("Warning: No operations map in .oagen-manifest.json. Method-level violations cannot be auto-patched. Implement buildOperationsMap in your emitter for the self-correcting loop to resolve these.");
5744
5744
  warnedManifest = true;
5745
5745
  }
5746
5746
  if (httpKey) {
@@ -5774,6 +5774,49 @@ function getExtractor(language) {
5774
5774
  return extractor;
5775
5775
  }
5776
5776
  //#endregion
5777
+ //#region src/engine/manifest-reader.ts
5778
+ /**
5779
+ * Read the operations map from `.oagen-manifest.json` in the given directory
5780
+ * and convert it to `ManifestEntry[]` for the overlay builder.
5781
+ *
5782
+ * Returns undefined if the manifest is absent or has no operations field.
5783
+ */
5784
+ function readManifestSync(dir) {
5785
+ const manifestPath = path$1.join(dir, MANIFEST_FILENAME);
5786
+ if (!existsSync(manifestPath)) return void 0;
5787
+ try {
5788
+ const operations = JSON.parse(readFileSync(manifestPath, "utf-8"))?.operations;
5789
+ if (!operations || typeof operations !== "object") return void 0;
5790
+ return convertOperationsToManifestEntries(operations);
5791
+ } catch {
5792
+ return;
5793
+ }
5794
+ }
5795
+ /** Convert the operations map from .oagen-manifest.json into ManifestEntry[]. */
5796
+ function convertOperationsToManifestEntries(operations) {
5797
+ const entries = [];
5798
+ for (const [httpKey, value] of Object.entries(operations)) {
5799
+ const spaceIdx = httpKey.indexOf(" ");
5800
+ const httpMethod = httpKey.slice(0, spaceIdx);
5801
+ const httpPath = httpKey.slice(spaceIdx + 1);
5802
+ const values = Array.isArray(value) ? value : [value];
5803
+ for (const v of values) {
5804
+ const entry = v;
5805
+ entries.push({
5806
+ operationId: "",
5807
+ sdkResourceProperty: entry.service ?? "",
5808
+ sdkMethodName: entry.sdkMethod ?? "",
5809
+ httpMethod,
5810
+ path: httpPath,
5811
+ pathParams: [],
5812
+ bodyFields: [],
5813
+ queryFields: []
5814
+ });
5815
+ }
5816
+ }
5817
+ return entries;
5818
+ }
5819
+ //#endregion
5777
5820
  //#region src/cli/overlay-loader.ts
5778
5821
  /**
5779
5822
  * Load an API surface from disk, discover/load the manifest, resolve
@@ -5792,30 +5835,7 @@ function loadOverlayContext(opts) {
5792
5835
  } catch (err) {
5793
5836
  throw new ConfigError(`Failed to parse API surface JSON: ${err instanceof Error ? err.message : String(err)}`, `Ensure "${opts.apiSurfacePath}" contains valid JSON. Re-run \`oagen extract\` to regenerate it.`);
5794
5837
  }
5795
- let manifest;
5796
- const resolvedManifestPath = opts.manifestPath ?? path$1.join(opts.outputDir, "smoke-manifest.json");
5797
- if (opts.manifestPath || existsSync(resolvedManifestPath)) try {
5798
- const parsed = JSON.parse(readFileSync(resolvedManifestPath, "utf-8"));
5799
- if (Array.isArray(parsed)) manifest = parsed;
5800
- else if (typeof parsed === "object" && parsed !== null) manifest = Object.entries(parsed).map(([httpKey, value]) => {
5801
- const spaceIdx = httpKey.indexOf(" ");
5802
- const httpMethod = httpKey.slice(0, spaceIdx);
5803
- const httpPath = httpKey.slice(spaceIdx + 1);
5804
- const v = value;
5805
- return {
5806
- operationId: "",
5807
- sdkResourceProperty: v.service ?? "",
5808
- sdkMethodName: v.sdkMethod ?? "",
5809
- httpMethod,
5810
- path: httpPath,
5811
- pathParams: [],
5812
- bodyFields: [],
5813
- queryFields: []
5814
- };
5815
- });
5816
- } catch {
5817
- if (opts.manifestPath) throw new ConfigError(`Failed to read manifest: ${resolvedManifestPath}`, `Verify the manifest file exists and contains valid JSON. If using --manifest, check the path is correct.`);
5818
- }
5838
+ const manifest = readManifestSync(opts.outputDir);
5819
5839
  let hints = nodeHints;
5820
5840
  try {
5821
5841
  hints = getExtractor(opts.lang).hints;
@@ -5896,7 +5916,6 @@ async function generateCommand(opts) {
5896
5916
  if (opts.apiSurface && opts.compatCheck !== false) {
5897
5917
  const ctx = loadOverlayContext({
5898
5918
  apiSurfacePath: opts.apiSurface,
5899
- manifestPath: opts.manifest,
5900
5919
  outputDir: opts.output,
5901
5920
  lang: opts.lang,
5902
5921
  spec: ir
@@ -6294,48 +6313,92 @@ async function extractCommand(opts) {
6294
6313
  console.log(`Extracted ${symbolCount} symbols → ${opts.output}`);
6295
6314
  }
6296
6315
  //#endregion
6297
- //#region src/cli/compat-extract.ts
6298
- const SNAPSHOT_FILENAME = ".oagen-compat-snapshot.json";
6299
- async function compatExtractCommand(opts) {
6300
- const extractor = getExtractor(opts.lang);
6301
- console.log(`Extracting ${opts.lang} compat snapshot from ${opts.sdkPath}...`);
6302
- const snapshot = await extractor.extractSnapshot(opts.sdkPath);
6303
- if (opts.spec) {
6304
- const specContent = readFileSync(opts.spec, "utf-8");
6305
- snapshot.source.specSha = createHash("sha256").update(specContent).digest("hex");
6306
- enrichWithSpecContext(snapshot, await parseSpec(opts.spec));
6316
+ //#region src/compat/policy.ts
6317
+ /** Built-in language defaults. */
6318
+ const LANGUAGE_DEFAULTS = {
6319
+ php: {
6320
+ callerUsesParamNames: true,
6321
+ constructorOrderMatters: true,
6322
+ methodParameterNamesArePublicApi: true,
6323
+ constructorParameterNamesArePublicApi: true,
6324
+ overloadsArePublicApi: false,
6325
+ arityIsPublicApi: false
6326
+ },
6327
+ python: {
6328
+ callerUsesParamNames: true,
6329
+ constructorOrderMatters: true,
6330
+ methodParameterNamesArePublicApi: true,
6331
+ constructorParameterNamesArePublicApi: true,
6332
+ overloadsArePublicApi: false,
6333
+ arityIsPublicApi: false
6334
+ },
6335
+ ruby: {
6336
+ callerUsesParamNames: true,
6337
+ constructorOrderMatters: true,
6338
+ methodParameterNamesArePublicApi: true,
6339
+ constructorParameterNamesArePublicApi: true,
6340
+ overloadsArePublicApi: false,
6341
+ arityIsPublicApi: false
6342
+ },
6343
+ go: {
6344
+ callerUsesParamNames: false,
6345
+ constructorOrderMatters: true,
6346
+ methodParameterNamesArePublicApi: false,
6347
+ constructorParameterNamesArePublicApi: false,
6348
+ overloadsArePublicApi: false,
6349
+ arityIsPublicApi: true
6350
+ },
6351
+ kotlin: {
6352
+ callerUsesParamNames: true,
6353
+ constructorOrderMatters: false,
6354
+ methodParameterNamesArePublicApi: true,
6355
+ constructorParameterNamesArePublicApi: true,
6356
+ overloadsArePublicApi: true,
6357
+ arityIsPublicApi: false
6358
+ },
6359
+ dotnet: {
6360
+ callerUsesParamNames: true,
6361
+ constructorOrderMatters: false,
6362
+ methodParameterNamesArePublicApi: true,
6363
+ constructorParameterNamesArePublicApi: true,
6364
+ overloadsArePublicApi: true,
6365
+ arityIsPublicApi: false
6366
+ },
6367
+ elixir: {
6368
+ callerUsesParamNames: true,
6369
+ constructorOrderMatters: false,
6370
+ methodParameterNamesArePublicApi: true,
6371
+ constructorParameterNamesArePublicApi: false,
6372
+ overloadsArePublicApi: false,
6373
+ arityIsPublicApi: true
6374
+ },
6375
+ rust: {
6376
+ callerUsesParamNames: false,
6377
+ constructorOrderMatters: true,
6378
+ methodParameterNamesArePublicApi: false,
6379
+ constructorParameterNamesArePublicApi: false,
6380
+ overloadsArePublicApi: false,
6381
+ arityIsPublicApi: true
6382
+ },
6383
+ node: {
6384
+ callerUsesParamNames: false,
6385
+ constructorOrderMatters: false,
6386
+ methodParameterNamesArePublicApi: false,
6387
+ constructorParameterNamesArePublicApi: false,
6388
+ overloadsArePublicApi: false,
6389
+ arityIsPublicApi: false
6307
6390
  }
6308
- const outputPath = resolve(opts.output, SNAPSHOT_FILENAME);
6309
- writeFileSync(outputPath, JSON.stringify(snapshot, null, 2));
6310
- console.log(`Extracted ${snapshot.symbols.length} symbols → ${outputPath}`);
6391
+ };
6392
+ /** Get the built-in default policy for a language. */
6393
+ function getDefaultPolicy(language) {
6394
+ return { ...LANGUAGE_DEFAULTS[language] };
6311
6395
  }
6312
- /**
6313
- * Enrich snapshot symbols with operationId and route from the parsed spec.
6314
- *
6315
- * Matches callable symbols to spec operations by comparing method names
6316
- * derived from the spec against the symbol's fqName.
6317
- */
6318
- function enrichWithSpecContext(snapshot, spec) {
6319
- const opLookup = /* @__PURE__ */ new Map();
6320
- for (const service of spec.services) for (const op of service.operations) {
6321
- const key = `${service.name}.${op.name}`;
6322
- opLookup.set(key, {
6323
- operationId: op.name,
6324
- method: op.httpMethod,
6325
- path: op.path
6326
- });
6327
- }
6328
- for (const sym of snapshot.symbols) {
6329
- if (sym.kind !== "callable") continue;
6330
- const match = opLookup.get(sym.fqName);
6331
- if (match) {
6332
- sym.operationId = match.operationId;
6333
- sym.route = {
6334
- method: match.method,
6335
- path: match.path
6336
- };
6337
- }
6338
- }
6396
+ /** Merge user overrides onto language defaults. Only provided keys are overridden. */
6397
+ function mergePolicy(defaults, overrides) {
6398
+ return {
6399
+ ...defaults,
6400
+ ...overrides
6401
+ };
6339
6402
  }
6340
6403
  //#endregion
6341
6404
  //#region src/compat/schema.ts
@@ -6346,93 +6409,380 @@ function validateSnapshot(data) {
6346
6409
  return typeof obj.schemaVersion === "string" && typeof obj.source === "object" && obj.source !== null && typeof obj.policies === "object" && obj.policies !== null && Array.isArray(obj.symbols);
6347
6410
  }
6348
6411
  //#endregion
6349
- //#region src/compat/config.ts
6350
- const BREAKING_CATEGORIES = new Set([
6351
- "symbol_removed",
6352
- "symbol_renamed",
6353
- "parameter_removed",
6354
- "parameter_renamed",
6355
- "parameter_requiredness_increased",
6356
- "parameter_type_narrowed",
6357
- "parameter_position_changed_order_sensitive",
6358
- "constructor_position_changed_order_sensitive",
6359
- "named_arg_name_removed",
6360
- "keyword_name_removed",
6361
- "overload_removed",
6362
- "union_wrapper_migration_without_compat_alias",
6363
- "field_type_changed",
6364
- "return_type_changed",
6365
- "enum_member_value_changed"
6366
- ]);
6367
- const SOFT_RISK_CATEGORIES = new Set([
6368
- "parameter_added_non_terminal_optional",
6369
- "constructor_reordered_named_friendly",
6370
- "default_value_changed",
6371
- "wrapper_stricter_than_previous_sdk_but_matches_spec",
6372
- "doc_surface_drift"
6373
- ]);
6374
- /** Get the default severity for a change category. */
6375
- function defaultSeverityForCategory(category) {
6376
- if (BREAKING_CATEGORIES.has(category)) return "breaking";
6377
- if (SOFT_RISK_CATEGORIES.has(category)) return "soft-risk";
6378
- return "additive";
6379
- }
6380
- /** Check whether a severity meets or exceeds a fail threshold. */
6381
- function severityMeetsThreshold(severity, threshold) {
6382
- if (threshold === "none") return false;
6383
- if (threshold === "soft-risk") return severity === "breaking" || severity === "soft-risk";
6384
- return severity === "breaking";
6385
- }
6386
- //#endregion
6387
- //#region src/compat/classify.ts
6412
+ //#region src/compat/ir.ts
6388
6413
  /**
6389
- * Classify changes between a baseline and candidate symbol.
6390
- * Returns one or more classified changes for the diff.
6414
+ * Convert a legacy ApiSurface to a CompatSnapshot.
6415
+ *
6416
+ * This bridge allows existing extractors (which produce ApiSurface) to feed
6417
+ * into the new classified diff engine without requiring immediate migration.
6391
6418
  */
6392
- function classifySymbolChanges(baseline, candidate, policy) {
6393
- const changes = [];
6394
- if (!candidate) {
6395
- changes.push(makeChange({
6396
- category: "symbol_removed",
6397
- symbol: baseline.fqName,
6398
- old: { symbol: baseline.fqName },
6399
- new: { symbol: "(removed)" },
6400
- message: `Symbol "${baseline.displayName}" was removed`,
6401
- policy
6402
- }));
6403
- return changes;
6404
- }
6405
- if (baseline.fqName !== candidate.fqName && baseline.id === candidate.id) changes.push(makeChange({
6406
- category: "symbol_renamed",
6407
- symbol: baseline.fqName,
6408
- old: { name: baseline.fqName },
6409
- new: { name: candidate.fqName },
6410
- message: `Symbol renamed from "${baseline.displayName}" to "${candidate.displayName}"`,
6411
- policy
6412
- }));
6413
- if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy));
6414
- if (baseline.returns && candidate.returns && baseline.returns.name !== candidate.returns.name) changes.push(makeChange({
6415
- category: "return_type_changed",
6416
- symbol: baseline.fqName,
6417
- old: { returnType: baseline.returns.name },
6418
- new: { returnType: candidate.returns.name },
6419
- message: `Return type changed for "${baseline.displayName}" from "${baseline.returns.name}" to "${candidate.returns.name}"`,
6420
- policy
6421
- }));
6422
- if (baseline.typeRef && candidate.typeRef && baseline.typeRef.name !== candidate.typeRef.name) changes.push(makeChange({
6423
- category: "field_type_changed",
6424
- symbol: baseline.fqName,
6425
- old: { type: baseline.typeRef.name },
6426
- new: { type: candidate.typeRef.name },
6427
- message: `Type changed for "${baseline.displayName}" from "${baseline.typeRef.name}" to "${candidate.typeRef.name}"`,
6428
- policy
6429
- }));
6430
- if (baseline.kind === "enum_member" && candidate.kind === "enum_member" && baseline.value !== void 0 && candidate.value !== void 0 && baseline.value !== candidate.value) changes.push(makeChange({
6431
- category: "enum_member_value_changed",
6432
- symbol: baseline.fqName,
6433
- old: { value: String(baseline.value) },
6434
- new: { value: String(candidate.value) },
6435
- message: `Enum value changed for "${baseline.displayName}" from "${baseline.value}" to "${candidate.value}"`,
6419
+ function apiSurfaceToSnapshot(surface) {
6420
+ const language = surface.language || "node";
6421
+ const symbols = [];
6422
+ for (const [className, cls] of Object.entries(surface.classes)) {
6423
+ symbols.push({
6424
+ id: `class:${className}`,
6425
+ kind: "service_accessor",
6426
+ fqName: className,
6427
+ displayName: className,
6428
+ visibility: "public",
6429
+ stability: cls.deprecationMessage ? "deprecated" : "stable",
6430
+ sourceKind: "generated_service_wrapper"
6431
+ });
6432
+ if (cls.constructorParams.length > 0) symbols.push({
6433
+ id: `ctor:${className}`,
6434
+ kind: "constructor",
6435
+ fqName: `${className}.constructor`,
6436
+ ownerFqName: className,
6437
+ displayName: `new ${className}`,
6438
+ visibility: "public",
6439
+ stability: "stable",
6440
+ sourceKind: "generated_resource_constructor",
6441
+ parameters: cls.constructorParams.map((p, i) => apiParamToCompatParam(p, i, language))
6442
+ });
6443
+ for (const [methodName, overloads] of Object.entries(cls.methods)) for (let oi = 0; oi < overloads.length; oi++) {
6444
+ const method = overloads[oi];
6445
+ const suffix = overloads.length > 1 ? `#${oi}` : "";
6446
+ symbols.push({
6447
+ id: `method:${className}.${methodName}${suffix}`,
6448
+ kind: "callable",
6449
+ fqName: `${className}.${methodName}`,
6450
+ ownerFqName: className,
6451
+ displayName: `${className}.${methodName}`,
6452
+ visibility: "public",
6453
+ stability: "stable",
6454
+ sourceKind: "generated_service_wrapper",
6455
+ parameters: method.params.map((p, i) => apiParamToCompatParam(p, i, language)),
6456
+ returns: { name: method.returnType }
6457
+ });
6458
+ }
6459
+ for (const [propName, prop] of Object.entries(cls.properties)) symbols.push({
6460
+ id: `prop:${className}.${propName}`,
6461
+ kind: "property",
6462
+ fqName: `${className}.${propName}`,
6463
+ ownerFqName: className,
6464
+ displayName: `${className}.${propName}`,
6465
+ visibility: "public",
6466
+ stability: "stable",
6467
+ sourceKind: "generated_service_wrapper",
6468
+ typeRef: { name: prop.type }
6469
+ });
6470
+ }
6471
+ for (const [ifaceName, iface] of Object.entries(surface.interfaces)) {
6472
+ symbols.push({
6473
+ id: `iface:${ifaceName}`,
6474
+ kind: "alias",
6475
+ fqName: ifaceName,
6476
+ displayName: ifaceName,
6477
+ visibility: "public",
6478
+ stability: "stable",
6479
+ sourceKind: "generated_resource_constructor"
6480
+ });
6481
+ for (const [fieldName, field] of Object.entries(iface.fields)) symbols.push({
6482
+ id: `field:${ifaceName}.${fieldName}`,
6483
+ kind: "field",
6484
+ fqName: `${ifaceName}.${fieldName}`,
6485
+ ownerFqName: ifaceName,
6486
+ displayName: `${ifaceName}.${fieldName}`,
6487
+ visibility: "public",
6488
+ stability: "stable",
6489
+ sourceKind: "generated_resource_constructor",
6490
+ typeRef: { name: field.type }
6491
+ });
6492
+ if (iface.hasCustomConstructor) symbols.push({
6493
+ id: `ctor:${ifaceName}`,
6494
+ kind: "constructor",
6495
+ fqName: `${ifaceName}.constructor`,
6496
+ ownerFqName: ifaceName,
6497
+ displayName: `new ${ifaceName}`,
6498
+ visibility: "public",
6499
+ stability: "stable",
6500
+ sourceKind: "generated_resource_constructor"
6501
+ });
6502
+ }
6503
+ for (const [aliasName] of Object.entries(surface.typeAliases)) symbols.push({
6504
+ id: `alias:${aliasName}`,
6505
+ kind: "alias",
6506
+ fqName: aliasName,
6507
+ displayName: aliasName,
6508
+ visibility: "public",
6509
+ stability: "stable",
6510
+ sourceKind: "generated_resource_constructor"
6511
+ });
6512
+ for (const [enumName, enumDef] of Object.entries(surface.enums)) {
6513
+ symbols.push({
6514
+ id: `enum:${enumName}`,
6515
+ kind: "enum",
6516
+ fqName: enumName,
6517
+ displayName: enumName,
6518
+ visibility: "public",
6519
+ stability: "stable",
6520
+ sourceKind: "generated_enum"
6521
+ });
6522
+ for (const [memberName, memberValue] of Object.entries(enumDef.members)) symbols.push({
6523
+ id: `enum_member:${enumName}.${memberName}`,
6524
+ kind: "enum_member",
6525
+ fqName: `${enumName}.${memberName}`,
6526
+ ownerFqName: enumName,
6527
+ displayName: `${enumName}.${memberName}`,
6528
+ visibility: "public",
6529
+ stability: "stable",
6530
+ sourceKind: "generated_enum",
6531
+ value: memberValue
6532
+ });
6533
+ }
6534
+ return {
6535
+ schemaVersion: "1",
6536
+ source: { extractedAt: surface.extractedAt },
6537
+ policies: getDefaultPolicy(language),
6538
+ symbols
6539
+ };
6540
+ }
6541
+ /** Convert a legacy ApiParam to a CompatParameter. */
6542
+ function apiParamToCompatParam(param, position, language) {
6543
+ const policy = getDefaultPolicy(language);
6544
+ return {
6545
+ publicName: param.name,
6546
+ position,
6547
+ required: !param.optional,
6548
+ nullable: false,
6549
+ hasDefault: param.optional,
6550
+ passing: param.passingStyle ?? inferPassingStyle(language),
6551
+ type: { name: param.type },
6552
+ sensitivity: {
6553
+ order: policy.constructorOrderMatters,
6554
+ publicName: policy.methodParameterNamesArePublicApi,
6555
+ requiredness: true,
6556
+ type: true
6557
+ }
6558
+ };
6559
+ }
6560
+ /** Infer the default parameter passing style for a language. */
6561
+ function inferPassingStyle(language) {
6562
+ switch (language) {
6563
+ case "python":
6564
+ case "ruby":
6565
+ case "elixir": return "keyword";
6566
+ case "php":
6567
+ case "kotlin":
6568
+ case "dotnet": return "named";
6569
+ case "node": return "options_object";
6570
+ default: return "positional";
6571
+ }
6572
+ }
6573
+ //#endregion
6574
+ //#region src/cli/compat-extract.ts
6575
+ const SNAPSHOT_FILENAME = ".oagen-compat-snapshot.json";
6576
+ /** Normalize a name for case-insensitive matching: strip underscores and lowercase. */
6577
+ function normalize(name) {
6578
+ return name.replace(/_/g, "").toLowerCase();
6579
+ }
6580
+ /**
6581
+ * Build a map of normalized service name → Set of generated method names
6582
+ * from the manifest's operations record.
6583
+ */
6584
+ function buildServiceMethodMap(operations) {
6585
+ const result = /* @__PURE__ */ new Map();
6586
+ for (const value of Object.values(operations)) {
6587
+ if (typeof value !== "object" || value === null) continue;
6588
+ const { sdkMethod, service } = value;
6589
+ if (typeof sdkMethod !== "string" || typeof service !== "string") continue;
6590
+ const key = normalize(service);
6591
+ if (!result.has(key)) result.set(key, /* @__PURE__ */ new Set());
6592
+ result.get(key).add(sdkMethod);
6593
+ }
6594
+ return result;
6595
+ }
6596
+ /**
6597
+ * Filter an ApiSurface to only include generated symbols based on the manifest.
6598
+ *
6599
+ * Two levels of filtering:
6600
+ * 1. File-level: classes/interfaces/enums from files not in the manifest are
6601
+ * dropped entirely (they are wholly hand-written).
6602
+ * 2. Method-level: for service classes (those whose name matches a manifest
6603
+ * service), only methods listed in the manifest operations are kept.
6604
+ * Constructors and properties are always preserved since they are structural.
6605
+ * Non-service classes (models, enums, etc.) keep all their symbols.
6606
+ */
6607
+ function filterSurfaceByManifest(surface, manifest) {
6608
+ const manifestFiles = new Set(manifest.files);
6609
+ const serviceMethodMap = manifest.operations ? buildServiceMethodMap(manifest.operations) : /* @__PURE__ */ new Map();
6610
+ let filesExcluded = 0;
6611
+ let methodsExcluded = 0;
6612
+ const filterByFile = (record) => {
6613
+ const result = {};
6614
+ for (const [name, entry] of Object.entries(record)) if (entry.sourceFile && manifestFiles.has(entry.sourceFile)) result[name] = entry;
6615
+ else filesExcluded++;
6616
+ return result;
6617
+ };
6618
+ const filteredClasses = {};
6619
+ for (const [name, cls] of Object.entries(surface.classes)) {
6620
+ if (cls.sourceFile && !manifestFiles.has(cls.sourceFile)) {
6621
+ filesExcluded++;
6622
+ continue;
6623
+ }
6624
+ const generatedMethods = serviceMethodMap.get(normalize(name));
6625
+ if (generatedMethods) {
6626
+ const methods = {};
6627
+ for (const [methodName, overloads] of Object.entries(cls.methods)) if (generatedMethods.has(methodName)) methods[methodName] = overloads;
6628
+ else methodsExcluded++;
6629
+ filteredClasses[name] = {
6630
+ ...cls,
6631
+ methods
6632
+ };
6633
+ } else filteredClasses[name] = cls;
6634
+ }
6635
+ const filteredExports = {};
6636
+ for (const [filePath, symbols] of Object.entries(surface.exports)) if (manifestFiles.has(filePath)) filteredExports[filePath] = symbols;
6637
+ return {
6638
+ filtered: {
6639
+ ...surface,
6640
+ classes: filteredClasses,
6641
+ interfaces: filterByFile(surface.interfaces),
6642
+ typeAliases: filterByFile(surface.typeAliases),
6643
+ enums: filterByFile(surface.enums),
6644
+ exports: filteredExports
6645
+ },
6646
+ filesExcluded,
6647
+ methodsExcluded
6648
+ };
6649
+ }
6650
+ async function compatExtractCommand(opts) {
6651
+ const extractor = getExtractor(opts.lang);
6652
+ console.log(`Extracting ${opts.lang} compat snapshot from ${opts.sdkPath}...`);
6653
+ let surface = await extractor.extract(opts.sdkPath);
6654
+ const manifest = await readManifest(opts.sdkPath);
6655
+ if (manifest) {
6656
+ const { filtered, filesExcluded, methodsExcluded } = filterSurfaceByManifest(surface, manifest);
6657
+ surface = filtered;
6658
+ if (filesExcluded > 0 || methodsExcluded > 0) console.log(`Manifest filter: excluded ${filesExcluded} hand-written file(s), ${methodsExcluded} hand-written method(s)`);
6659
+ }
6660
+ const snapshot = apiSurfaceToSnapshot(surface);
6661
+ if (opts.spec) {
6662
+ const specContent = readFileSync(opts.spec, "utf-8");
6663
+ snapshot.source.specSha = createHash("sha256").update(specContent).digest("hex");
6664
+ enrichWithSpecContext(snapshot, await parseSpec(opts.spec));
6665
+ }
6666
+ const outputPath = resolve(opts.output, SNAPSHOT_FILENAME);
6667
+ writeFileSync(outputPath, JSON.stringify(snapshot, null, 2));
6668
+ console.log(`Extracted ${snapshot.symbols.length} symbols → ${outputPath}`);
6669
+ }
6670
+ /**
6671
+ * Enrich snapshot symbols with operationId and route from the parsed spec.
6672
+ *
6673
+ * Matches callable symbols to spec operations by comparing method names
6674
+ * derived from the spec against the symbol's fqName.
6675
+ */
6676
+ function enrichWithSpecContext(snapshot, spec) {
6677
+ const opLookup = /* @__PURE__ */ new Map();
6678
+ for (const service of spec.services) for (const op of service.operations) {
6679
+ const key = `${service.name}.${op.name}`;
6680
+ opLookup.set(key, {
6681
+ operationId: op.name,
6682
+ method: op.httpMethod,
6683
+ path: op.path
6684
+ });
6685
+ }
6686
+ for (const sym of snapshot.symbols) {
6687
+ if (sym.kind !== "callable") continue;
6688
+ const match = opLookup.get(sym.fqName);
6689
+ if (match) {
6690
+ sym.operationId = match.operationId;
6691
+ sym.route = {
6692
+ method: match.method,
6693
+ path: match.path
6694
+ };
6695
+ }
6696
+ }
6697
+ }
6698
+ //#endregion
6699
+ //#region src/compat/config.ts
6700
+ const BREAKING_CATEGORIES = new Set([
6701
+ "symbol_removed",
6702
+ "symbol_renamed",
6703
+ "parameter_removed",
6704
+ "parameter_renamed",
6705
+ "parameter_requiredness_increased",
6706
+ "parameter_type_narrowed",
6707
+ "parameter_position_changed_order_sensitive",
6708
+ "constructor_position_changed_order_sensitive",
6709
+ "named_arg_name_removed",
6710
+ "keyword_name_removed",
6711
+ "overload_removed",
6712
+ "union_wrapper_migration_without_compat_alias",
6713
+ "field_type_changed",
6714
+ "return_type_changed",
6715
+ "enum_member_value_changed"
6716
+ ]);
6717
+ const SOFT_RISK_CATEGORIES = new Set([
6718
+ "parameter_added_non_terminal_optional",
6719
+ "constructor_reordered_named_friendly",
6720
+ "default_value_changed",
6721
+ "wrapper_stricter_than_previous_sdk_but_matches_spec",
6722
+ "doc_surface_drift"
6723
+ ]);
6724
+ /** Get the default severity for a change category. */
6725
+ function defaultSeverityForCategory(category) {
6726
+ if (BREAKING_CATEGORIES.has(category)) return "breaking";
6727
+ if (SOFT_RISK_CATEGORIES.has(category)) return "soft-risk";
6728
+ return "additive";
6729
+ }
6730
+ /** Check whether a severity meets or exceeds a fail threshold. */
6731
+ function severityMeetsThreshold(severity, threshold) {
6732
+ if (threshold === "none") return false;
6733
+ if (threshold === "soft-risk") return severity === "breaking" || severity === "soft-risk";
6734
+ return severity === "breaking";
6735
+ }
6736
+ //#endregion
6737
+ //#region src/compat/classify.ts
6738
+ /**
6739
+ * Classify changes between a baseline and candidate symbol.
6740
+ * Returns one or more classified changes for the diff.
6741
+ */
6742
+ function classifySymbolChanges(baseline, candidate, policy) {
6743
+ const changes = [];
6744
+ if (!candidate) {
6745
+ changes.push(makeChange({
6746
+ category: "symbol_removed",
6747
+ symbol: baseline.fqName,
6748
+ old: { symbol: baseline.fqName },
6749
+ new: { symbol: "(removed)" },
6750
+ message: `Symbol "${baseline.displayName}" was removed`,
6751
+ policy
6752
+ }));
6753
+ return changes;
6754
+ }
6755
+ if (baseline.fqName !== candidate.fqName && baseline.id === candidate.id) changes.push(makeChange({
6756
+ category: "symbol_renamed",
6757
+ symbol: baseline.fqName,
6758
+ old: { name: baseline.fqName },
6759
+ new: { name: candidate.fqName },
6760
+ message: `Symbol renamed from "${baseline.displayName}" to "${candidate.displayName}"`,
6761
+ policy
6762
+ }));
6763
+ if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy));
6764
+ if (baseline.returns && candidate.returns && baseline.returns.name !== candidate.returns.name) changes.push(makeChange({
6765
+ category: "return_type_changed",
6766
+ symbol: baseline.fqName,
6767
+ old: { returnType: baseline.returns.name },
6768
+ new: { returnType: candidate.returns.name },
6769
+ message: `Return type changed for "${baseline.displayName}" from "${baseline.returns.name}" to "${candidate.returns.name}"`,
6770
+ policy
6771
+ }));
6772
+ if (baseline.typeRef && candidate.typeRef && baseline.typeRef.name !== candidate.typeRef.name) changes.push(makeChange({
6773
+ category: "field_type_changed",
6774
+ symbol: baseline.fqName,
6775
+ old: { type: baseline.typeRef.name },
6776
+ new: { type: candidate.typeRef.name },
6777
+ message: `Type changed for "${baseline.displayName}" from "${baseline.typeRef.name}" to "${candidate.typeRef.name}"`,
6778
+ policy
6779
+ }));
6780
+ if (baseline.kind === "enum_member" && candidate.kind === "enum_member" && baseline.value !== void 0 && candidate.value !== void 0 && baseline.value !== candidate.value) changes.push(makeChange({
6781
+ category: "enum_member_value_changed",
6782
+ symbol: baseline.fqName,
6783
+ old: { value: String(baseline.value) },
6784
+ new: { value: String(candidate.value) },
6785
+ message: `Enum value changed for "${baseline.displayName}" from "${baseline.value}" to "${candidate.value}"`,
6436
6786
  policy
6437
6787
  }));
6438
6788
  return changes;
@@ -7043,342 +7393,92 @@ function formatCrossLanguageRollup(reports) {
7043
7393
  }
7044
7394
  function formatConceptualTable(concepts, languages) {
7045
7395
  const lines = [];
7046
- const langHeaders = languages.map((l) => l).join(" | ");
7047
- lines.push(`| Symbol | Category | ${langHeaders} |`);
7048
- lines.push(`| --- | --- | ${languages.map(() => "---").join(" | ")} |`);
7049
- for (const c of concepts) {
7050
- const severities = languages.map((l) => severityIcon(c.impact[l])).join(" | ");
7051
- lines.push(`| \`${c.symbol}\` | \`${c.category}\` | ${severities} |`);
7052
- }
7053
- return lines.join("\n");
7054
- }
7055
- function severityIcon(severity) {
7056
- if (!severity) return "—";
7057
- switch (severity) {
7058
- case "breaking": return ":x: breaking";
7059
- case "soft-risk": return ":warning: soft-risk";
7060
- case "additive": return ":white_check_mark: additive";
7061
- }
7062
- }
7063
- function formatChangesTable(changes) {
7064
- const lines = [];
7065
- lines.push("| Category | Symbol | Detail |");
7066
- lines.push("| --- | --- | --- |");
7067
- for (const c of changes) {
7068
- const detail = c.message ?? formatOldNew(c.old, c.new);
7069
- lines.push(`| \`${c.category}\` | \`${c.symbol}\` | ${escapeMarkdown(detail)} |`);
7070
- }
7071
- return lines.join("\n");
7072
- }
7073
- function formatOldNew(old, updated) {
7074
- const parts = [];
7075
- for (const key of Object.keys(old)) if (updated[key] && old[key] !== updated[key]) parts.push(`${key}: \`${old[key]}\` → \`${updated[key]}\``);
7076
- return parts.length > 0 ? parts.join(", ") : "";
7077
- }
7078
- function escapeMarkdown(text) {
7079
- return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
7080
- }
7081
- //#endregion
7082
- //#region src/compat/approvals.ts
7083
- /**
7084
- * Match a classified change against a list of approvals.
7085
- * Returns the first matching approval, or null if none match.
7086
- */
7087
- function matchApproval(change, approvals, language) {
7088
- for (const approval of approvals) if (approvalMatchesChange(approval, change, language)) return approval;
7089
- return null;
7090
- }
7091
- /**
7092
- * Filter changes to only those NOT covered by approvals.
7093
- * These are the remaining unapproved changes that may cause failure.
7094
- */
7095
- function unapprovedChanges(changes, approvals, language) {
7096
- return changes.filter((change) => !matchApproval(change, approvals, language));
7097
- }
7098
- function approvalMatchesChange(approval, change, language) {
7099
- if (approval.approved === false) return false;
7100
- if (approval.category !== change.category) return false;
7101
- if (!symbolMatches(approval.symbol, change.symbol)) return false;
7102
- if (!languageMatches(approval.appliesTo, language)) return false;
7103
- if (approval.match && !narrowingMatches(approval.match, change)) return false;
7104
- return true;
7105
- }
7106
- function symbolMatches(approvalSymbol, changeSymbol) {
7107
- if (approvalSymbol === changeSymbol) return true;
7108
- return approvalSymbol.replace(/::/g, ".").replace(/\\/g, ".") === changeSymbol.replace(/::/g, ".").replace(/\\/g, ".");
7109
- }
7110
- function languageMatches(appliesTo, language) {
7111
- if (!appliesTo || appliesTo === "all-impacted-languages") return true;
7112
- return appliesTo.includes(language);
7113
- }
7114
- function narrowingMatches(match, change) {
7115
- if (match.parameter) {
7116
- const paramInOld = change.old.parameter === match.parameter;
7117
- const paramInNew = change.new.parameter === match.parameter;
7118
- if (!paramInOld && !paramInNew) return false;
7119
- }
7120
- if (match.member) {
7121
- const memberInOld = change.old.member === match.member;
7122
- const memberInNew = change.new.member === match.member;
7123
- if (!memberInOld && !memberInNew) return false;
7124
- }
7125
- if (match.oldName) {
7126
- if (!Object.values(change.old).some((v) => v === match.oldName)) return false;
7127
- }
7128
- if (match.newName) {
7129
- if (!Object.values(change.new).some((v) => v === match.newName)) return false;
7130
- }
7131
- return true;
7132
- }
7133
- //#endregion
7134
- //#region src/compat/policy.ts
7135
- /** Built-in language defaults. */
7136
- const LANGUAGE_DEFAULTS = {
7137
- php: {
7138
- callerUsesParamNames: true,
7139
- constructorOrderMatters: true,
7140
- methodParameterNamesArePublicApi: true,
7141
- constructorParameterNamesArePublicApi: true,
7142
- overloadsArePublicApi: false,
7143
- arityIsPublicApi: false
7144
- },
7145
- python: {
7146
- callerUsesParamNames: true,
7147
- constructorOrderMatters: true,
7148
- methodParameterNamesArePublicApi: true,
7149
- constructorParameterNamesArePublicApi: true,
7150
- overloadsArePublicApi: false,
7151
- arityIsPublicApi: false
7152
- },
7153
- ruby: {
7154
- callerUsesParamNames: true,
7155
- constructorOrderMatters: true,
7156
- methodParameterNamesArePublicApi: true,
7157
- constructorParameterNamesArePublicApi: true,
7158
- overloadsArePublicApi: false,
7159
- arityIsPublicApi: false
7160
- },
7161
- go: {
7162
- callerUsesParamNames: false,
7163
- constructorOrderMatters: true,
7164
- methodParameterNamesArePublicApi: false,
7165
- constructorParameterNamesArePublicApi: false,
7166
- overloadsArePublicApi: false,
7167
- arityIsPublicApi: true
7168
- },
7169
- kotlin: {
7170
- callerUsesParamNames: true,
7171
- constructorOrderMatters: false,
7172
- methodParameterNamesArePublicApi: true,
7173
- constructorParameterNamesArePublicApi: true,
7174
- overloadsArePublicApi: true,
7175
- arityIsPublicApi: false
7176
- },
7177
- dotnet: {
7178
- callerUsesParamNames: true,
7179
- constructorOrderMatters: false,
7180
- methodParameterNamesArePublicApi: true,
7181
- constructorParameterNamesArePublicApi: true,
7182
- overloadsArePublicApi: true,
7183
- arityIsPublicApi: false
7184
- },
7185
- elixir: {
7186
- callerUsesParamNames: true,
7187
- constructorOrderMatters: false,
7188
- methodParameterNamesArePublicApi: true,
7189
- constructorParameterNamesArePublicApi: false,
7190
- overloadsArePublicApi: false,
7191
- arityIsPublicApi: true
7192
- },
7193
- rust: {
7194
- callerUsesParamNames: false,
7195
- constructorOrderMatters: true,
7196
- methodParameterNamesArePublicApi: false,
7197
- constructorParameterNamesArePublicApi: false,
7198
- overloadsArePublicApi: false,
7199
- arityIsPublicApi: true
7200
- },
7201
- node: {
7202
- callerUsesParamNames: false,
7203
- constructorOrderMatters: false,
7204
- methodParameterNamesArePublicApi: false,
7205
- constructorParameterNamesArePublicApi: false,
7206
- overloadsArePublicApi: false,
7207
- arityIsPublicApi: false
7396
+ const langHeaders = languages.map((l) => l).join(" | ");
7397
+ lines.push(`| Symbol | Category | ${langHeaders} |`);
7398
+ lines.push(`| --- | --- | ${languages.map(() => "---").join(" | ")} |`);
7399
+ for (const c of concepts) {
7400
+ const severities = languages.map((l) => severityIcon(c.impact[l])).join(" | ");
7401
+ lines.push(`| \`${c.symbol}\` | \`${c.category}\` | ${severities} |`);
7208
7402
  }
7209
- };
7210
- /** Get the built-in default policy for a language. */
7211
- function getDefaultPolicy(language) {
7212
- return { ...LANGUAGE_DEFAULTS[language] };
7403
+ return lines.join("\n");
7213
7404
  }
7214
- /** Merge user overrides onto language defaults. Only provided keys are overridden. */
7215
- function mergePolicy(defaults, overrides) {
7216
- return {
7217
- ...defaults,
7218
- ...overrides
7219
- };
7405
+ function severityIcon(severity) {
7406
+ if (!severity) return "—";
7407
+ switch (severity) {
7408
+ case "breaking": return ":x: breaking";
7409
+ case "soft-risk": return ":warning: soft-risk";
7410
+ case "additive": return ":white_check_mark: additive";
7411
+ }
7412
+ }
7413
+ function formatChangesTable(changes) {
7414
+ const lines = [];
7415
+ lines.push("| Category | Symbol | Detail |");
7416
+ lines.push("| --- | --- | --- |");
7417
+ for (const c of changes) {
7418
+ const detail = c.message ?? formatOldNew(c.old, c.new);
7419
+ lines.push(`| \`${c.category}\` | \`${c.symbol}\` | ${escapeMarkdown(detail)} |`);
7420
+ }
7421
+ return lines.join("\n");
7422
+ }
7423
+ function formatOldNew(old, updated) {
7424
+ const parts = [];
7425
+ for (const key of Object.keys(old)) if (updated[key] && old[key] !== updated[key]) parts.push(`${key}: \`${old[key]}\` → \`${updated[key]}\``);
7426
+ return parts.length > 0 ? parts.join(", ") : "";
7427
+ }
7428
+ function escapeMarkdown(text) {
7429
+ return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
7220
7430
  }
7221
7431
  //#endregion
7222
- //#region src/compat/ir.ts
7432
+ //#region src/compat/approvals.ts
7223
7433
  /**
7224
- * Convert a legacy ApiSurface to a CompatSnapshot.
7225
- *
7226
- * This bridge allows existing extractors (which produce ApiSurface) to feed
7227
- * into the new classified diff engine without requiring immediate migration.
7434
+ * Match a classified change against a list of approvals.
7435
+ * Returns the first matching approval, or null if none match.
7228
7436
  */
7229
- function apiSurfaceToSnapshot(surface) {
7230
- const language = surface.language || "node";
7231
- const symbols = [];
7232
- for (const [className, cls] of Object.entries(surface.classes)) {
7233
- symbols.push({
7234
- id: `class:${className}`,
7235
- kind: "service_accessor",
7236
- fqName: className,
7237
- displayName: className,
7238
- visibility: "public",
7239
- stability: cls.deprecationMessage ? "deprecated" : "stable",
7240
- sourceKind: "generated_service_wrapper"
7241
- });
7242
- if (cls.constructorParams.length > 0) symbols.push({
7243
- id: `ctor:${className}`,
7244
- kind: "constructor",
7245
- fqName: `${className}.constructor`,
7246
- ownerFqName: className,
7247
- displayName: `new ${className}`,
7248
- visibility: "public",
7249
- stability: "stable",
7250
- sourceKind: "generated_resource_constructor",
7251
- parameters: cls.constructorParams.map((p, i) => apiParamToCompatParam(p, i, language))
7252
- });
7253
- for (const [methodName, overloads] of Object.entries(cls.methods)) for (let oi = 0; oi < overloads.length; oi++) {
7254
- const method = overloads[oi];
7255
- const suffix = overloads.length > 1 ? `#${oi}` : "";
7256
- symbols.push({
7257
- id: `method:${className}.${methodName}${suffix}`,
7258
- kind: "callable",
7259
- fqName: `${className}.${methodName}`,
7260
- ownerFqName: className,
7261
- displayName: `${className}.${methodName}`,
7262
- visibility: "public",
7263
- stability: "stable",
7264
- sourceKind: "generated_service_wrapper",
7265
- parameters: method.params.map((p, i) => apiParamToCompatParam(p, i, language)),
7266
- returns: { name: method.returnType }
7267
- });
7268
- }
7269
- for (const [propName, prop] of Object.entries(cls.properties)) symbols.push({
7270
- id: `prop:${className}.${propName}`,
7271
- kind: "property",
7272
- fqName: `${className}.${propName}`,
7273
- ownerFqName: className,
7274
- displayName: `${className}.${propName}`,
7275
- visibility: "public",
7276
- stability: "stable",
7277
- sourceKind: "generated_service_wrapper",
7278
- typeRef: { name: prop.type }
7279
- });
7437
+ function matchApproval(change, approvals, language) {
7438
+ for (const approval of approvals) if (approvalMatchesChange(approval, change, language)) return approval;
7439
+ return null;
7440
+ }
7441
+ /**
7442
+ * Filter changes to only those NOT covered by approvals.
7443
+ * These are the remaining unapproved changes that may cause failure.
7444
+ */
7445
+ function unapprovedChanges(changes, approvals, language) {
7446
+ return changes.filter((change) => !matchApproval(change, approvals, language));
7447
+ }
7448
+ function approvalMatchesChange(approval, change, language) {
7449
+ if (approval.approved === false) return false;
7450
+ if (approval.category !== change.category) return false;
7451
+ if (!symbolMatches(approval.symbol, change.symbol)) return false;
7452
+ if (!languageMatches(approval.appliesTo, language)) return false;
7453
+ if (approval.match && !narrowingMatches(approval.match, change)) return false;
7454
+ return true;
7455
+ }
7456
+ function symbolMatches(approvalSymbol, changeSymbol) {
7457
+ if (approvalSymbol === changeSymbol) return true;
7458
+ return approvalSymbol.replace(/::/g, ".").replace(/\\/g, ".") === changeSymbol.replace(/::/g, ".").replace(/\\/g, ".");
7459
+ }
7460
+ function languageMatches(appliesTo, language) {
7461
+ if (!appliesTo || appliesTo === "all-impacted-languages") return true;
7462
+ return appliesTo.includes(language);
7463
+ }
7464
+ function narrowingMatches(match, change) {
7465
+ if (match.parameter) {
7466
+ const paramInOld = change.old.parameter === match.parameter;
7467
+ const paramInNew = change.new.parameter === match.parameter;
7468
+ if (!paramInOld && !paramInNew) return false;
7280
7469
  }
7281
- for (const [ifaceName, iface] of Object.entries(surface.interfaces)) {
7282
- symbols.push({
7283
- id: `iface:${ifaceName}`,
7284
- kind: "alias",
7285
- fqName: ifaceName,
7286
- displayName: ifaceName,
7287
- visibility: "public",
7288
- stability: "stable",
7289
- sourceKind: "generated_resource_constructor"
7290
- });
7291
- for (const [fieldName, field] of Object.entries(iface.fields)) symbols.push({
7292
- id: `field:${ifaceName}.${fieldName}`,
7293
- kind: "field",
7294
- fqName: `${ifaceName}.${fieldName}`,
7295
- ownerFqName: ifaceName,
7296
- displayName: `${ifaceName}.${fieldName}`,
7297
- visibility: "public",
7298
- stability: "stable",
7299
- sourceKind: "generated_resource_constructor",
7300
- typeRef: { name: field.type }
7301
- });
7302
- if (iface.hasCustomConstructor) symbols.push({
7303
- id: `ctor:${ifaceName}`,
7304
- kind: "constructor",
7305
- fqName: `${ifaceName}.constructor`,
7306
- ownerFqName: ifaceName,
7307
- displayName: `new ${ifaceName}`,
7308
- visibility: "public",
7309
- stability: "stable",
7310
- sourceKind: "generated_resource_constructor"
7311
- });
7470
+ if (match.member) {
7471
+ const memberInOld = change.old.member === match.member;
7472
+ const memberInNew = change.new.member === match.member;
7473
+ if (!memberInOld && !memberInNew) return false;
7312
7474
  }
7313
- for (const [aliasName] of Object.entries(surface.typeAliases)) symbols.push({
7314
- id: `alias:${aliasName}`,
7315
- kind: "alias",
7316
- fqName: aliasName,
7317
- displayName: aliasName,
7318
- visibility: "public",
7319
- stability: "stable",
7320
- sourceKind: "generated_resource_constructor"
7321
- });
7322
- for (const [enumName, enumDef] of Object.entries(surface.enums)) {
7323
- symbols.push({
7324
- id: `enum:${enumName}`,
7325
- kind: "enum",
7326
- fqName: enumName,
7327
- displayName: enumName,
7328
- visibility: "public",
7329
- stability: "stable",
7330
- sourceKind: "generated_enum"
7331
- });
7332
- for (const [memberName, memberValue] of Object.entries(enumDef.members)) symbols.push({
7333
- id: `enum_member:${enumName}.${memberName}`,
7334
- kind: "enum_member",
7335
- fqName: `${enumName}.${memberName}`,
7336
- ownerFqName: enumName,
7337
- displayName: `${enumName}.${memberName}`,
7338
- visibility: "public",
7339
- stability: "stable",
7340
- sourceKind: "generated_enum",
7341
- value: memberValue
7342
- });
7475
+ if (match.oldName) {
7476
+ if (!Object.values(change.old).some((v) => v === match.oldName)) return false;
7343
7477
  }
7344
- return {
7345
- schemaVersion: "1",
7346
- source: { extractedAt: surface.extractedAt },
7347
- policies: getDefaultPolicy(language),
7348
- symbols
7349
- };
7350
- }
7351
- /** Convert a legacy ApiParam to a CompatParameter. */
7352
- function apiParamToCompatParam(param, position, language) {
7353
- const policy = getDefaultPolicy(language);
7354
- return {
7355
- publicName: param.name,
7356
- position,
7357
- required: !param.optional,
7358
- nullable: false,
7359
- hasDefault: param.optional,
7360
- passing: param.passingStyle ?? inferPassingStyle(language),
7361
- type: { name: param.type },
7362
- sensitivity: {
7363
- order: policy.constructorOrderMatters,
7364
- publicName: policy.methodParameterNamesArePublicApi,
7365
- requiredness: true,
7366
- type: true
7367
- }
7368
- };
7369
- }
7370
- /** Infer the default parameter passing style for a language. */
7371
- function inferPassingStyle(language) {
7372
- switch (language) {
7373
- case "python":
7374
- case "ruby":
7375
- case "elixir": return "keyword";
7376
- case "php":
7377
- case "kotlin":
7378
- case "dotnet": return "named";
7379
- case "node": return "options_object";
7380
- default: return "positional";
7478
+ if (match.newName) {
7479
+ if (!Object.values(change.new).some((v) => v === match.newName)) return false;
7381
7480
  }
7481
+ return true;
7382
7482
  }
7383
7483
  //#endregion
7384
7484
  //#region src/verify/run-compat-check.ts
@@ -7454,37 +7554,9 @@ function cleanGeneratedFiles(dir) {
7454
7554
  } catch {}
7455
7555
  }
7456
7556
  }
7457
- /**
7458
- * Load a smoke-manifest.json from the generated SDK directory and convert
7459
- * it to ManifestEntry[] for the overlay builder.
7460
- */
7461
- function loadManifestFromDir(outputDir) {
7462
- const manifestPath = path$1.join(outputDir, "smoke-manifest.json");
7463
- if (!existsSync(manifestPath)) return void 0;
7464
- try {
7465
- const parsed = JSON.parse(readFileSync(manifestPath, "utf-8"));
7466
- if (Array.isArray(parsed)) return parsed;
7467
- if (typeof parsed === "object" && parsed !== null) return Object.entries(parsed).map(([httpKey, value]) => {
7468
- const spaceIdx = httpKey.indexOf(" ");
7469
- const httpMethod = httpKey.slice(0, spaceIdx);
7470
- const httpPath = httpKey.slice(spaceIdx + 1);
7471
- const v = value;
7472
- return {
7473
- operationId: "",
7474
- sdkResourceProperty: v.service ?? "",
7475
- sdkMethodName: v.sdkMethod ?? "",
7476
- httpMethod,
7477
- path: httpPath,
7478
- pathParams: [],
7479
- bodyFields: [],
7480
- queryFields: []
7481
- };
7482
- });
7483
- } catch {}
7484
- }
7485
7557
  async function runOverlayRetryLoop(opts) {
7486
7558
  const { baseline, parsedSpec, outputDir, lang, maxRetries, namespace, onRetry } = opts;
7487
- let overlay = buildOverlayLookup(baseline, loadManifestFromDir(outputDir), parsedSpec, getExtractor(lang).hints);
7559
+ let overlay = buildOverlayLookup(baseline, readManifestSync(outputDir), parsedSpec, getExtractor(lang).hints);
7488
7560
  let prevPreserved = -1;
7489
7561
  const patchedPerIteration = [];
7490
7562
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -8039,8 +8111,8 @@ export const ${`${toCamelCase(lang)}Emitter`}: Emitter = {
8039
8111
  return [];
8040
8112
  },
8041
8113
 
8042
- generateManifest(_spec: ApiSpec, _ctx: EmitterContext): GeneratedFile[] {
8043
- return [];
8114
+ buildOperationsMap(_spec: ApiSpec, _ctx: EmitterContext) {
8115
+ return {};
8044
8116
  },
8045
8117
 
8046
8118
  fileHeader(): string {
@@ -8254,7 +8326,7 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
8254
8326
  }
8255
8327
  return parseCommand(opts);
8256
8328
  });
8257
- program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--manifest <path>", "Path to smoke-manifest.json for method overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").action((opts) => {
8329
+ program.command("generate").description("Run a registered emitter against an OpenAPI spec").option("--spec <path>", "Path to OpenAPI spec file (or set OPENAPI_SPEC_PATH)").requiredOption("--lang <language>", "Target language").requiredOption("--output <dir>", "Output directory").option("--target <dir>", "Target directory for live SDK integration (merged output)").option("--namespace <name>", "SDK namespace/package name").option("--dry-run", "Preview files without writing").option("--api-surface <path>", "Path to baseline API surface JSON for compat overlay").option("--no-compat-check", "Skip compat overlay even if --api-surface is provided").option("--no-prune", "Skip deletion of stale files recorded in the previous .oagen-manifest.json").action((opts) => {
8258
8330
  opts.spec ??= process.env.OPENAPI_SPEC_PATH;
8259
8331
  if (!opts.spec) {
8260
8332
  console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");