@workos/oagen 0.14.0 → 0.15.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 +142 -32
- package/dist/cli/index.mjs.map +1 -1
- package/dist/compat/index.mjs +120 -7
- package/dist/compat/index.mjs.map +1 -1
- package/dist/differ-fdF9qogA.d.mts.map +1 -1
- package/dist/{overlay-DGcHh1KN.mjs → overlay-YkIApOxh.mjs} +143 -33
- package/dist/overlay-YkIApOxh.mjs.map +1 -0
- package/dist/verify/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/overlay-DGcHh1KN.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -7221,7 +7221,10 @@ function diffSnapshots(baseline, candidate, policy) {
|
|
|
7221
7221
|
changes.push(classifyAddedSymbol(candSym));
|
|
7222
7222
|
}
|
|
7223
7223
|
detectForkedSchemas(changes, baseline, candidate);
|
|
7224
|
-
|
|
7224
|
+
const typeRenames = detectTypeRenames(changes, baseline, candidate);
|
|
7225
|
+
const enumRenames = detectEnumRenames(changes, baseline, candidate);
|
|
7226
|
+
inferTransitiveTypeRenames(typeRenames, baseline, candidate, changes);
|
|
7227
|
+
cascadeRenameDowngrades(changes, typeRenames, enumRenames, baseline, candidate);
|
|
7225
7228
|
return {
|
|
7226
7229
|
changes,
|
|
7227
7230
|
summary: summarizeChanges(changes)
|
|
@@ -7331,27 +7334,86 @@ function detectTypeRenames(changes, baseline, candidate) {
|
|
|
7331
7334
|
const candidateTypeFields = collectTypeFieldSets(candidate);
|
|
7332
7335
|
const baselineTypeNames = new Set(baselineTypeFields.keys());
|
|
7333
7336
|
const candidateTypesSorted = [...candidateTypeFields.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
if (!removedFields || removedFields.size === 0) continue;
|
|
7337
|
+
const parentRemovalByName = /* @__PURE__ */ new Map();
|
|
7338
|
+
for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
|
|
7339
|
+
for (const [baselineTypeName, baselineFields] of baselineTypeFields) {
|
|
7340
|
+
if (baselineFields.size === 0) continue;
|
|
7341
|
+
const sameNameFields = candidateTypeFields.get(baselineTypeName);
|
|
7342
|
+
if (sameNameFields && setEquals(baselineFields, sameNameFields)) continue;
|
|
7341
7343
|
const match = candidateTypesSorted.find(([candName, candFields]) => {
|
|
7342
7344
|
if (baselineTypeNames.has(candName)) return false;
|
|
7343
|
-
for (const f of
|
|
7345
|
+
for (const f of baselineFields) if (!candFields.has(f)) return false;
|
|
7344
7346
|
return true;
|
|
7345
7347
|
});
|
|
7346
7348
|
if (!match) continue;
|
|
7347
7349
|
const [newName] = match;
|
|
7348
|
-
renameMap.set(
|
|
7349
|
-
|
|
7350
|
-
|
|
7350
|
+
renameMap.set(baselineTypeName, newName);
|
|
7351
|
+
const parentRemoval = parentRemovalByName.get(baselineTypeName);
|
|
7352
|
+
if (parentRemoval) {
|
|
7353
|
+
parentRemoval.severity = "soft-risk";
|
|
7354
|
+
parentRemoval.remediation = `Type "${baselineTypeName}" 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 "${baselineTypeName}" type annotations need to migrate. Consider emitting a deprecated alias \`type ${baselineTypeName} = ${newName}\` in languages that support it.`;
|
|
7355
|
+
}
|
|
7351
7356
|
}
|
|
7352
7357
|
return renameMap;
|
|
7353
7358
|
}
|
|
7354
7359
|
/**
|
|
7360
|
+
* Transitive rename inference. After `detectTypeRenames` records direct
|
|
7361
|
+
* structural matches (e.g. `ApiKeyWithValue → OrganizationApiKeyWithValue`),
|
|
7362
|
+
* walk the parent's fields and follow the `typeRef.name` of each: if the
|
|
7363
|
+
* baseline parent's field references an alias-type `OldOwner` and the new
|
|
7364
|
+
* parent's same-named field references a different alias `NewOwner`, the
|
|
7365
|
+
* pair is the same type-concept under a renamed name. Record the
|
|
7366
|
+
* secondary rename so the cascade can downgrade `OldOwner`'s
|
|
7367
|
+
* `symbol_removed` and any `field_type_changed` pointing at the pair.
|
|
7368
|
+
*
|
|
7369
|
+
* Why this is necessary: discriminated-union owner types (Go's
|
|
7370
|
+
* `APIKeyWithValueOwner`, PHP/Python's nested constructor params) are
|
|
7371
|
+
* frequently emitted as `kind: 'alias'` symbols with no extractable
|
|
7372
|
+
* field children — so `collectTypeFieldSets` returns empty for them and
|
|
7373
|
+
* the structural matcher can't pair them directly. The transitive
|
|
7374
|
+
* lookup gives us a deterministic, conservative way to propagate the
|
|
7375
|
+
* rename: it only fires when the parent rename has already been
|
|
7376
|
+
* positively identified, and only follows fields that share the same
|
|
7377
|
+
* wire name across baseline and candidate.
|
|
7378
|
+
*/
|
|
7379
|
+
function inferTransitiveTypeRenames(typeRenames, baseline, candidate, changes) {
|
|
7380
|
+
if (typeRenames.size === 0) return;
|
|
7381
|
+
const baselineFieldType = /* @__PURE__ */ new Map();
|
|
7382
|
+
for (const sym of baseline.symbols) {
|
|
7383
|
+
if (sym.kind !== "field" && sym.kind !== "property") continue;
|
|
7384
|
+
if (!sym.ownerFqName || !sym.typeRef) continue;
|
|
7385
|
+
const localName = sym.fqName.includes(".") ? sym.fqName.slice(sym.fqName.lastIndexOf(".") + 1) : sym.fqName;
|
|
7386
|
+
baselineFieldType.set(`${sym.ownerFqName}.${localName.toLowerCase()}`, sym.typeRef.name);
|
|
7387
|
+
}
|
|
7388
|
+
const candidateFieldType = /* @__PURE__ */ new Map();
|
|
7389
|
+
for (const sym of candidate.symbols) {
|
|
7390
|
+
if (sym.kind !== "field" && sym.kind !== "property") continue;
|
|
7391
|
+
if (!sym.ownerFqName || !sym.typeRef) continue;
|
|
7392
|
+
const localName = sym.fqName.includes(".") ? sym.fqName.slice(sym.fqName.lastIndexOf(".") + 1) : sym.fqName;
|
|
7393
|
+
candidateFieldType.set(`${sym.ownerFqName}.${localName.toLowerCase()}`, sym.typeRef.name);
|
|
7394
|
+
}
|
|
7395
|
+
const parentRemovalByName = /* @__PURE__ */ new Map();
|
|
7396
|
+
for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
|
|
7397
|
+
const initialEntries = [...typeRenames.entries()];
|
|
7398
|
+
for (const [oldOwner, newOwner] of initialEntries) for (const [baselineKey, baselineFieldTypeRef] of baselineFieldType) {
|
|
7399
|
+
if (!baselineKey.startsWith(`${oldOwner}.`)) continue;
|
|
7400
|
+
const localName = baselineKey.slice(oldOwner.length + 1);
|
|
7401
|
+
const candidateKey = `${newOwner}.${localName}`;
|
|
7402
|
+
const candidateFieldTypeRef = candidateFieldType.get(candidateKey);
|
|
7403
|
+
if (!candidateFieldTypeRef) continue;
|
|
7404
|
+
const oldT = bareTypeName(baselineFieldTypeRef);
|
|
7405
|
+
const newT = bareTypeName(candidateFieldTypeRef);
|
|
7406
|
+
if (!oldT || !newT || oldT === newT) continue;
|
|
7407
|
+
if (typeRenames.has(oldT)) continue;
|
|
7408
|
+
typeRenames.set(oldT, newT);
|
|
7409
|
+
const removal = parentRemovalByName.get(oldT);
|
|
7410
|
+
if (removal) {
|
|
7411
|
+
removal.severity = "soft-risk";
|
|
7412
|
+
removal.remediation = `Type "${oldT}" appears to have been renamed to "${newT}" — inferred transitively because the renamed parent "${oldOwner}" → "${newOwner}" has a "${localName}" field whose type swapped from "${oldT}" to "${newT}". Consider emitting a deprecated alias \`type ${oldT} = ${newT}\` in languages that support it.`;
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
7416
|
+
/**
|
|
7355
7417
|
* Detect enum canonical-flips: a baseline enum disappears and a candidate
|
|
7356
7418
|
* enum with the **same wire-value set** takes its place.
|
|
7357
7419
|
*
|
|
@@ -7374,24 +7436,21 @@ function detectEnumRenames(changes, baseline, candidate) {
|
|
|
7374
7436
|
const candidateEnumValues = collectEnumValueSets(candidate);
|
|
7375
7437
|
const baselineEnumNames = new Set(baselineEnumValues.keys());
|
|
7376
7438
|
const candidateEnumsSorted = [...candidateEnumValues.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
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
|
-
});
|
|
7439
|
+
const parentRemovalByName = /* @__PURE__ */ new Map();
|
|
7440
|
+
for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
|
|
7441
|
+
for (const [baselineEnumName, baselineValues] of baselineEnumValues) {
|
|
7442
|
+
if (baselineValues.size === 0) continue;
|
|
7443
|
+
const sameNameValues = candidateEnumValues.get(baselineEnumName);
|
|
7444
|
+
if (sameNameValues && setEquals(baselineValues, sameNameValues)) continue;
|
|
7445
|
+
const match = candidateEnumsSorted.find(([candName, candValues]) => !baselineEnumNames.has(candName) && setEquals(baselineValues, candValues));
|
|
7390
7446
|
if (!match) continue;
|
|
7391
7447
|
const [newName] = match;
|
|
7392
|
-
renameMap.set(
|
|
7393
|
-
|
|
7394
|
-
|
|
7448
|
+
renameMap.set(baselineEnumName, newName);
|
|
7449
|
+
const parentRemoval = parentRemovalByName.get(baselineEnumName);
|
|
7450
|
+
if (parentRemoval) {
|
|
7451
|
+
parentRemoval.severity = "soft-risk";
|
|
7452
|
+
parentRemoval.remediation = `Enum "${baselineEnumName}" 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.`;
|
|
7453
|
+
}
|
|
7395
7454
|
}
|
|
7396
7455
|
return renameMap;
|
|
7397
7456
|
}
|
|
@@ -7409,8 +7468,40 @@ function detectEnumRenames(changes, baseline, candidate) {
|
|
|
7409
7468
|
* Each cascaded change gets a remediation pointing at the parent rename
|
|
7410
7469
|
* so the reviewer can find the explanation in the report.
|
|
7411
7470
|
*/
|
|
7412
|
-
function cascadeRenameDowngrades(changes, typeRenames, enumRenames) {
|
|
7413
|
-
|
|
7471
|
+
function cascadeRenameDowngrades(changes, typeRenames, enumRenames, baseline, candidate) {
|
|
7472
|
+
let baselineTypeFields;
|
|
7473
|
+
let candidateTypeFields;
|
|
7474
|
+
const getBaselineFields = () => {
|
|
7475
|
+
if (!baselineTypeFields) baselineTypeFields = collectTypeFieldSets(baseline);
|
|
7476
|
+
return baselineTypeFields;
|
|
7477
|
+
};
|
|
7478
|
+
const getCandidateFields = () => {
|
|
7479
|
+
if (!candidateTypeFields) candidateTypeFields = collectTypeFieldSets(candidate);
|
|
7480
|
+
return candidateTypeFields;
|
|
7481
|
+
};
|
|
7482
|
+
/**
|
|
7483
|
+
* Structural equivalence: candidate type's field set is a non-strict
|
|
7484
|
+
* superset of baseline type's field set, AND the candidate type was
|
|
7485
|
+
* newly added in this diff (not a swap to a pre-existing type, which
|
|
7486
|
+
* is a real signature break the consumer chose). When `New ⊇ Old` and
|
|
7487
|
+
* `New` is new, every field the consumer accessed on the old type
|
|
7488
|
+
* still exists on the value they receive — so the swap is non-breaking
|
|
7489
|
+
* at the value level even though the declared type changed. This is
|
|
7490
|
+
* the fork-detector's positive signal, applied as a severity
|
|
7491
|
+
* downgrade in addition to the existing remediation hint.
|
|
7492
|
+
*/
|
|
7493
|
+
const isStructurallyEquivalent = (oldT, newT) => {
|
|
7494
|
+
if (!oldT || !newT || oldT === newT) return false;
|
|
7495
|
+
const baselineFields = getBaselineFields();
|
|
7496
|
+
const candidateFields = getCandidateFields();
|
|
7497
|
+
if (baselineFields.has(newT)) return false;
|
|
7498
|
+
const oldFields = baselineFields.get(oldT);
|
|
7499
|
+
const newFields = candidateFields.get(newT);
|
|
7500
|
+
if (!oldFields || !newFields) return false;
|
|
7501
|
+
if (oldFields.size === 0) return false;
|
|
7502
|
+
for (const f of oldFields) if (!newFields.has(f)) return false;
|
|
7503
|
+
return true;
|
|
7504
|
+
};
|
|
7414
7505
|
for (const change of changes) {
|
|
7415
7506
|
if (change.severity !== "breaking") continue;
|
|
7416
7507
|
if (change.category === "symbol_removed") {
|
|
@@ -7430,6 +7521,9 @@ function cascadeRenameDowngrades(changes, typeRenames, enumRenames) {
|
|
|
7430
7521
|
if (typeRenames.get(oldT) === newT) {
|
|
7431
7522
|
change.severity = "soft-risk";
|
|
7432
7523
|
change.remediation = `Return type swap matches recorded rename "${oldT}" → "${newT}". The underlying field set is preserved (see the rename advisory on "${oldT}").`;
|
|
7524
|
+
} else if (isStructurallyEquivalent(oldT, newT)) {
|
|
7525
|
+
change.severity = "soft-risk";
|
|
7526
|
+
if (!change.remediation) change.remediation = `Return type swap from "${oldT}" to "${newT}" is structurally equivalent — every field on "${oldT}" still exists on "${newT}". Consumer code accessing fields on the returned value continues to work; only explicit "${oldT}" type annotations need to migrate.`;
|
|
7433
7527
|
}
|
|
7434
7528
|
continue;
|
|
7435
7529
|
}
|
|
@@ -7439,6 +7533,9 @@ function cascadeRenameDowngrades(changes, typeRenames, enumRenames) {
|
|
|
7439
7533
|
if ((typeRenames.get(oldT) ?? enumRenames.get(oldT)) === newT) {
|
|
7440
7534
|
change.severity = "soft-risk";
|
|
7441
7535
|
change.remediation = `Field type swap matches recorded rename "${oldT}" → "${newT}". On-the-wire shape is unchanged (see the rename advisory on "${oldT}").`;
|
|
7536
|
+
} else if (isStructurallyEquivalent(oldT, newT)) {
|
|
7537
|
+
change.severity = "soft-risk";
|
|
7538
|
+
if (!change.remediation) change.remediation = `Field type swap from "${oldT}" to "${newT}" is structurally equivalent — every field on "${oldT}" still exists on "${newT}".`;
|
|
7442
7539
|
}
|
|
7443
7540
|
}
|
|
7444
7541
|
}
|
|
@@ -7468,6 +7565,12 @@ function collectEnumValueSets(snapshot) {
|
|
|
7468
7565
|
}
|
|
7469
7566
|
return result;
|
|
7470
7567
|
}
|
|
7568
|
+
/** Set equality for the small string sets used by rename detection. */
|
|
7569
|
+
function setEquals(a, b) {
|
|
7570
|
+
if (a.size !== b.size) return false;
|
|
7571
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
7572
|
+
return true;
|
|
7573
|
+
}
|
|
7471
7574
|
/**
|
|
7472
7575
|
* Strip array/nullable suffixes so we compare bare type names. Languages
|
|
7473
7576
|
* encode these differently (`Foo[]`, `Foo | null`, `Foo?`, `List<Foo>`,
|
|
@@ -7476,10 +7579,17 @@ function collectEnumValueSets(snapshot) {
|
|
|
7476
7579
|
*/
|
|
7477
7580
|
function bareTypeName(t) {
|
|
7478
7581
|
let s = t.trim();
|
|
7582
|
+
s = s.replace(/^\*/, "");
|
|
7583
|
+
if (s.startsWith("\\")) {
|
|
7584
|
+
const lastBackslash = s.lastIndexOf("\\");
|
|
7585
|
+
s = s.slice(lastBackslash + 1);
|
|
7586
|
+
}
|
|
7479
7587
|
s = s.replace(/\s*\|\s*null$/, "").replace(/\?$/, "");
|
|
7480
7588
|
s = s.replace(/\[\]$/, "");
|
|
7481
|
-
const
|
|
7482
|
-
if (
|
|
7589
|
+
const angleGeneric = s.match(/^[A-Za-z_][A-Za-z0-9_.]*<\s*([A-Za-z_*][A-Za-z0-9_.*]*)\s*>$/);
|
|
7590
|
+
if (angleGeneric) return bareTypeName(angleGeneric[1]);
|
|
7591
|
+
const bracketGeneric = s.match(/^[A-Za-z_][A-Za-z0-9_.]*\[\s*([A-Za-z_*][A-Za-z0-9_.*]*)\s*\]$/);
|
|
7592
|
+
if (bracketGeneric) return bareTypeName(bracketGeneric[1]);
|
|
7483
7593
|
return s;
|
|
7484
7594
|
}
|
|
7485
7595
|
/**
|