@workos/oagen 0.29.1 → 0.29.3

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.
@@ -246,7 +246,10 @@ function apiSurfaceToSnapshot(surface) {
246
246
  }
247
247
  return {
248
248
  schemaVersion: "1",
249
- source: { extractedAt: surface.extractedAt },
249
+ source: {
250
+ extractedAt: surface.extractedAt,
251
+ language
252
+ },
250
253
  policies: getDefaultPolicy(language),
251
254
  symbols
252
255
  };
@@ -326,10 +329,138 @@ function severityMeetsThreshold(severity, threshold) {
326
329
  //#endregion
327
330
  //#region src/compat/classify.ts
328
331
  /**
332
+ * Normalize a type annotation string to a canonical form before equality
333
+ * comparison. Emitters evolve their annotation style across releases (e.g.
334
+ * Python's PEP 604 `X | None` replacing `Optional[X]`, PEP 585 lowercase
335
+ * generics, or dropping quotes from forward refs); such changes are cosmetic
336
+ * and must not register as breaking type changes. Only Python needs this
337
+ * today — other languages encode optionality with a single stable spelling
338
+ * (`*Foo`, `Option<T>`, `T?`, `T.nilable(Foo)`) so they use exact comparison.
339
+ * String-literal *values* inside `Literal[...]` are preserved verbatim so a
340
+ * genuine change to a wire value stays visible. When `language` is absent
341
+ * (older snapshots) no normalization is applied, preserving prior behaviour.
342
+ */
343
+ function normalizeType(type, language) {
344
+ if (!type) return type;
345
+ if (language !== "python") return type;
346
+ return canonicalizePythonType(type);
347
+ }
348
+ /** PEP 585 capitalized builtins that map to their lowercase spellings, so
349
+ * `List[str]` and `list[str]` compare equal. */
350
+ const PEP585_BUILTINS = {
351
+ List: "list",
352
+ Dict: "dict",
353
+ Tuple: "tuple",
354
+ Set: "set",
355
+ FrozenSet: "frozenset",
356
+ Type: "type"
357
+ };
358
+ /** Recursively canonicalize a Python annotation. Structural forms are
359
+ * normalized (`Optional[X]`→`X | None`, `Union[...]`→` | ` joins, PEP 585
360
+ * builtins, quoted forward refs, parenthesized unions), but `Literal[...]`
361
+ * arguments are preserved verbatim — their quotes and inner whitespace are
362
+ * part of the wire value, not cosmetic. */
363
+ function canonicalizePythonType(s) {
364
+ const t = s.trim();
365
+ if (t.length === 0) return t;
366
+ if (t.startsWith("(") && t.endsWith(")")) return canonicalizePythonType(t.slice(1, -1));
367
+ if (t.startsWith("Literal[") && t.endsWith("]")) return `Literal[${splitTopLevel(t.slice(8, -1), ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizeLiteralArg).join(", ")}]`;
368
+ if (t.startsWith("Optional[") && t.endsWith("]")) return `${canonicalizePythonType(t.slice(9, -1))} | None`;
369
+ if (t.startsWith("Union[") && t.endsWith("]")) return splitTopLevel(t.slice(6, -1), ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizePythonType).join(" | ");
370
+ if (t.includes("|")) {
371
+ const arms = splitTopLevel(t, "|").map((p) => p.trim()).filter((p) => p.length > 0);
372
+ if (arms.length > 1) return arms.map(canonicalizePythonType).join(" | ");
373
+ }
374
+ const open = t.indexOf("[");
375
+ if (open !== -1 && t.endsWith("]")) {
376
+ const outer = t.slice(0, open).trim();
377
+ const inner = t.slice(open + 1, -1);
378
+ return `${PEP585_BUILTINS[outer] ?? outer}[${splitTopLevel(inner, ",").map((p) => p.trim()).filter((p) => p.length > 0).map(canonicalizePythonType).join(", ")}]`;
379
+ }
380
+ if (t.startsWith("\"") && t.endsWith("\"") && t.length >= 2 || t.startsWith("'") && t.endsWith("'") && t.length >= 2) return t.slice(1, -1);
381
+ return PEP585_BUILTINS[t] ?? t;
382
+ }
383
+ /** Canonicalize one `Literal[...]` argument so semantically identical string
384
+ * values compare equal regardless of quote style or escape form. The value
385
+ * is decoded (backslash escapes processed) then re-encoded in a canonical
386
+ * double-quoted form, so `Literal['say "hi"']` and `Literal["say \\"hi\\""]`
387
+ * both canonicalize to `"say \"hi\""`. Bare (unquoted) arguments — e.g. an
388
+ * enum name or a number — are returned unchanged. */
389
+ function canonicalizeLiteralArg(arg) {
390
+ const a = arg.trim();
391
+ if (a.length < 2) return a;
392
+ let quote = null;
393
+ if (a.startsWith("\"") && a.endsWith("\"")) quote = "\"";
394
+ else if (a.startsWith("'") && a.endsWith("'")) quote = "'";
395
+ if (!quote) return a;
396
+ return `"${encodeDoubleQuoted(decodeStringLiteral(a.slice(1, -1)))}"`;
397
+ }
398
+ /** Decode the backslash escapes inside a Python string literal's content.
399
+ * Only the delimiter-relevant escapes (`\\`, `\'`, `\"`) are processed;
400
+ * other sequences (`\n`, `\t`, `\u…`) are kept verbatim since they're
401
+ * written identically regardless of quote style and so compare equal already. */
402
+ function decodeStringLiteral(content) {
403
+ let out = "";
404
+ for (let i = 0; i < content.length; i++) {
405
+ const c = content[i];
406
+ if (c === "\\" && i + 1 < content.length) {
407
+ const next = content[i + 1];
408
+ if (next === "\\" || next === "\"" || next === "'") {
409
+ out += next;
410
+ i++;
411
+ continue;
412
+ }
413
+ out += c + next;
414
+ i++;
415
+ continue;
416
+ }
417
+ out += c;
418
+ }
419
+ return out;
420
+ }
421
+ /** Re-encode a decoded value as a canonical double-quoted Python string. */
422
+ function encodeDoubleQuoted(value) {
423
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
424
+ }
425
+ /** Split on a separator that sits at bracket-depth 0 and outside string
426
+ * literals, so commas/pipes inside `Literal["a,b"]` or `"x|y"` are not split.
427
+ * An escaped matching quote (`\"` inside a double-quoted string) does not
428
+ * close the string: only a quote preceded by an even number of backslashes
429
+ * (zero counts) is a real closing delimiter. */
430
+ function splitTopLevel(s, sep) {
431
+ const parts = [];
432
+ let depth = 0;
433
+ let inStr = null;
434
+ let start = 0;
435
+ for (let i = 0; i < s.length; i++) {
436
+ const c = s[i];
437
+ if (inStr) {
438
+ if (c === inStr) {
439
+ let precedingBackslashes = 0;
440
+ for (let j = i - 1; j >= 0 && s[j] === "\\"; j--) precedingBackslashes++;
441
+ if (precedingBackslashes % 2 === 0) inStr = null;
442
+ }
443
+ continue;
444
+ }
445
+ if (c === "\"" || c === "'") {
446
+ inStr = c;
447
+ continue;
448
+ }
449
+ if (c === "[") depth++;
450
+ else if (c === "]") depth--;
451
+ else if (c === sep && depth === 0) {
452
+ parts.push(s.slice(start, i));
453
+ start = i + 1;
454
+ }
455
+ }
456
+ parts.push(s.slice(start));
457
+ return parts;
458
+ }
459
+ /**
329
460
  * Classify changes between a baseline and candidate symbol.
330
461
  * Returns one or more classified changes for the diff.
331
462
  */
332
- function classifySymbolChanges(baseline, candidate, policy) {
463
+ function classifySymbolChanges(baseline, candidate, policy, language) {
333
464
  const changes = [];
334
465
  const specRef = baseline.schemaName ?? candidate?.schemaName;
335
466
  if (!candidate) {
@@ -353,8 +484,8 @@ function classifySymbolChanges(baseline, candidate, policy) {
353
484
  policy,
354
485
  specRef
355
486
  }));
356
- if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy, specRef));
357
- if (baseline.returns && candidate.returns && baseline.returns.name !== candidate.returns.name) changes.push(makeChange({
487
+ if (baseline.parameters && candidate.parameters) changes.push(...classifyParameterChanges(baseline, candidate, policy, specRef, language));
488
+ if (baseline.returns && candidate.returns && normalizeType(baseline.returns.name, language) !== normalizeType(candidate.returns.name, language)) changes.push(makeChange({
358
489
  category: "return_type_changed",
359
490
  symbol: baseline.fqName,
360
491
  old: { returnType: baseline.returns.name },
@@ -372,7 +503,7 @@ function classifySymbolChanges(baseline, candidate, policy) {
372
503
  policy,
373
504
  specRef
374
505
  }));
375
- if (baseline.typeRef && candidate.typeRef && baseline.typeRef.name !== candidate.typeRef.name) changes.push(makeChange({
506
+ if (baseline.typeRef && candidate.typeRef && normalizeType(baseline.typeRef.name, language) !== normalizeType(candidate.typeRef.name, language)) changes.push(makeChange({
376
507
  category: "field_type_changed",
377
508
  symbol: baseline.fqName,
378
509
  old: { type: baseline.typeRef.name },
@@ -394,7 +525,7 @@ function classifySymbolChanges(baseline, candidate, policy) {
394
525
  /**
395
526
  * Classify parameter-level changes between two symbol versions.
396
527
  */
397
- function classifyParameterChanges(baseline, candidate, policy, specRef) {
528
+ function classifyParameterChanges(baseline, candidate, policy, specRef, language) {
398
529
  const changes = [];
399
530
  const baseParams = baseline.parameters ?? [];
400
531
  const candParams = candidate.parameters ?? [];
@@ -443,7 +574,7 @@ function classifyParameterChanges(baseline, candidate, policy, specRef) {
443
574
  policy,
444
575
  specRef
445
576
  }));
446
- if (baseParam.type.name !== candParam.type.name) changes.push(makeChange({
577
+ if (normalizeType(baseParam.type.name, language) !== normalizeType(candParam.type.name, language)) changes.push(makeChange({
447
578
  category: "parameter_type_narrowed",
448
579
  symbol: baseline.fqName,
449
580
  old: {
@@ -956,7 +1087,7 @@ function diffSnapshots(baseline, candidate, policy) {
956
1087
  for (const baseSym of baseline.symbols) {
957
1088
  if (isServiceWrapperConstructor(baseSym, serviceAccessors)) continue;
958
1089
  const candSym = candById.get(baseSym.id) ?? candByFqName.get(baseSym.fqName);
959
- changes.push(...classifySymbolChanges(baseSym, candSym, effectivePolicy));
1090
+ changes.push(...classifySymbolChanges(baseSym, candSym, effectivePolicy, baseline.source.language));
960
1091
  }
961
1092
  for (const candSym of candidate.symbols) if (!baseByFqName.has(candSym.fqName)) {
962
1093
  if (isServiceWrapperConstructor(candSym, serviceAccessors)) continue;
@@ -2123,4 +2254,4 @@ function patchOverlay(overlay, changes, baseline) {
2123
2254
  //#endregion
2124
2255
  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 };
2125
2256
 
2126
- //# sourceMappingURL=overlay-jsSkFJ2g.mjs.map
2257
+ //# sourceMappingURL=overlay-CihWp6dQ.mjs.map