@workos/oagen 0.13.0 → 0.14.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.
@@ -1,5 +1,5 @@
1
1
  import { t as safeParse$1, v as ExtractorError } from "../tree-sitter-DcUvm0KA.mjs";
2
- import { A as getDefaultPolicy, C as defaultSeverityForCategory, D as isCompatibleSchemaVersion, E as COMPAT_SCHEMA_VERSION, O as validateSnapshot, S as summarizeChanges, T as apiSurfaceToSnapshot, _ as nodeHints, a as registerExtractor, b as classifyAddedSymbol, c as filterSurface, d as specDerivedHttpKeys, f as specDerivedMethodPaths, g as namedTypeWordsOverlap, h as defaultIsNullableOnlyDifference, i as getExtractor, j as mergePolicy, k as ALL_LANGUAGE_IDS, l as specDerivedEnumValues, m as NAMED_TYPE_RE, n as isPatchableChange, o as diffSnapshots, p as specDerivedNames, r as patchOverlay, s as diffSurfaces, t as buildOverlayLookup, u as specDerivedFieldPaths, v as resolveHints, w as severityMeetsThreshold, x as classifySymbolChanges, y as typeExistsInSurface } from "../overlay-xp7F8_dW.mjs";
2
+ import { A as getDefaultPolicy, C as defaultSeverityForCategory, D as isCompatibleSchemaVersion, E as COMPAT_SCHEMA_VERSION, O as validateSnapshot, S as summarizeChanges, T as apiSurfaceToSnapshot, _ as nodeHints, a as registerExtractor, b as classifyAddedSymbol, c as filterSurface, d as specDerivedHttpKeys, f as specDerivedMethodPaths, g as namedTypeWordsOverlap, h as defaultIsNullableOnlyDifference, i as getExtractor, j as mergePolicy, k as ALL_LANGUAGE_IDS, l as specDerivedEnumValues, m as NAMED_TYPE_RE, n as isPatchableChange, o as diffSnapshots, p as specDerivedNames, r as patchOverlay, s as diffSurfaces, t as buildOverlayLookup, u as specDerivedFieldPaths, v as resolveHints, w as severityMeetsThreshold, x as classifySymbolChanges, y as typeExistsInSurface } from "../overlay-Dlk7bV5R.mjs";
3
3
  import path, { basename, dirname, join, relative, resolve } from "node:path";
4
4
  import Parser from "tree-sitter";
5
5
  import ts from "typescript";
