@workos/oagen 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.mjs +299 -14
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.d.mts +3 -1
- package/dist/compat/index.d.mts.map +1 -1
- package/dist/compat/index.mjs +5 -2
- package/dist/compat/index.mjs.map +1 -1
- package/dist/{diff-CihuFSbJ.mjs → diff-C38px0NK.mjs} +2 -2
- package/dist/diff-C38px0NK.mjs.map +1 -0
- package/dist/{differ-BTx-rDgJ.d.mts → differ-fdF9qogA.d.mts} +8 -1
- package/dist/differ-fdF9qogA.d.mts.map +1 -0
- package/dist/index.d.mts +37 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{overlay-DTaApIqN.mjs → overlay-DGcHh1KN.mjs} +258 -1
- package/dist/overlay-DGcHh1KN.mjs.map +1 -0
- package/dist/scripts/smoke/baseline.mjs +1 -1
- package/dist/scripts/smoke/baseline.mjs.map +1 -1
- package/dist/smoke.d.mts +24 -0
- package/dist/smoke.d.mts.map +1 -1
- package/dist/smoke.mjs +1 -1
- package/dist/smoke.mjs.map +1 -1
- package/dist/verify/index.d.mts +1 -1
- package/dist/verify/index.mjs +2 -2
- package/docs/advanced/index.md +1 -0
- package/docs/advanced/transform-spec.md +124 -0
- package/docs/cli.md +13 -12
- package/docs/core/compatibility-policy.md +26 -0
- package/docs/core/compatibility.md +6 -0
- package/docs/index.md +1 -0
- package/package.json +1 -1
- package/dist/diff-CihuFSbJ.mjs.map +0 -1
- package/dist/differ-BTx-rDgJ.d.mts.map +0 -1
- package/dist/overlay-DTaApIqN.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -2278,7 +2278,7 @@ function validateModelRefs(spec) {
|
|
|
2278
2278
|
//#region src/parser/parse.ts
|
|
2279
2279
|
async function parseSpec(specPath, options) {
|
|
2280
2280
|
const { parsed } = await loadAndBundleSpec(specPath);
|
|
2281
|
-
const spec = parsed;
|
|
2281
|
+
const spec = options?.transformSpec ? options.transformSpec(parsed) : parsed;
|
|
2282
2282
|
const version = spec.openapi ?? "";
|
|
2283
2283
|
if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
|
|
2284
2284
|
const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
|
|
@@ -2326,7 +2326,11 @@ function extractAuthSchemes(securitySchemes) {
|
|
|
2326
2326
|
//#endregion
|
|
2327
2327
|
//#region src/cli/parse.ts
|
|
2328
2328
|
async function parseCommand(opts) {
|
|
2329
|
-
const ir = await parseSpec(opts.spec
|
|
2329
|
+
const ir = await parseSpec(opts.spec, {
|
|
2330
|
+
operationIdTransform: opts.operationIdTransform,
|
|
2331
|
+
schemaNameTransform: opts.schemaNameTransform,
|
|
2332
|
+
transformSpec: opts.transformSpec
|
|
2333
|
+
});
|
|
2330
2334
|
console.log(JSON.stringify(ir, null, 2));
|
|
2331
2335
|
}
|
|
2332
2336
|
//#endregion
|
|
@@ -4289,7 +4293,7 @@ async function writeFiles(files, outputDir, options) {
|
|
|
4289
4293
|
continue;
|
|
4290
4294
|
}
|
|
4291
4295
|
if (file.skipIfExists) {
|
|
4292
|
-
if (header && !existingContent.startsWith(header)) await fs.writeFile(fullPath, header + "\n\n" + existingContent, "utf-8");
|
|
4296
|
+
if (header && !existingContent.startsWith(header) && !file.path.endsWith(".json")) await fs.writeFile(fullPath, header + "\n\n" + existingContent, "utf-8");
|
|
4293
4297
|
result.skipped.push(file.path);
|
|
4294
4298
|
continue;
|
|
4295
4299
|
}
|
|
@@ -5907,7 +5911,8 @@ function expandDocUrls(spec, docUrl) {
|
|
|
5907
5911
|
async function generateCommand(opts) {
|
|
5908
5912
|
let ir = await parseSpec(opts.spec, {
|
|
5909
5913
|
operationIdTransform: opts.operationIdTransform,
|
|
5910
|
-
schemaNameTransform: opts.schemaNameTransform
|
|
5914
|
+
schemaNameTransform: opts.schemaNameTransform,
|
|
5915
|
+
transformSpec: opts.transformSpec
|
|
5911
5916
|
});
|
|
5912
5917
|
if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
|
|
5913
5918
|
const emitter = getEmitter(opts.lang);
|
|
@@ -6293,7 +6298,8 @@ function summarize(changes) {
|
|
|
6293
6298
|
async function diffCommand(opts) {
|
|
6294
6299
|
const parseOptions = {
|
|
6295
6300
|
operationIdTransform: opts.operationIdTransform,
|
|
6296
|
-
schemaNameTransform: opts.schemaNameTransform
|
|
6301
|
+
schemaNameTransform: opts.schemaNameTransform,
|
|
6302
|
+
transformSpec: opts.transformSpec
|
|
6297
6303
|
};
|
|
6298
6304
|
let [oldSpec, newSpec] = await Promise.all([parseSpec(opts.old, parseOptions), parseSpec(opts.new, parseOptions)]);
|
|
6299
6305
|
if (opts.docUrl) {
|
|
@@ -6668,7 +6674,10 @@ async function compatExtractCommand(opts) {
|
|
|
6668
6674
|
if (opts.spec) {
|
|
6669
6675
|
const specContent = readFileSync(opts.spec, "utf-8");
|
|
6670
6676
|
snapshot.source.specSha = createHash("sha256").update(specContent).digest("hex");
|
|
6671
|
-
enrichWithSpecContext(snapshot, await parseSpec(opts.spec, {
|
|
6677
|
+
enrichWithSpecContext(snapshot, await parseSpec(opts.spec, {
|
|
6678
|
+
schemaNameTransform: opts.schemaNameTransform,
|
|
6679
|
+
transformSpec: opts.transformSpec
|
|
6680
|
+
}));
|
|
6672
6681
|
}
|
|
6673
6682
|
const outputPath = resolve(opts.output, SNAPSHOT_FILENAME);
|
|
6674
6683
|
writeFileSync(outputPath, JSON.stringify(snapshot, null, 2));
|
|
@@ -7211,12 +7220,269 @@ function diffSnapshots(baseline, candidate, policy) {
|
|
|
7211
7220
|
if (isServiceWrapperConstructor(candSym, serviceAccessors)) continue;
|
|
7212
7221
|
changes.push(classifyAddedSymbol(candSym));
|
|
7213
7222
|
}
|
|
7223
|
+
detectForkedSchemas(changes, baseline, candidate);
|
|
7224
|
+
cascadeRenameDowngrades(changes, detectTypeRenames(changes, baseline, candidate), detectEnumRenames(changes, baseline, candidate));
|
|
7214
7225
|
return {
|
|
7215
7226
|
changes,
|
|
7216
7227
|
summary: summarizeChanges(changes)
|
|
7217
7228
|
};
|
|
7218
7229
|
}
|
|
7219
7230
|
/**
|
|
7231
|
+
* Detect "schema fork" antipattern and attach a remediation hint.
|
|
7232
|
+
*
|
|
7233
|
+
* Fires when:
|
|
7234
|
+
* 1. A return type or field type changed from `OldType` → `NewType`.
|
|
7235
|
+
* 2. `NewType` is newly added in the candidate snapshot.
|
|
7236
|
+
* 3. `NewType.fields ⊇ OldType.fields` (every field on the old type still
|
|
7237
|
+
* exists on the new one — the new schema is a strict superset, so the
|
|
7238
|
+
* same fields could have been added to the existing schema additively).
|
|
7239
|
+
*
|
|
7240
|
+
* Mutates `changes` in place: sets `remediation` on the matching change.
|
|
7241
|
+
* Leaves the original category and severity alone — this is just a hint.
|
|
7242
|
+
*
|
|
7243
|
+
* Identity match is by `displayName` of the type symbol, which corresponds
|
|
7244
|
+
* to the type name as it appears in `change.old.returnType` / `.type` and
|
|
7245
|
+
* `change.new.returnType` / `.type`.
|
|
7246
|
+
*/
|
|
7247
|
+
function detectForkedSchemas(changes, baseline, candidate) {
|
|
7248
|
+
const baselineTypeFields = collectTypeFieldSets(baseline);
|
|
7249
|
+
const candidateTypeFields = collectTypeFieldSets(candidate);
|
|
7250
|
+
const baselineTypeNames = new Set(baselineTypeFields.keys());
|
|
7251
|
+
for (const change of changes) {
|
|
7252
|
+
if (change.remediation) continue;
|
|
7253
|
+
let oldType;
|
|
7254
|
+
let newType;
|
|
7255
|
+
if (change.category === "return_type_changed") {
|
|
7256
|
+
oldType = change.old.returnType;
|
|
7257
|
+
newType = change.new.returnType;
|
|
7258
|
+
} else if (change.category === "field_type_changed") {
|
|
7259
|
+
oldType = change.old.type;
|
|
7260
|
+
newType = change.new.type;
|
|
7261
|
+
} else continue;
|
|
7262
|
+
if (!oldType || !newType || oldType === newType) continue;
|
|
7263
|
+
const oldTypeBare = bareTypeName(oldType);
|
|
7264
|
+
const newTypeBare = bareTypeName(newType);
|
|
7265
|
+
if (baselineTypeNames.has(newTypeBare)) continue;
|
|
7266
|
+
const oldFields = baselineTypeFields.get(oldTypeBare);
|
|
7267
|
+
const newFields = candidateTypeFields.get(newTypeBare);
|
|
7268
|
+
if (!oldFields || !newFields) continue;
|
|
7269
|
+
if (oldFields.size === 0) continue;
|
|
7270
|
+
let isSuperset = true;
|
|
7271
|
+
for (const f of oldFields) if (!newFields.has(f)) {
|
|
7272
|
+
isSuperset = false;
|
|
7273
|
+
break;
|
|
7274
|
+
}
|
|
7275
|
+
if (!isSuperset) continue;
|
|
7276
|
+
change.remediation = `Schema "${newTypeBare}" looks like "${oldTypeBare}" with additional fields. Consider adding the new fields to "${oldTypeBare}" instead of forking a new schema — forking forces a breaking type-name change in typed SDKs, while extending the existing schema is additive.`;
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
/**
|
|
7280
|
+
* Build a map from type fqName → set of (lowercased) child field names.
|
|
7281
|
+
* Used by `detectForkedSchemas` for superset comparison.
|
|
7282
|
+
*/
|
|
7283
|
+
function collectTypeFieldSets(snapshot) {
|
|
7284
|
+
const result = /* @__PURE__ */ new Map();
|
|
7285
|
+
for (const sym of snapshot.symbols) {
|
|
7286
|
+
if (sym.kind !== "field" && sym.kind !== "property") continue;
|
|
7287
|
+
if (!sym.ownerFqName) continue;
|
|
7288
|
+
const localName = sym.fqName.includes(".") ? sym.fqName.slice(sym.fqName.lastIndexOf(".") + 1) : sym.fqName;
|
|
7289
|
+
let set = result.get(sym.ownerFqName);
|
|
7290
|
+
if (!set) {
|
|
7291
|
+
set = /* @__PURE__ */ new Set();
|
|
7292
|
+
result.set(sym.ownerFqName, set);
|
|
7293
|
+
}
|
|
7294
|
+
set.add(localName.toLowerCase());
|
|
7295
|
+
}
|
|
7296
|
+
return result;
|
|
7297
|
+
}
|
|
7298
|
+
/**
|
|
7299
|
+
* Detect type renames: a baseline type symbol disappears and a candidate
|
|
7300
|
+
* type symbol with the same (or superset) field set takes its place.
|
|
7301
|
+
*
|
|
7302
|
+
* Common when an upstream spec promotes a single schema into multiple
|
|
7303
|
+
* (e.g. `ApiKey` → `OrganizationApiKey` + `UserApiKey`). The wire shape
|
|
7304
|
+
* returned by individual endpoints is unchanged — `OrganizationApiKey`
|
|
7305
|
+
* has the same fields `ApiKey` had — so consumer code accessing those
|
|
7306
|
+
* fields keeps working. The compat report flags `ApiKey` as removed
|
|
7307
|
+
* because its symbol is gone; this pass downgrades the removal to
|
|
7308
|
+
* soft-risk and records the rename so its child fields/methods cascade.
|
|
7309
|
+
*
|
|
7310
|
+
* Identity criteria (must all hold):
|
|
7311
|
+
* 1. The removed symbol owns ≥ 1 field/property in the baseline (i.e.
|
|
7312
|
+
* it's a type-shaped symbol — model/interface/class — not a plain
|
|
7313
|
+
* function or constant).
|
|
7314
|
+
* 2. Some candidate type that did *not* exist in the baseline has a
|
|
7315
|
+
* field set that is a non-strict superset of the removed type's
|
|
7316
|
+
* fields. (Strict-subset would mean fields were lost — a real break.)
|
|
7317
|
+
* 3. The candidate type is the alphabetically-first such match, so
|
|
7318
|
+
* pairing is deterministic when multiple candidates fit (e.g. both
|
|
7319
|
+
* `OrganizationApiKey` and `UserApiKey` share the original fields).
|
|
7320
|
+
*
|
|
7321
|
+
* Returns a `removedName -> newName` map so a downstream cascade pass
|
|
7322
|
+
* can downgrade owned-field removals and `*_type_changed` pointing at
|
|
7323
|
+
* the same pair.
|
|
7324
|
+
*
|
|
7325
|
+
* Mutates matching `symbol_removed` entries in `changes`: severity →
|
|
7326
|
+
* `soft-risk`, attaches a `remediation` describing the rename.
|
|
7327
|
+
*/
|
|
7328
|
+
function detectTypeRenames(changes, baseline, candidate) {
|
|
7329
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
7330
|
+
const baselineTypeFields = collectTypeFieldSets(baseline);
|
|
7331
|
+
const candidateTypeFields = collectTypeFieldSets(candidate);
|
|
7332
|
+
const baselineTypeNames = new Set(baselineTypeFields.keys());
|
|
7333
|
+
const candidateTypesSorted = [...candidateTypeFields.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7334
|
+
for (const change of changes) {
|
|
7335
|
+
if (change.category !== "symbol_removed") continue;
|
|
7336
|
+
if (change.severity !== "breaking") continue;
|
|
7337
|
+
const removedName = bareTypeName(change.old.symbol ?? "");
|
|
7338
|
+
if (!removedName) continue;
|
|
7339
|
+
const removedFields = baselineTypeFields.get(removedName);
|
|
7340
|
+
if (!removedFields || removedFields.size === 0) continue;
|
|
7341
|
+
const match = candidateTypesSorted.find(([candName, candFields]) => {
|
|
7342
|
+
if (baselineTypeNames.has(candName)) return false;
|
|
7343
|
+
for (const f of removedFields) if (!candFields.has(f)) return false;
|
|
7344
|
+
return true;
|
|
7345
|
+
});
|
|
7346
|
+
if (!match) continue;
|
|
7347
|
+
const [newName] = match;
|
|
7348
|
+
renameMap.set(removedName, newName);
|
|
7349
|
+
change.severity = "soft-risk";
|
|
7350
|
+
change.remediation = `Type "${removedName}" appears to have been renamed to "${newName}" — the new type has every field of the old (a non-strict superset). Field accesses and method calls on values of type "${newName}" continue to work; only explicit "${removedName}" type annotations need to migrate. Consider emitting a deprecated alias \`type ${removedName} = ${newName}\` in languages that support it.`;
|
|
7351
|
+
}
|
|
7352
|
+
return renameMap;
|
|
7353
|
+
}
|
|
7354
|
+
/**
|
|
7355
|
+
* Detect enum canonical-flips: a baseline enum disappears and a candidate
|
|
7356
|
+
* enum with the **same wire-value set** takes its place.
|
|
7357
|
+
*
|
|
7358
|
+
* Caused by the emitter's enum-dedup heuristic picking a different
|
|
7359
|
+
* canonical name when a new same-shape enum joins the spec. Languages
|
|
7360
|
+
* that emit type aliases (Go, Ruby, Python, PHP, Kotlin) handle this
|
|
7361
|
+
* transparently via `type Old = New`; languages without first-class
|
|
7362
|
+
* aliases (dotnet) report the old enum as removed. The wire values are
|
|
7363
|
+
* unchanged — every legal value still serializes to the same JSON — so
|
|
7364
|
+
* consumer code constructing or matching on these enum values keeps
|
|
7365
|
+
* working. Only references to the typed enum class need migration.
|
|
7366
|
+
*
|
|
7367
|
+
* Identity criterion: a removed enum's value set is *exactly* equal to
|
|
7368
|
+
* a newly-added enum's value set (not superset — narrowing the value
|
|
7369
|
+
* set would be a real break for consumers expecting the dropped values).
|
|
7370
|
+
*/
|
|
7371
|
+
function detectEnumRenames(changes, baseline, candidate) {
|
|
7372
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
7373
|
+
const baselineEnumValues = collectEnumValueSets(baseline);
|
|
7374
|
+
const candidateEnumValues = collectEnumValueSets(candidate);
|
|
7375
|
+
const baselineEnumNames = new Set(baselineEnumValues.keys());
|
|
7376
|
+
const candidateEnumsSorted = [...candidateEnumValues.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7377
|
+
for (const change of changes) {
|
|
7378
|
+
if (change.category !== "symbol_removed") continue;
|
|
7379
|
+
if (change.severity !== "breaking") continue;
|
|
7380
|
+
const removedName = bareTypeName(change.old.symbol ?? "");
|
|
7381
|
+
if (!removedName) continue;
|
|
7382
|
+
const removedValues = baselineEnumValues.get(removedName);
|
|
7383
|
+
if (!removedValues || removedValues.size === 0) continue;
|
|
7384
|
+
const match = candidateEnumsSorted.find(([candName, candValues]) => {
|
|
7385
|
+
if (baselineEnumNames.has(candName)) return false;
|
|
7386
|
+
if (candValues.size !== removedValues.size) return false;
|
|
7387
|
+
for (const v of removedValues) if (!candValues.has(v)) return false;
|
|
7388
|
+
return true;
|
|
7389
|
+
});
|
|
7390
|
+
if (!match) continue;
|
|
7391
|
+
const [newName] = match;
|
|
7392
|
+
renameMap.set(removedName, newName);
|
|
7393
|
+
change.severity = "soft-risk";
|
|
7394
|
+
change.remediation = `Enum "${removedName}" appears to have been renamed to "${newName}" — both enums have identical wire values, so on-the-wire serialization is unchanged. This is typically the emitter's dedup canonical-flip after a new same-shape enum joined the spec. Consider emitting a deprecated alias in languages that support it, or pinning the canonical via emitter config.`;
|
|
7395
|
+
}
|
|
7396
|
+
return renameMap;
|
|
7397
|
+
}
|
|
7398
|
+
/**
|
|
7399
|
+
* Cascade rename downgrades to changes whose meaning depends on a renamed
|
|
7400
|
+
* symbol. Walks every change and:
|
|
7401
|
+
*
|
|
7402
|
+
* - Downgrades child removals (`Owner.field` removed where `Owner` was
|
|
7403
|
+
* renamed) — the field still exists, just under a new owner fqName.
|
|
7404
|
+
* Same logic for enum members under a renamed enum.
|
|
7405
|
+
* - Downgrades `return_type_changed` / `field_type_changed` whose
|
|
7406
|
+
* old → new pair matches a recorded rename — the type swap is the
|
|
7407
|
+
* rename itself, not a meaningful signature break.
|
|
7408
|
+
*
|
|
7409
|
+
* Each cascaded change gets a remediation pointing at the parent rename
|
|
7410
|
+
* so the reviewer can find the explanation in the report.
|
|
7411
|
+
*/
|
|
7412
|
+
function cascadeRenameDowngrades(changes, typeRenames, enumRenames) {
|
|
7413
|
+
if (typeRenames.size === 0 && enumRenames.size === 0) return;
|
|
7414
|
+
for (const change of changes) {
|
|
7415
|
+
if (change.severity !== "breaking") continue;
|
|
7416
|
+
if (change.category === "symbol_removed") {
|
|
7417
|
+
const removed = change.old.symbol ?? "";
|
|
7418
|
+
const dotIdx = removed.indexOf(".");
|
|
7419
|
+
if (dotIdx <= 0) continue;
|
|
7420
|
+
const ownerName = removed.slice(0, dotIdx);
|
|
7421
|
+
const renamedTo = typeRenames.get(ownerName) ?? enumRenames.get(ownerName);
|
|
7422
|
+
if (!renamedTo) continue;
|
|
7423
|
+
change.severity = "soft-risk";
|
|
7424
|
+
change.remediation = `Owned by renamed symbol "${ownerName}" (now "${renamedTo}"). The same member exists on the new symbol under "${renamedTo}.${removed.slice(dotIdx + 1)}".`;
|
|
7425
|
+
continue;
|
|
7426
|
+
}
|
|
7427
|
+
if (change.category === "return_type_changed") {
|
|
7428
|
+
const oldT = bareTypeName(change.old.returnType ?? "");
|
|
7429
|
+
const newT = bareTypeName(change.new.returnType ?? "");
|
|
7430
|
+
if (typeRenames.get(oldT) === newT) {
|
|
7431
|
+
change.severity = "soft-risk";
|
|
7432
|
+
change.remediation = `Return type swap matches recorded rename "${oldT}" → "${newT}". The underlying field set is preserved (see the rename advisory on "${oldT}").`;
|
|
7433
|
+
}
|
|
7434
|
+
continue;
|
|
7435
|
+
}
|
|
7436
|
+
if (change.category === "field_type_changed") {
|
|
7437
|
+
const oldT = bareTypeName(change.old.type ?? "");
|
|
7438
|
+
const newT = bareTypeName(change.new.type ?? "");
|
|
7439
|
+
if ((typeRenames.get(oldT) ?? enumRenames.get(oldT)) === newT) {
|
|
7440
|
+
change.severity = "soft-risk";
|
|
7441
|
+
change.remediation = `Field type swap matches recorded rename "${oldT}" → "${newT}". On-the-wire shape is unchanged (see the rename advisory on "${oldT}").`;
|
|
7442
|
+
}
|
|
7443
|
+
}
|
|
7444
|
+
}
|
|
7445
|
+
}
|
|
7446
|
+
/**
|
|
7447
|
+
* Build a map from enum fqName → set of wire values. Used by
|
|
7448
|
+
* `detectEnumRenames` to find structurally-identical enums across
|
|
7449
|
+
* baseline and candidate. Wire values come from `enum_member.value`
|
|
7450
|
+
* (the JSON-level value) — not the member names, which are
|
|
7451
|
+
* language-specific PascalCase forms.
|
|
7452
|
+
*
|
|
7453
|
+
* Members whose `value` is undefined are skipped — they contribute no
|
|
7454
|
+
* identity information.
|
|
7455
|
+
*/
|
|
7456
|
+
function collectEnumValueSets(snapshot) {
|
|
7457
|
+
const result = /* @__PURE__ */ new Map();
|
|
7458
|
+
for (const sym of snapshot.symbols) {
|
|
7459
|
+
if (sym.kind !== "enum_member") continue;
|
|
7460
|
+
if (!sym.ownerFqName) continue;
|
|
7461
|
+
if (sym.value === void 0) continue;
|
|
7462
|
+
let set = result.get(sym.ownerFqName);
|
|
7463
|
+
if (!set) {
|
|
7464
|
+
set = /* @__PURE__ */ new Set();
|
|
7465
|
+
result.set(sym.ownerFqName, set);
|
|
7466
|
+
}
|
|
7467
|
+
set.add(String(sym.value));
|
|
7468
|
+
}
|
|
7469
|
+
return result;
|
|
7470
|
+
}
|
|
7471
|
+
/**
|
|
7472
|
+
* Strip array/nullable suffixes so we compare bare type names. Languages
|
|
7473
|
+
* encode these differently (`Foo[]`, `Foo | null`, `Foo?`, `List<Foo>`,
|
|
7474
|
+
* `Optional[Foo]`); this is a best-effort common-case stripper, not an
|
|
7475
|
+
* exhaustive parser.
|
|
7476
|
+
*/
|
|
7477
|
+
function bareTypeName(t) {
|
|
7478
|
+
let s = t.trim();
|
|
7479
|
+
s = s.replace(/\s*\|\s*null$/, "").replace(/\?$/, "");
|
|
7480
|
+
s = s.replace(/\[\]$/, "");
|
|
7481
|
+
const generic = s.match(/^[A-Za-z_][A-Za-z0-9_.]*<\s*([A-Za-z_][A-Za-z0-9_.]*)\s*>$/);
|
|
7482
|
+
if (generic) s = generic[1];
|
|
7483
|
+
return s;
|
|
7484
|
+
}
|
|
7485
|
+
/**
|
|
7220
7486
|
* Check if a symbol is a constructor belonging to a service wrapper class.
|
|
7221
7487
|
*
|
|
7222
7488
|
* Service wrapper constructors are internal plumbing (taking a client/config
|
|
@@ -7300,7 +7566,8 @@ function generateReport(diff, language) {
|
|
|
7300
7566
|
provenance: c.provenance,
|
|
7301
7567
|
old: c.old,
|
|
7302
7568
|
new: c.new,
|
|
7303
|
-
...c.message ? { message: c.message } : {}
|
|
7569
|
+
...c.message ? { message: c.message } : {},
|
|
7570
|
+
...c.remediation ? { remediation: c.remediation } : {}
|
|
7304
7571
|
}))
|
|
7305
7572
|
};
|
|
7306
7573
|
}
|
|
@@ -7321,6 +7588,7 @@ function formatHumanSummary(diff, opts) {
|
|
|
7321
7588
|
lines.push(" Breaking:");
|
|
7322
7589
|
for (const c of breaking) {
|
|
7323
7590
|
lines.push(` [${c.category}] ${c.symbol} — ${c.message}`);
|
|
7591
|
+
if (c.remediation) lines.push(` hint: ${c.remediation}`);
|
|
7324
7592
|
if (opts?.explain && c.provenance !== "unknown") lines.push(` provenance: ${c.provenance}`);
|
|
7325
7593
|
}
|
|
7326
7594
|
}
|
|
@@ -7329,6 +7597,7 @@ function formatHumanSummary(diff, opts) {
|
|
|
7329
7597
|
lines.push(" Soft-risk:");
|
|
7330
7598
|
for (const c of softRisk) {
|
|
7331
7599
|
lines.push(` [${c.category}] ${c.symbol} — ${c.message}`);
|
|
7600
|
+
if (c.remediation) lines.push(` hint: ${c.remediation}`);
|
|
7332
7601
|
if (opts?.explain && c.provenance !== "unknown") lines.push(` provenance: ${c.provenance}`);
|
|
7333
7602
|
}
|
|
7334
7603
|
}
|
|
@@ -7895,7 +8164,7 @@ function printCompatResult(result, baseline) {
|
|
|
7895
8164
|
console.log(`compat: ${result.preservationScore}% (${result.preservedSymbols}/${result.totalBaselineSymbols} symbols preserved)`);
|
|
7896
8165
|
}
|
|
7897
8166
|
async function verifyCommand(opts) {
|
|
7898
|
-
const { spec, oldSpec, lang, output, apiSurface, rawResults, smokeConfig, smokeRunner, scope, diagnostics, operationIdTransform, schemaNameTransform, compatConfig, compatReport, compatExplain } = opts;
|
|
8167
|
+
const { spec, oldSpec, lang, output, apiSurface, rawResults, smokeConfig, smokeRunner, scope, diagnostics, operationIdTransform, schemaNameTransform, transformSpec, compatConfig, compatReport, compatExplain } = opts;
|
|
7899
8168
|
const maxRetries = opts.maxRetries ?? 3;
|
|
7900
8169
|
const diagData = {};
|
|
7901
8170
|
const effectiveFailOn = opts.compatFailOn ?? compatConfig?.failOn ?? "breaking";
|
|
@@ -7911,7 +8180,8 @@ async function verifyCommand(opts) {
|
|
|
7911
8180
|
let parsedSpec;
|
|
7912
8181
|
if (effectiveScope === "spec-only" && spec) parsedSpec = await parseSpec(spec, {
|
|
7913
8182
|
operationIdTransform,
|
|
7914
|
-
schemaNameTransform
|
|
8183
|
+
schemaNameTransform,
|
|
8184
|
+
transformSpec
|
|
7915
8185
|
});
|
|
7916
8186
|
else if (effectiveScope === "spec-only") throw new CommandError("error: --scope spec-only requires --spec <path>", "", 1);
|
|
7917
8187
|
const compatFlow = parsedSpec && maxRetries > 0 ? await runOverlayRetryLoop({
|
|
@@ -7978,10 +8248,12 @@ async function verifyCommand(opts) {
|
|
|
7978
8248
|
console.log(separator);
|
|
7979
8249
|
const stalenessResult = runStalenessCheck(baseline, await parseSpec(oldSpec, {
|
|
7980
8250
|
operationIdTransform,
|
|
7981
|
-
schemaNameTransform
|
|
8251
|
+
schemaNameTransform,
|
|
8252
|
+
transformSpec
|
|
7982
8253
|
}), await parseSpec(spec, {
|
|
7983
8254
|
operationIdTransform,
|
|
7984
|
-
schemaNameTransform
|
|
8255
|
+
schemaNameTransform,
|
|
8256
|
+
transformSpec
|
|
7985
8257
|
}), lang);
|
|
7986
8258
|
if (stalenessResult.violations.length > 0) {
|
|
7987
8259
|
console.log(`Found ${stalenessResult.violations.length} stale symbol(s):`);
|
|
@@ -8281,7 +8553,8 @@ async function initCommand(opts) {
|
|
|
8281
8553
|
async function resolveCommand(opts) {
|
|
8282
8554
|
let ir = await parseSpec(opts.spec, {
|
|
8283
8555
|
operationIdTransform: opts.operationIdTransform,
|
|
8284
|
-
schemaNameTransform: opts.schemaNameTransform
|
|
8556
|
+
schemaNameTransform: opts.schemaNameTransform,
|
|
8557
|
+
transformSpec: opts.transformSpec
|
|
8285
8558
|
});
|
|
8286
8559
|
if (opts.docUrl) ir = expandDocUrls(ir, opts.docUrl);
|
|
8287
8560
|
const resolved = resolveOperations(ir, opts.operationHints, opts.mountRules);
|
|
@@ -8377,6 +8650,7 @@ const explicitConfigPath = configArgIdx !== -1 ? process.argv[configArgIdx + 1]
|
|
|
8377
8650
|
let configSmokeRunners;
|
|
8378
8651
|
let configOperationIdTransform;
|
|
8379
8652
|
let configSchemaNameTransform;
|
|
8653
|
+
let configTransformSpec;
|
|
8380
8654
|
let configDocUrl;
|
|
8381
8655
|
let configOperationHints;
|
|
8382
8656
|
let configMountRules;
|
|
@@ -8389,6 +8663,7 @@ try {
|
|
|
8389
8663
|
configSmokeRunners = config.smokeRunners;
|
|
8390
8664
|
configOperationIdTransform = config.operationIdTransform;
|
|
8391
8665
|
configSchemaNameTransform = config.schemaNameTransform;
|
|
8666
|
+
configTransformSpec = config.transformSpec;
|
|
8392
8667
|
configDocUrl = config.docUrl;
|
|
8393
8668
|
configOperationHints = config.operationHints;
|
|
8394
8669
|
configMountRules = config.mountRules;
|
|
@@ -8405,7 +8680,12 @@ program.command("parse").description("Parse an OpenAPI spec and output IR as JSO
|
|
|
8405
8680
|
console.error("error: --spec <path> or OPENAPI_SPEC_PATH env var is required");
|
|
8406
8681
|
process.exit(1);
|
|
8407
8682
|
}
|
|
8408
|
-
return parseCommand(
|
|
8683
|
+
return parseCommand({
|
|
8684
|
+
...opts,
|
|
8685
|
+
operationIdTransform: configOperationIdTransform,
|
|
8686
|
+
schemaNameTransform: configSchemaNameTransform,
|
|
8687
|
+
transformSpec: configTransformSpec
|
|
8688
|
+
});
|
|
8409
8689
|
});
|
|
8410
8690
|
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) => {
|
|
8411
8691
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
@@ -8417,6 +8697,7 @@ program.command("generate").description("Run a registered emitter against an Ope
|
|
|
8417
8697
|
...opts,
|
|
8418
8698
|
operationIdTransform: configOperationIdTransform,
|
|
8419
8699
|
schemaNameTransform: configSchemaNameTransform,
|
|
8700
|
+
transformSpec: configTransformSpec,
|
|
8420
8701
|
docUrl: configDocUrl,
|
|
8421
8702
|
operationHints: configOperationHints,
|
|
8422
8703
|
mountRules: configMountRules,
|
|
@@ -8428,6 +8709,7 @@ program.command("diff").description("Compare two OpenAPI specs and output a diff
|
|
|
8428
8709
|
...opts,
|
|
8429
8710
|
operationIdTransform: configOperationIdTransform,
|
|
8430
8711
|
schemaNameTransform: configSchemaNameTransform,
|
|
8712
|
+
transformSpec: configTransformSpec,
|
|
8431
8713
|
docUrl: configDocUrl
|
|
8432
8714
|
}).catch(handleError);
|
|
8433
8715
|
});
|
|
@@ -8439,7 +8721,8 @@ program.command("compat-extract").description("Extract a compat snapshot from a
|
|
|
8439
8721
|
opts.spec ??= process.env.OPENAPI_SPEC_PATH;
|
|
8440
8722
|
compatExtractCommand({
|
|
8441
8723
|
...opts,
|
|
8442
|
-
schemaNameTransform: configSchemaNameTransform
|
|
8724
|
+
schemaNameTransform: configSchemaNameTransform,
|
|
8725
|
+
transformSpec: configTransformSpec
|
|
8443
8726
|
}).catch(handleError);
|
|
8444
8727
|
});
|
|
8445
8728
|
program.command("compat-diff").description("Diff two compat snapshot files and produce a classified change report").requiredOption("--baseline <path>", "Path to the baseline compat snapshot JSON").requiredOption("--candidate <path>", "Path to the candidate compat snapshot JSON").option("--output <path>", "Write machine-readable report to this path").option("--fail-on <level>", "Fail threshold: none, breaking, or soft-risk", "breaking").option("--explain", "Include provenance explanations in output").action((opts) => {
|
|
@@ -8456,6 +8739,7 @@ program.command("verify").description("Run smoke tests (and optional compat chec
|
|
|
8456
8739
|
maxRetries: parseInt(opts.maxRetries, 10),
|
|
8457
8740
|
operationIdTransform: configOperationIdTransform,
|
|
8458
8741
|
schemaNameTransform: configSchemaNameTransform,
|
|
8742
|
+
transformSpec: configTransformSpec,
|
|
8459
8743
|
compatConfig: configCompat,
|
|
8460
8744
|
compatReport: opts.compatReport,
|
|
8461
8745
|
compatFailOn: opts.compatFailOn,
|
|
@@ -8473,6 +8757,7 @@ program.command("resolve").description("Resolve operation names from spec and ou
|
|
|
8473
8757
|
...opts,
|
|
8474
8758
|
operationIdTransform: configOperationIdTransform,
|
|
8475
8759
|
schemaNameTransform: configSchemaNameTransform,
|
|
8760
|
+
transformSpec: configTransformSpec,
|
|
8476
8761
|
docUrl: configDocUrl,
|
|
8477
8762
|
operationHints: configOperationHints,
|
|
8478
8763
|
mountRules: configMountRules
|