@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.
- package/dist/cli/index.mjs +600 -528
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.mjs +1 -1
- package/dist/{diff-BaH9qa8k.mjs → diff-DZvzV9p3.mjs} +21 -21
- package/dist/{diff-BaH9qa8k.mjs.map → diff-DZvzV9p3.mjs.map} +1 -1
- package/dist/index.d.mts +10 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{overlay-TYxHOCow.mjs → overlay-Bt6oz0dg.mjs} +2 -2
- package/dist/{overlay-TYxHOCow.mjs.map → overlay-Bt6oz0dg.mjs.map} +1 -1
- package/dist/verify/index.d.mts.map +1 -1
- package/dist/verify/index.mjs +46 -31
- package/dist/verify/index.mjs.map +1 -1
- package/docs/agents/architecture.md +1 -1
- package/docs/architecture/emitter-contract.md +3 -3
- package/docs/architecture/pipeline.md +1 -1
- package/docs/cli.md +10 -11
- package/package.json +1 -1
- package/skills/generate-emitter/SKILL.md +8 -11
- package/skills/generate-smoke-test/SKILL.md +6 -6
- package/skills/integrate/SKILL.md +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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/
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
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
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6391
|
+
};
|
|
6392
|
+
/** Get the built-in default policy for a language. */
|
|
6393
|
+
function getDefaultPolicy(language) {
|
|
6394
|
+
return { ...LANGUAGE_DEFAULTS[language] };
|
|
6311
6395
|
}
|
|
6312
|
-
/**
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
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/
|
|
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
|
-
*
|
|
6390
|
-
*
|
|
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
|
|
6393
|
-
const
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
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
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
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/
|
|
7432
|
+
//#region src/compat/approvals.ts
|
|
7223
7433
|
/**
|
|
7224
|
-
*
|
|
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
|
|
7230
|
-
const
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
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
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
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
|
-
|
|
7314
|
-
|
|
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
|
-
|
|
7345
|
-
|
|
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,
|
|
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
|
-
|
|
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("--
|
|
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");
|