@@ -1 +1 @@
1
- {"version":3,"file":"differ-fdF9qogA.d.mts","names":[],"sources":["../src/compat/classify.ts","../src/compat/spec-filter.ts","../src/compat/differ.ts"],"mappings":";;;;UAaiB,gBAAA;EAcJ;EAZX,QAAA,EAAU,oBAAA;EAAV;EAEA,QAAA,EAAU,oBAAA;EAAV;EAEA,MAAA;EAAA;EAEA,kBAAA;EAEA;EAAA,UAAA,EAAY,gBAAA;EAEZ;EAAA,GAAA,EAAK,MAAA;EAEL;EAAA,GAAA,EAAK,MAAA;EAEL;EAAA,OAAA;EAOW;;AAIb;;;;EAJE,WAAA;AAAA;;UAIe,oBAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA;IACE,QAAA;IACA,QAAA;IACA,QAAA;EAAA;AAAA;;;;;iBAYY,qBAAA,CACd,QAAA,EAAU,YAAA,EACV,SAAA,EAAW,YAAA,cACX,MAAA,EAAQ,iBAAA,GACP,gBAAA;;;;iBAkUa,mBAAA,CAAoB,MAAA,EAAQ,YAAA,GAAe,gBAAA;;iBAc3C,gBAAA,CAAiB,OAAA,EAAS,gBAAA,KAAqB,oBAAA;;;;AAhY/D;;;;iBCJgB,gBAAA,CAAiB,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,aAAA,GAAgB,GAAA;;;;;iBA0CvD,qBAAA,CAAsB,IAAA,EAAM,OAAA,GAAU,GAAA,SAAY,GAAA;;;;;;iBAalD,qBAAA,CAAsB,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,aAAA,GAAgB,GAAA;;;;;;iBAqB5D,sBAAA,CAAuB,IAAA,EAAM,OAAA,GAAU,GAAA;;;;;iBAcvC,mBAAA,CAAoB,IAAA,EAAM,OAAA,GAAU,GAAA;;;;;;;;;;;;AD1CpD;;;;iBC8FgB,aAAA,CACd,OAAA,EAAS,UAAA,EACT,YAAA,EAAc,GAAA,UACd,IAAA;EACE,UAAA,GAAa,GAAA;EACb,WAAA,GAAc,GAAA;EACd,UAAA,GAAa,GAAA,SAAY,GAAA;AAAA,IAE1B,UAAA;;;;UChIc,gBAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA,EAAS,oBAAA;AAAA;;;;;iBAOK,aAAA,CACd,QAAA,EAAU,cAAA,EACV,SAAA,EAAW,cAAA,EACX,MAAA,GAAS,iBAAA,GACR,gBAAA;;;;;;AFJH;;;iBEsMgB,YAAA,CAAa,QAAA,EAAU,UAAA,EAAY,SAAA,EAAW,UAAA,EAAY,KAAA,EAAO,aAAA,GAAgB,UAAA"}
1
+ {"version":3,"file":"differ-fdF9qogA.d.mts","names":[],"sources":["../src/compat/classify.ts","../src/compat/spec-filter.ts","../src/compat/differ.ts"],"mappings":";;;;UAaiB,gBAAA;EAcJ;EAZX,QAAA,EAAU,oBAAA;EAAV;EAEA,QAAA,EAAU,oBAAA;EAAV;EAEA,MAAA;EAAA;EAEA,kBAAA;EAEA;EAAA,UAAA,EAAY,gBAAA;EAEZ;EAAA,GAAA,EAAK,MAAA;EAEL;EAAA,GAAA,EAAK,MAAA;EAEL;EAAA,OAAA;EAOW;;AAIb;;;;EAJE,WAAA;AAAA;;UAIe,oBAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA;IACE,QAAA;IACA,QAAA;IACA,QAAA;EAAA;AAAA;;;;;iBAYY,qBAAA,CACd,QAAA,EAAU,YAAA,EACV,SAAA,EAAW,YAAA,cACX,MAAA,EAAQ,iBAAA,GACP,gBAAA;;;;iBAkUa,mBAAA,CAAoB,MAAA,EAAQ,YAAA,GAAe,gBAAA;;iBAc3C,gBAAA,CAAiB,OAAA,EAAS,gBAAA,KAAqB,oBAAA;;;;AAhY/D;;;;iBCJgB,gBAAA,CAAiB,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,aAAA,GAAgB,GAAA;;;;;iBA0CvD,qBAAA,CAAsB,IAAA,EAAM,OAAA,GAAU,GAAA,SAAY,GAAA;;;;;;iBAalD,qBAAA,CAAsB,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,aAAA,GAAgB,GAAA;;;;;;iBAqB5D,sBAAA,CAAuB,IAAA,EAAM,OAAA,GAAU,GAAA;;;;;iBAcvC,mBAAA,CAAoB,IAAA,EAAM,OAAA,GAAU,GAAA;;;;;;;;;;;;AD1CpD;;;;iBC8FgB,aAAA,CACd,OAAA,EAAS,UAAA,EACT,YAAA,EAAc,GAAA,UACd,IAAA;EACE,UAAA,GAAa,GAAA;EACb,WAAA,GAAc,GAAA;EACd,UAAA,GAAa,GAAA,SAAY,GAAA;AAAA,IAE1B,UAAA;;;;UChIc,gBAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA,EAAS,oBAAA;AAAA;;;;;iBAOK,aAAA,CACd,QAAA,EAAU,cAAA,EACV,SAAA,EAAW,cAAA,EACX,MAAA,GAAS,iBAAA,GACR,gBAAA;;;;;;AFJH;;;iBEopBgB,YAAA,CAAa,QAAA,EAAU,UAAA,EAAY,SAAA,EAAW,UAAA,EAAY,KAAA,EAAO,aAAA,GAAgB,UAAA"}
@@ -913,6 +913,10 @@ function diffSnapshots(baseline, candidate, policy) {
913
913
  changes.push(classifyAddedSymbol(candSym));
914
914
  }
915
915
  detectForkedSchemas(changes, baseline, candidate);
916
+ const typeRenames = detectTypeRenames(changes, baseline, candidate);
917
+ const enumRenames = detectEnumRenames(changes, baseline, candidate);
918
+ inferTransitiveTypeRenames(typeRenames, baseline, candidate, changes);
919
+ cascadeRenameDowngrades(changes, typeRenames, enumRenames, baseline, candidate);
916
920
  return {
917
921
  changes,
918
922
  summary: summarizeChanges(changes)
@@ -987,6 +991,279 @@ function collectTypeFieldSets(snapshot) {
987
991
  return result;
988
992
  }
989
993
  /**
994
+ * Detect type renames: a baseline type symbol disappears and a candidate
995
+ * type symbol with the same (or superset) field set takes its place.
996
+ *
997
+ * Common when an upstream spec promotes a single schema into multiple
998
+ * (e.g. `ApiKey` → `OrganizationApiKey` + `UserApiKey`). The wire shape
999
+ * returned by individual endpoints is unchanged — `OrganizationApiKey`
1000
+ * has the same fields `ApiKey` had — so consumer code accessing those
1001
+ * fields keeps working. The compat report flags `ApiKey` as removed
1002
+ * because its symbol is gone; this pass downgrades the removal to
1003
+ * soft-risk and records the rename so its child fields/methods cascade.
1004
+ *
1005
+ * Identity criteria (must all hold):
1006
+ * 1. The removed symbol owns ≥ 1 field/property in the baseline (i.e.
1007
+ * it's a type-shaped symbol — model/interface/class — not a plain
1008
+ * function or constant).
1009
+ * 2. Some candidate type that did *not* exist in the baseline has a
1010
+ * field set that is a non-strict superset of the removed type's
1011
+ * fields. (Strict-subset would mean fields were lost — a real break.)
1012
+ * 3. The candidate type is the alphabetically-first such match, so
1013
+ * pairing is deterministic when multiple candidates fit (e.g. both
1014
+ * `OrganizationApiKey` and `UserApiKey` share the original fields).
1015
+ *
1016
+ * Returns a `removedName -> newName` map so a downstream cascade pass
1017
+ * can downgrade owned-field removals and `*_type_changed` pointing at
1018
+ * the same pair.
1019
+ *
1020
+ * Mutates matching `symbol_removed` entries in `changes`: severity →
1021
+ * `soft-risk`, attaches a `remediation` describing the rename.
1022
+ */
1023
+ function detectTypeRenames(changes, baseline, candidate) {
1024
+ const renameMap = /* @__PURE__ */ new Map();
1025
+ const baselineTypeFields = collectTypeFieldSets(baseline);
1026
+ const candidateTypeFields = collectTypeFieldSets(candidate);
1027
+ const baselineTypeNames = new Set(baselineTypeFields.keys());
1028
+ const candidateTypesSorted = [...candidateTypeFields.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1029
+ const parentRemovalByName = /* @__PURE__ */ new Map();
1030
+ for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
1031
+ for (const [baselineTypeName, baselineFields] of baselineTypeFields) {
1032
+ if (baselineFields.size === 0) continue;
1033
+ const sameNameFields = candidateTypeFields.get(baselineTypeName);
1034
+ if (sameNameFields && setEquals(baselineFields, sameNameFields)) continue;
1035
+ const match = candidateTypesSorted.find(([candName, candFields]) => {
1036
+ if (baselineTypeNames.has(candName)) return false;
1037
+ for (const f of baselineFields) if (!candFields.has(f)) return false;
1038
+ return true;
1039
+ });
1040
+ if (!match) continue;
1041
+ const [newName] = match;
1042
+ renameMap.set(baselineTypeName, newName);
1043
+ const parentRemoval = parentRemovalByName.get(baselineTypeName);
1044
+ if (parentRemoval) {
1045
+ parentRemoval.severity = "soft-risk";
1046
+ 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.`;
1047
+ }
1048
+ }
1049
+ return renameMap;
1050
+ }
1051
+ /**
1052
+ * Transitive rename inference. After `detectTypeRenames` records direct
1053
+ * structural matches (e.g. `ApiKeyWithValue → OrganizationApiKeyWithValue`),
1054
+ * walk the parent's fields and follow the `typeRef.name` of each: if the
1055
+ * baseline parent's field references an alias-type `OldOwner` and the new
1056
+ * parent's same-named field references a different alias `NewOwner`, the
1057
+ * pair is the same type-concept under a renamed name. Record the
1058
+ * secondary rename so the cascade can downgrade `OldOwner`'s
1059
+ * `symbol_removed` and any `field_type_changed` pointing at the pair.
1060
+ *
1061
+ * Why this is necessary: discriminated-union owner types (Go's
1062
+ * `APIKeyWithValueOwner`, PHP/Python's nested constructor params) are
1063
+ * frequently emitted as `kind: 'alias'` symbols with no extractable
1064
+ * field children — so `collectTypeFieldSets` returns empty for them and
1065
+ * the structural matcher can't pair them directly. The transitive
1066
+ * lookup gives us a deterministic, conservative way to propagate the
1067
+ * rename: it only fires when the parent rename has already been
1068
+ * positively identified, and only follows fields that share the same
1069
+ * wire name across baseline and candidate.
1070
+ */
1071
+ function inferTransitiveTypeRenames(typeRenames, baseline, candidate, changes) {
1072
+ if (typeRenames.size === 0) return;
1073
+ const baselineFieldType = /* @__PURE__ */ new Map();
1074
+ for (const sym of baseline.symbols) {
1075
+ if (sym.kind !== "field" && sym.kind !== "property") continue;
1076
+ if (!sym.ownerFqName || !sym.typeRef) continue;
1077
+ const localName = sym.fqName.includes(".") ? sym.fqName.slice(sym.fqName.lastIndexOf(".") + 1) : sym.fqName;
1078
+ baselineFieldType.set(`${sym.ownerFqName}.${localName.toLowerCase()}`, sym.typeRef.name);
1079
+ }
1080
+ const candidateFieldType = /* @__PURE__ */ new Map();
1081
+ for (const sym of candidate.symbols) {
1082
+ if (sym.kind !== "field" && sym.kind !== "property") continue;
1083
+ if (!sym.ownerFqName || !sym.typeRef) continue;
1084
+ const localName = sym.fqName.includes(".") ? sym.fqName.slice(sym.fqName.lastIndexOf(".") + 1) : sym.fqName;
1085
+ candidateFieldType.set(`${sym.ownerFqName}.${localName.toLowerCase()}`, sym.typeRef.name);
1086
+ }
1087
+ const parentRemovalByName = /* @__PURE__ */ new Map();
1088
+ for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
1089
+ const initialEntries = [...typeRenames.entries()];
1090
+ for (const [oldOwner, newOwner] of initialEntries) for (const [baselineKey, baselineFieldTypeRef] of baselineFieldType) {
1091
+ if (!baselineKey.startsWith(`${oldOwner}.`)) continue;
1092
+ const localName = baselineKey.slice(oldOwner.length + 1);
1093
+ const candidateKey = `${newOwner}.${localName}`;
1094
+ const candidateFieldTypeRef = candidateFieldType.get(candidateKey);
1095
+ if (!candidateFieldTypeRef) continue;
1096
+ const oldT = bareTypeName(baselineFieldTypeRef);
1097
+ const newT = bareTypeName(candidateFieldTypeRef);
1098
+ if (!oldT || !newT || oldT === newT) continue;
1099
+ if (typeRenames.has(oldT)) continue;
1100
+ typeRenames.set(oldT, newT);
1101
+ const removal = parentRemovalByName.get(oldT);
1102
+ if (removal) {
1103
+ removal.severity = "soft-risk";
1104
+ 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.`;
1105
+ }
1106
+ }
1107
+ }
1108
+ /**
1109
+ * Detect enum canonical-flips: a baseline enum disappears and a candidate
1110
+ * enum with the **same wire-value set** takes its place.
1111
+ *
1112
+ * Caused by the emitter's enum-dedup heuristic picking a different
1113
+ * canonical name when a new same-shape enum joins the spec. Languages
1114
+ * that emit type aliases (Go, Ruby, Python, PHP, Kotlin) handle this
1115
+ * transparently via `type Old = New`; languages without first-class
1116
+ * aliases (dotnet) report the old enum as removed. The wire values are
1117
+ * unchanged — every legal value still serializes to the same JSON — so
1118
+ * consumer code constructing or matching on these enum values keeps
1119
+ * working. Only references to the typed enum class need migration.
1120
+ *
1121
+ * Identity criterion: a removed enum's value set is *exactly* equal to
1122
+ * a newly-added enum's value set (not superset — narrowing the value
1123
+ * set would be a real break for consumers expecting the dropped values).
1124
+ */
1125
+ function detectEnumRenames(changes, baseline, candidate) {
1126
+ const renameMap = /* @__PURE__ */ new Map();
1127
+ const baselineEnumValues = collectEnumValueSets(baseline);
1128
+ const candidateEnumValues = collectEnumValueSets(candidate);
1129
+ const baselineEnumNames = new Set(baselineEnumValues.keys());
1130
+ const candidateEnumsSorted = [...candidateEnumValues.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1131
+ const parentRemovalByName = /* @__PURE__ */ new Map();
1132
+ for (const change of changes) if (change.category === "symbol_removed" && change.severity === "breaking") parentRemovalByName.set(change.old.symbol ?? "", change);
1133
+ for (const [baselineEnumName, baselineValues] of baselineEnumValues) {
1134
+ if (baselineValues.size === 0) continue;
1135
+ const sameNameValues = candidateEnumValues.get(baselineEnumName);
1136
+ if (sameNameValues && setEquals(baselineValues, sameNameValues)) continue;
1137
+ const match = candidateEnumsSorted.find(([candName, candValues]) => !baselineEnumNames.has(candName) && setEquals(baselineValues, candValues));
1138
+ if (!match) continue;
1139
+ const [newName] = match;
1140
+ renameMap.set(baselineEnumName, newName);
1141
+ const parentRemoval = parentRemovalByName.get(baselineEnumName);
1142
+ if (parentRemoval) {
1143
+ parentRemoval.severity = "soft-risk";
1144
+ 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.`;
1145
+ }
1146
+ }
1147
+ return renameMap;
1148
+ }
1149
+ /**
1150
+ * Cascade rename downgrades to changes whose meaning depends on a renamed
1151
+ * symbol. Walks every change and:
1152
+ *
1153
+ * - Downgrades child removals (`Owner.field` removed where `Owner` was
1154
+ * renamed) — the field still exists, just under a new owner fqName.
1155
+ * Same logic for enum members under a renamed enum.
1156
+ * - Downgrades `return_type_changed` / `field_type_changed` whose
1157
+ * old → new pair matches a recorded rename — the type swap is the
1158
+ * rename itself, not a meaningful signature break.
1159
+ *
1160
+ * Each cascaded change gets a remediation pointing at the parent rename
1161
+ * so the reviewer can find the explanation in the report.
1162
+ */
1163
+ function cascadeRenameDowngrades(changes, typeRenames, enumRenames, baseline, candidate) {
1164
+ let baselineTypeFields;
1165
+ let candidateTypeFields;
1166
+ const getBaselineFields = () => {
1167
+ if (!baselineTypeFields) baselineTypeFields = collectTypeFieldSets(baseline);
1168
+ return baselineTypeFields;
1169
+ };
1170
+ const getCandidateFields = () => {
1171
+ if (!candidateTypeFields) candidateTypeFields = collectTypeFieldSets(candidate);
1172
+ return candidateTypeFields;
1173
+ };
1174
+ /**
1175
+ * Structural equivalence: candidate type's field set is a non-strict
1176
+ * superset of baseline type's field set, AND the candidate type was
1177
+ * newly added in this diff (not a swap to a pre-existing type, which
1178
+ * is a real signature break the consumer chose). When `New ⊇ Old` and
1179
+ * `New` is new, every field the consumer accessed on the old type
1180
+ * still exists on the value they receive — so the swap is non-breaking
1181
+ * at the value level even though the declared type changed. This is
1182
+ * the fork-detector's positive signal, applied as a severity
1183
+ * downgrade in addition to the existing remediation hint.
1184
+ */
1185
+ const isStructurallyEquivalent = (oldT, newT) => {
1186
+ if (!oldT || !newT || oldT === newT) return false;
1187
+ const baselineFields = getBaselineFields();
1188
+ const candidateFields = getCandidateFields();
1189
+ if (baselineFields.has(newT)) return false;
1190
+ const oldFields = baselineFields.get(oldT);
1191
+ const newFields = candidateFields.get(newT);
1192
+ if (!oldFields || !newFields) return false;
1193
+ if (oldFields.size === 0) return false;
1194
+ for (const f of oldFields) if (!newFields.has(f)) return false;
1195
+ return true;
1196
+ };
1197
+ for (const change of changes) {
1198
+ if (change.severity !== "breaking") continue;
1199
+ if (change.category === "symbol_removed") {
1200
+ const removed = change.old.symbol ?? "";
1201
+ const dotIdx = removed.indexOf(".");
1202
+ if (dotIdx <= 0) continue;
1203
+ const ownerName = removed.slice(0, dotIdx);
1204
+ const renamedTo = typeRenames.get(ownerName) ?? enumRenames.get(ownerName);
1205
+ if (!renamedTo) continue;
1206
+ change.severity = "soft-risk";
1207
+ change.remediation = `Owned by renamed symbol "${ownerName}" (now "${renamedTo}"). The same member exists on the new symbol under "${renamedTo}.${removed.slice(dotIdx + 1)}".`;
1208
+ continue;
1209
+ }
1210
+ if (change.category === "return_type_changed") {
1211
+ const oldT = bareTypeName(change.old.returnType ?? "");
1212
+ const newT = bareTypeName(change.new.returnType ?? "");
1213
+ if (typeRenames.get(oldT) === newT) {
1214
+ change.severity = "soft-risk";
1215
+ change.remediation = `Return type swap matches recorded rename "${oldT}" → "${newT}". The underlying field set is preserved (see the rename advisory on "${oldT}").`;
1216
+ } else if (isStructurallyEquivalent(oldT, newT)) {
1217
+ change.severity = "soft-risk";
1218
+ 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.`;
1219
+ }
1220
+ continue;
1221
+ }
1222
+ if (change.category === "field_type_changed") {
1223
+ const oldT = bareTypeName(change.old.type ?? "");
1224
+ const newT = bareTypeName(change.new.type ?? "");
1225
+ if ((typeRenames.get(oldT) ?? enumRenames.get(oldT)) === newT) {
1226
+ change.severity = "soft-risk";
1227
+ change.remediation = `Field type swap matches recorded rename "${oldT}" → "${newT}". On-the-wire shape is unchanged (see the rename advisory on "${oldT}").`;
1228
+ } else if (isStructurallyEquivalent(oldT, newT)) {
1229
+ change.severity = "soft-risk";
1230
+ if (!change.remediation) change.remediation = `Field type swap from "${oldT}" to "${newT}" is structurally equivalent — every field on "${oldT}" still exists on "${newT}".`;
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ /**
1236
+ * Build a map from enum fqName → set of wire values. Used by
1237
+ * `detectEnumRenames` to find structurally-identical enums across
1238
+ * baseline and candidate. Wire values come from `enum_member.value`
1239
+ * (the JSON-level value) — not the member names, which are
1240
+ * language-specific PascalCase forms.
1241
+ *
1242
+ * Members whose `value` is undefined are skipped — they contribute no
1243
+ * identity information.
1244
+ */
1245
+ function collectEnumValueSets(snapshot) {
1246
+ const result = /* @__PURE__ */ new Map();
1247
+ for (const sym of snapshot.symbols) {
1248
+ if (sym.kind !== "enum_member") continue;
1249
+ if (!sym.ownerFqName) continue;
1250
+ if (sym.value === void 0) continue;
1251
+ let set = result.get(sym.ownerFqName);
1252
+ if (!set) {
1253
+ set = /* @__PURE__ */ new Set();
1254
+ result.set(sym.ownerFqName, set);
1255
+ }
1256
+ set.add(String(sym.value));
1257
+ }
1258
+ return result;
1259
+ }
1260
+ /** Set equality for the small string sets used by rename detection. */
1261
+ function setEquals(a, b) {
1262
+ if (a.size !== b.size) return false;
1263
+ for (const v of a) if (!b.has(v)) return false;
1264
+ return true;
1265
+ }
1266
+ /**
990
1267
  * Strip array/nullable suffixes so we compare bare type names. Languages
991
1268
  * encode these differently (`Foo[]`, `Foo | null`, `Foo?`, `List<Foo>`,
992
1269
  * `Optional[Foo]`); this is a best-effort common-case stripper, not an
@@ -994,10 +1271,13 @@ function collectTypeFieldSets(snapshot) {
994
1271
  */
995
1272
  function bareTypeName(t) {
996
1273
  let s = t.trim();
1274
+ s = s.replace(/^\*/, "");
997
1275
  s = s.replace(/\s*\|\s*null$/, "").replace(/\?$/, "");
998
1276
  s = s.replace(/\[\]$/, "");
999
- const generic = s.match(/^[A-Za-z_][A-Za-z0-9_.]*<\s*([A-Za-z_][A-Za-z0-9_.]*)\s*>$/);
1000
- if (generic) s = generic[1];
1277
+ const angleGeneric = s.match(/^[A-Za-z_][A-Za-z0-9_.]*<\s*([A-Za-z_*][A-Za-z0-9_.*]*)\s*>$/);
1278
+ if (angleGeneric) return bareTypeName(angleGeneric[1]);
1279
+ const bracketGeneric = s.match(/^[A-Za-z_][A-Za-z0-9_.]*\[\s*([A-Za-z_*][A-Za-z0-9_.*]*)\s*\]$/);
1280
+ if (bracketGeneric) return bareTypeName(bracketGeneric[1]);
1001
1281
  return s;
1002
1282
  }
1003
1283
  /**
@@ -1794,4 +2074,4 @@ function patchOverlay(overlay, changes, baseline) {
1794
2074
  //#endregion
1795
2075
  export { getDefaultPolicy as A, defaultSeverityForCategory as C, isCompatibleSchemaVersion as D, COMPAT_SCHEMA_VERSION as E, validateSnapshot as O, summarizeChanges as S, apiSurfaceToSnapshot as T, nodeHints as _, registerExtractor as a, classifyAddedSymbol as b, filterSurface as c, specDerivedHttpKeys as d, specDerivedMethodPaths as f, namedTypeWordsOverlap as g, defaultIsNullableOnlyDifference as h, getExtractor as i, mergePolicy as j, ALL_LANGUAGE_IDS as k, specDerivedEnumValues as l, NAMED_TYPE_RE as m, isPatchableChange as n, diffSnapshots as o, specDerivedNames as p, patchOverlay as r, diffSurfaces as s, buildOverlayLookup as t, specDerivedFieldPaths as u, resolveHints as v, severityMeetsThreshold as w, classifySymbolChanges as x, typeExistsInSurface as y };
1796
2076
 
1797
- //# sourceMappingURL=overlay-xp7F8_dW.mjs.map
2077
+ //# sourceMappingURL=overlay-Dlk7bV5R.mjs.map