@workos/oagen-emitters 0.16.0 → 0.16.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,8 +1,8 @@
1
+ import { assignModelsToServices, assignModelsToServices as assignModelsToServices$1, collectEnumRefs, collectFieldDependencies, collectFieldDependencies as collectFieldDependencies$1, collectModelRefs, mapTypeRef, planOperation, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, walkTypeRef } from "@workos/oagen";
1
2
  import * as fs$1 from "node:fs";
2
3
  import fs, { existsSync, readFileSync } from "node:fs";
3
4
  import * as path$1 from "node:path";
4
5
  import path, { resolve } from "node:path";
5
- import { assignModelsToServices, assignModelsToServices as assignModelsToServices$1, collectEnumRefs, collectFieldDependencies, collectFieldDependencies as collectFieldDependencies$1, collectModelRefs, mapTypeRef, planOperation, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, toUpperSnakeCase, walkTypeRef } from "@workos/oagen";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { dotnetExtractor, elixirExtractor, goExtractor, kotlinExtractor, nodeExtractor, phpExtractor, pythonExtractor, rubyExtractor, rustExtractor } from "@workos/oagen/compat";
8
8
  import { fileURLToPath } from "node:url";
@@ -2844,6 +2844,25 @@ function setBaselineInterfaceNames(names) {
2844
2844
  baselineInterfaceNames = names;
2845
2845
  }
2846
2846
  /**
2847
+ * Every name DECLARED by the live SDK or baseline api-surface — interfaces
2848
+ * AND type aliases. Exact-name declarations preempt structural renames in
2849
+ * `resolveInterfaceName`: when the IR model's own name is already declared,
2850
+ * the structural matcher must not re-point it at a different declaration.
2851
+ *
2852
+ * This matters for alias-form files (`export type X = Y;`): the engine's
2853
+ * api-surface records X under `typeAliases` with no fields, so its
2854
+ * exact-name pass cannot claim X and the Jaccard fallback "renames" IR
2855
+ * model X to whatever interface looks similar. Propagating that rename
2856
+ * emitted duplicate, renamed declarations whose form flip-flopped on every
2857
+ * regeneration (workos-node ApiKeyOwner / UserManagement model files).
2858
+ *
2859
+ * Set by `index.ts` immediately after `getSurface(ctx)` runs.
2860
+ */
2861
+ let baselineDeclaredNames = /* @__PURE__ */ new Set();
2862
+ function setBaselineDeclaredNames(names) {
2863
+ baselineDeclaredNames = names;
2864
+ }
2865
+ /**
2847
2866
  * IR models that belong to newly-adopted services should not be renamed by
2848
2867
  * structural baseline matches from unrelated hand-written services.
2849
2868
  */
@@ -2880,6 +2899,142 @@ function setStructurallyRenamedDomainNames(names) {
2880
2899
  structurallyRenamedDomainNames = names;
2881
2900
  }
2882
2901
  /**
2902
+ * The structural half of `resolveInterfaceName`, pre-injectivity: look up the
2903
+ * engine's structurally-inferred match (`overlayLookup.modelNameByIR`), apply
2904
+ * the adopted/discriminated/declared-name guards, and normalize legacy
2905
+ * `Serialized*` / wire-shaped `*Response` matches down to the baseline domain
2906
+ * name. Returns the candidate live name, or undefined when no structural
2907
+ * match applies. Shared by `resolveInterfaceName` and the claims registry so
2908
+ * both see the exact same candidate for every IR model.
2909
+ */
2910
+ function inferStructuralRename(name, ctx) {
2911
+ let inferred = adoptedModelNames.has(name) || discriminatedModelNames.has(name) ? void 0 : ctx.overlayLookup?.modelNameByIR?.get(name);
2912
+ if (inferred && inferred !== name && baselineDeclaredNames.has(name)) return;
2913
+ if (!inferred) return void 0;
2914
+ if (inferred.startsWith("Serialized")) {
2915
+ const stripped = inferred.slice(10);
2916
+ if (stripped && ctx.apiSurface?.interfaces?.[stripped]) inferred = stripped;
2917
+ }
2918
+ if (inferred.endsWith("Response") && ctx.apiSurface?.interfaces) {
2919
+ const stripped = inferred.slice(0, -8);
2920
+ if (stripped && ctx.apiSurface.interfaces[stripped]) inferred = stripped;
2921
+ }
2922
+ return inferred;
2923
+ }
2924
+ /**
2925
+ * Per-run registry making structural name resolution INJECTIVE: live-surface
2926
+ * name → the single IR model name allowed to claim it. Built lazily from
2927
+ * `ctx.spec.models` on first use and cached per ctx.
2928
+ *
2929
+ * Cache-correctness invariant (relied on, not assumed): the `ctx` reaching
2930
+ * this function is always the memoized `nodeCtx` from `withNodeOperationOverrides`
2931
+ * — every emitter hook derives it as its first step and threads it through
2932
+ * `getSurface`/`resolveInterfaceName`, and that helper returns one stable
2933
+ * object per run. `nodeCtx.spec` is built once via spread and `spec.models` is
2934
+ * never reassigned or mutated in place anywhere (enrichment pushes only onto a
2935
+ * pre-enrichment local collector). So the cached value can never drift from
2936
+ * the `spec.models` it was computed from. Do not begin mutating `spec.models`
2937
+ * under a live ctx without invalidating this cache.
2938
+ *
2939
+ * Without it, the structural fallback could map two distinct IR models onto
2940
+ * one live declaration. workos-node AuditLogs evidence: IR
2941
+ * `AuditLogEventActor` and `AuditLogEventTarget` (near-identical shapes) both
2942
+ * resolved to the hand-written `AuditLogActor`, so
2943
+ * `audit-log-event-target.interface.ts` was emitted declaring
2944
+ * `export interface AuditLogActor` (file stem and declaration disagree),
2945
+ * with duplicate imports/`describe` blocks and two `serializeAuditLogActor`
2946
+ * definitions downstream. The raw engine overlay is injective on its own
2947
+ * names, but the resolver's `Serialized*`/`*Response` normalization below can
2948
+ * collapse two distinct raw matches onto one bare name — the claims registry
2949
+ * gates the final, post-normalization answer.
2950
+ *
2951
+ * Claim order:
2952
+ * 1. Exact-name overrides (`overlayLookup.interfaceByName`) claim first.
2953
+ * 2. Identity structural matches (IR name === live name) claim their own name.
2954
+ * 3. Contested renames go to the claimant with the higher field-overlap
2955
+ * similarity; ties break toward the closer name (edit distance), then
2956
+ * lexicographically for determinism. Losers are NEVER unified — they keep
2957
+ * their canonical IR-derived names.
2958
+ */
2959
+ const structuralClaimsCache = /* @__PURE__ */ new WeakMap();
2960
+ /** Jaccard similarity between two normalized field-name sets. */
2961
+ function fieldJaccard(a, b) {
2962
+ if (a.size === 0 && b.size === 0) return 0;
2963
+ let intersection = 0;
2964
+ for (const item of a) if (b.has(item)) intersection++;
2965
+ const union = a.size + b.size - intersection;
2966
+ return union === 0 ? 0 : intersection / union;
2967
+ }
2968
+ /** Levenshtein distance over lowercased names (tie-break for contested claims). */
2969
+ function nameDistance(a, b) {
2970
+ const s = a.toLowerCase();
2971
+ const t = b.toLowerCase();
2972
+ if (s === t) return 0;
2973
+ let prev = Array.from({ length: t.length + 1 }, (_, i) => i);
2974
+ for (let i = 1; i <= s.length; i++) {
2975
+ const curr = [i];
2976
+ for (let j = 1; j <= t.length; j++) {
2977
+ const cost = s[i - 1] === t[j - 1] ? 0 : 1;
2978
+ curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
2979
+ }
2980
+ prev = curr;
2981
+ }
2982
+ return prev[t.length];
2983
+ }
2984
+ /** Normalized field-name signature of an IR model. */
2985
+ function irFieldSignature(model) {
2986
+ return new Set(model.fields.map((f) => toSnakeCase(f.name)));
2987
+ }
2988
+ /** Normalized field-name signature of a live-surface interface, if known. */
2989
+ function liveFieldSignature(ctx, liveName) {
2990
+ const iface = ctx.apiSurface?.interfaces?.[liveName];
2991
+ if (!iface?.fields) return void 0;
2992
+ return new Set(Object.keys(iface.fields).map((f) => toSnakeCase(f)));
2993
+ }
2994
+ function getStructuralNameClaims(ctx) {
2995
+ const cached = structuralClaimsCache.get(ctx);
2996
+ if (cached) return cached;
2997
+ const claims = /* @__PURE__ */ new Map();
2998
+ for (const [irName, liveName] of ctx.overlayLookup?.interfaceByName ?? /* @__PURE__ */ new Map()) if (!claims.has(liveName)) claims.set(liveName, irName);
2999
+ const contested = /* @__PURE__ */ new Map();
3000
+ const modelsByName = /* @__PURE__ */ new Map();
3001
+ for (const model of ctx.spec.models) {
3002
+ modelsByName.set(model.name, model);
3003
+ if (ctx.overlayLookup?.interfaceByName?.has(model.name)) continue;
3004
+ const inferred = inferStructuralRename(model.name, ctx);
3005
+ if (!inferred) continue;
3006
+ if (inferred === model.name) {
3007
+ if (!claims.has(inferred)) claims.set(inferred, model.name);
3008
+ continue;
3009
+ }
3010
+ const group = contested.get(inferred);
3011
+ if (group) group.push(model.name);
3012
+ else contested.set(inferred, [model.name]);
3013
+ }
3014
+ for (const [liveName, irNames] of contested) {
3015
+ if (claims.has(liveName)) continue;
3016
+ let winner = irNames[0];
3017
+ if (irNames.length > 1) {
3018
+ const liveFields = liveFieldSignature(ctx, liveName);
3019
+ const scoreOf = (irName) => {
3020
+ const model = modelsByName.get(irName);
3021
+ if (!model || !liveFields) return 0;
3022
+ return fieldJaccard(irFieldSignature(model), liveFields);
3023
+ };
3024
+ winner = [...irNames].sort((a, b) => {
3025
+ const scoreDiff = scoreOf(b) - scoreOf(a);
3026
+ if (scoreDiff !== 0) return scoreDiff;
3027
+ const distDiff = nameDistance(a, liveName) - nameDistance(b, liveName);
3028
+ if (distDiff !== 0) return distDiff;
3029
+ return a < b ? -1 : a > b ? 1 : 0;
3030
+ })[0];
3031
+ }
3032
+ claims.set(liveName, winner);
3033
+ }
3034
+ structuralClaimsCache.set(ctx, claims);
3035
+ return claims;
3036
+ }
3037
+ /**
2883
3038
  * Wire/response interface name.
2884
3039
  *
2885
3040
  * Resolution order:
@@ -2978,7 +3133,8 @@ function resolveClassName$5(service, ctx) {
2978
3133
  * 1. `overlayLookup.interfaceByName` — exact-name overrides from the live SDK.
2979
3134
  * 2. `overlayLookup.modelNameByIR` — structurally-inferred matches (e.g., IR
2980
3135
  * `ValidateApiKey` with one field `value: string` → live SDK interface
2981
- * `ValidateApiKeyOptions`).
3136
+ * `ValidateApiKeyOptions`), gated by the injective claims registry: each
3137
+ * live name goes to at most one IR model (see `getStructuralNameClaims`).
2982
3138
  * 3. Type-alias resolution (when an alias points to an interface).
2983
3139
  * 4. Suffix-fallback heuristic for the workos-node `*Options` convention:
2984
3140
  * when the IR name `X` has no baseline match but `XOptions` does, use
@@ -2989,17 +3145,13 @@ function resolveClassName$5(service, ctx) {
2989
3145
  function resolveInterfaceName(name, ctx, opts) {
2990
3146
  const existing = ctx.overlayLookup?.interfaceByName?.get(name);
2991
3147
  if (existing) return existing;
2992
- let inferred = adoptedModelNames.has(name) || discriminatedModelNames.has(name) ? void 0 : ctx.overlayLookup?.modelNameByIR?.get(name);
2993
- if (inferred) {
2994
- if (inferred.startsWith("Serialized")) {
2995
- const stripped = inferred.slice(10);
2996
- if (stripped && ctx.apiSurface?.interfaces?.[stripped]) inferred = stripped;
2997
- }
2998
- if (inferred.endsWith("Response") && ctx.apiSurface?.interfaces) {
2999
- const stripped = inferred.slice(0, -8);
3000
- if (stripped && ctx.apiSurface.interfaces[stripped]) inferred = stripped;
3148
+ let inferred = inferStructuralRename(name, ctx);
3149
+ if (inferred !== void 0) {
3150
+ if (inferred !== name) {
3151
+ const claimant = getStructuralNameClaims(ctx).get(inferred);
3152
+ if (claimant !== void 0 && claimant !== name) inferred = void 0;
3001
3153
  }
3002
- return inferred;
3154
+ if (inferred !== void 0) return inferred;
3003
3155
  }
3004
3156
  if (!opts?.skipTypeAlias && ctx.apiSurface?.typeAliases) {
3005
3157
  const alias = ctx.apiSurface.typeAliases[name];
@@ -3120,416 +3272,84 @@ function parenthesizeUnion$1(type) {
3120
3272
  return type.includes(" | ") || type.includes(" & ") ? `(${type})` : type;
3121
3273
  }
3122
3274
  //#endregion
3123
- //#region src/node/utils.ts
3124
- /**
3125
- * Compute a relative import path between two files within the generated SDK.
3126
- */
3127
- function relativeImport(fromFile, toFile) {
3128
- const fromDir = fromFile.split("/").slice(0, -1);
3129
- const toFileParts = toFile.split("/");
3130
- const toDir = toFileParts.slice(0, -1);
3131
- const toFileName = toFileParts[toFileParts.length - 1];
3132
- let common = 0;
3133
- while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) common++;
3134
- const ups = fromDir.length - common;
3135
- const downs = toDir.slice(common);
3136
- let result = [
3137
- ...Array(ups).fill(".."),
3138
- ...downs,
3139
- toFileName
3140
- ].join("/");
3141
- result = result.replace(/\.ts$/, "");
3142
- if (!result.startsWith(".")) result = "./" + result;
3143
- return result;
3275
+ //#region src/node/options.ts
3276
+ function nodeOptions(ctx) {
3277
+ return ctx.emitterOptions ?? {};
3144
3278
  }
3145
- /**
3146
- * Render a JSDoc comment block from a description string.
3147
- */
3148
- function docComment$2(description, indent = 0) {
3149
- const pad = " ".repeat(indent);
3150
- const descLines = description.split("\n");
3151
- if (descLines.length === 1) return [`${pad}/** ${descLines[0]} */`];
3152
- const lines = [`${pad}/**`];
3153
- for (const line of descLines) lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
3154
- lines.push(`${pad} */`);
3155
- return lines;
3279
+ function normalizeServiceName(name) {
3280
+ return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
3156
3281
  }
3157
- /**
3158
- * Build a map from model name -> default type args string for generic models.
3159
- */
3160
- function buildGenericModelDefaults(models) {
3161
- const result = /* @__PURE__ */ new Map();
3162
- for (const model of models) {
3163
- if (!model.typeParams?.length) continue;
3164
- const defaults = model.typeParams.map((tp) => tp.default ? mapTypeRef$7(tp.default) : "unknown");
3165
- result.set(model.name, `<${defaults.join(", ")}>`);
3166
- }
3167
- return result;
3282
+ function ownedLookupNames(name) {
3283
+ const names = [name];
3284
+ if (name.startsWith("__baseline_dir__:")) names.push(name.slice(17));
3285
+ return names;
3168
3286
  }
3287
+ function isNodeOwnedService(ctx, ...names) {
3288
+ const configured = nodeOptions(ctx).ownedServices ?? [];
3289
+ if (configured.length === 0) return false;
3290
+ const owned = new Set(configured.map(normalizeServiceName));
3291
+ return names.some((name) => name !== void 0 ? ownedLookupNames(name).some((candidate) => owned.has(normalizeServiceName(candidate))) : false);
3292
+ }
3293
+ //#endregion
3294
+ //#region src/node/live-surface.ts
3295
+ const SRC_DIR = "src";
3169
3296
  /**
3170
- * Remove unused imports from generated source code.
3297
+ * Walk `${rootDir}/src/` and build a live-surface snapshot.
3298
+ *
3299
+ * If `${rootDir}/src/` does not exist, returns an empty surface (greenfield).
3300
+ * Reads each `.ts` file once; ignores `.spec.ts` and `.test.ts`.
3171
3301
  */
3172
- function pruneUnusedImports(lines) {
3173
- const importLines = [];
3174
- const bodyLines = [];
3175
- let inBody = false;
3176
- for (const line of lines) if (!inBody && (line.startsWith("import ") || line === "")) importLines.push(line);
3177
- else {
3178
- inBody = true;
3179
- bodyLines.push(line);
3180
- }
3181
- const body = bodyLines.join("\n");
3182
- const kept = [];
3183
- for (const line of importLines) {
3184
- if (line === "") {
3185
- kept.push(line);
3186
- continue;
3187
- }
3188
- const match = line.match(/\{([^}]+)\}/);
3189
- if (!match) {
3190
- kept.push(line);
3302
+ function buildLiveSurface(rootDir) {
3303
+ const surface = {
3304
+ rootDir,
3305
+ files: /* @__PURE__ */ new Set(),
3306
+ trackedFiles: loadGitTrackedFiles(rootDir),
3307
+ protectedFiles: /* @__PURE__ */ new Set(),
3308
+ autogenFiles: /* @__PURE__ */ new Set(),
3309
+ classes: /* @__PURE__ */ new Map(),
3310
+ interfaces: /* @__PURE__ */ new Map(),
3311
+ functions: /* @__PURE__ */ new Map(),
3312
+ constObjectEnums: /* @__PURE__ */ new Map()
3313
+ };
3314
+ const srcRoot = path$1.join(rootDir, SRC_DIR);
3315
+ if (!fs$1.existsSync(srcRoot) || !fs$1.statSync(srcRoot).isDirectory()) return surface;
3316
+ for (const absPath of walk$1(srcRoot)) {
3317
+ const rel = toPosix(path$1.relative(rootDir, absPath));
3318
+ surface.files.add(rel);
3319
+ if (!rel.endsWith(".ts")) continue;
3320
+ if (surface.trackedFiles.size > 0 && !surface.trackedFiles.has(rel)) continue;
3321
+ let text;
3322
+ try {
3323
+ text = fs$1.readFileSync(absPath, "utf8");
3324
+ } catch {
3191
3325
  continue;
3192
3326
  }
3193
- const names = match[1].split(",").map((n) => n.trim()).filter(Boolean);
3194
- const usedNames = names.filter((name) => {
3195
- return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(body);
3196
- });
3197
- if (usedNames.length === 0) continue;
3198
- if (usedNames.length === names.length) kept.push(line);
3199
- else {
3200
- const isTypeImport = line.startsWith("import type");
3201
- const fromMatch = line.match(/from\s+['"]([^'"]+)['"]/);
3202
- if (fromMatch) {
3203
- const prefix = isTypeImport ? "import type" : "import";
3204
- kept.push(`${prefix} { ${usedNames.join(", ")} } from '${fromMatch[1]}';`);
3205
- } else kept.push(line);
3206
- }
3327
+ if (isProtected(text)) surface.protectedFiles.add(rel);
3328
+ else if (isAutogen(text)) surface.autogenFiles.add(rel);
3329
+ if (rel.endsWith(".spec.ts") || rel.endsWith(".test.ts")) continue;
3330
+ extractDeclarations(text, rel, surface);
3207
3331
  }
3208
- return [...kept, ...bodyLines];
3332
+ return surface;
3209
3333
  }
3210
- /** Built-in TypeScript types that are always available. */
3211
- const TS_BUILTINS = new Set([
3212
- "Record",
3213
- "Promise",
3214
- "Array",
3215
- "Map",
3216
- "Set",
3217
- "Date",
3218
- "string",
3219
- "number",
3220
- "boolean",
3221
- "void",
3222
- "null",
3223
- "undefined",
3224
- "any",
3225
- "never",
3226
- "unknown",
3227
- "true",
3228
- "false"
3229
- ]);
3230
3334
  /**
3231
- * Build a comprehensive set of all known type names from the IR and baseline.
3335
+ * Module-level snapshot of the active live-surface, set by `index.ts` once per
3336
+ * generation run. Used by callers that don't have a clean way to thread
3337
+ * `EmitterContext` through (e.g. deeply-nested helpers in `resources.ts`
3338
+ * that decide whether to inline a serializer call).
3232
3339
  */
3233
- function buildKnownTypeNames(models, ctx) {
3234
- const knownNames = /* @__PURE__ */ new Set();
3235
- for (const m of models) knownNames.add(resolveInterfaceName(m.name, ctx));
3236
- for (const e of ctx.spec.enums) knownNames.add(e.name);
3237
- if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) knownNames.add(name);
3238
- if (ctx.apiSurface?.typeAliases) for (const name of Object.keys(ctx.apiSurface.typeAliases)) knownNames.add(name);
3239
- if (ctx.apiSurface?.enums) for (const name of Object.keys(ctx.apiSurface.enums)) knownNames.add(name);
3240
- return knownNames;
3340
+ let activeSurface = null;
3341
+ function setActiveLiveSurface(surface) {
3342
+ activeSurface = surface;
3241
3343
  }
3242
- /**
3243
- * Create a service directory resolver bundle.
3244
- *
3245
- * When `ctx.apiSurface` is populated, the baseline `sourceFile` of an
3246
- * existing interface wins over the IR-derived first-reference assignment.
3247
- * This keeps generated imports pointing at the existing live SDK location
3248
- * instead of duplicating a model into a different service directory.
3249
- */
3250
- function createServiceDirResolver(models, services, ctx) {
3251
- const modelToService = assignModelsToServices(models, services, ctx.modelHints);
3252
- const serviceNameMap = buildServiceNameMap(services, ctx);
3253
- const baselineDirByModel = /* @__PURE__ */ new Map();
3254
- const recordSource = (name, info) => {
3255
- const sourceFile = info?.sourceFile;
3256
- if (!sourceFile) return;
3257
- const m = sourceFile.match(/^src\/([^/]+)\//);
3258
- if (!m) return;
3259
- baselineDirByModel.set(name, m[1]);
3260
- };
3261
- for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) recordSource(name, info);
3262
- for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) if (!baselineDirByModel.has(name)) recordSource(name, info);
3263
- for (const [modelName] of modelToService) {
3264
- const dir = baselineDirByModel.get(modelName);
3265
- if (!dir) continue;
3266
- const synthetic = `__baseline_dir__:${dir}`;
3267
- modelToService.set(modelName, synthetic);
3268
- if (!serviceNameMap.has(synthetic)) serviceNameMap.set(synthetic, dir);
3269
- }
3270
- const resolveDir = (irService) => {
3271
- if (!irService) return "common";
3272
- if (irService.startsWith("__baseline_dir__:")) return irService.slice(17);
3273
- return resolveServiceDir$1(serviceNameMap.get(irService) ?? irService);
3274
- };
3275
- return {
3276
- modelToService,
3277
- serviceNameMap,
3278
- resolveDir
3279
- };
3344
+ function liveSurfaceHasFunction(name) {
3345
+ return activeSurface?.functions.has(name) ?? false;
3280
3346
  }
3281
- /**
3282
- * Check if baseline interface fields appear to contain generic type parameters.
3283
- *
3284
- * Heuristic: strip string literals first (so `'GoogleSAML'` is not mistaken
3285
- * for a type name), then look for any PascalCase token that isn't a known
3286
- * type — those indicate an unbound generic parameter like `TCustomAttributes`.
3287
- */
3288
- function isBaselineGeneric(fields, knownNames) {
3289
- for (const [, bf] of Object.entries(fields)) {
3290
- const typeNames = bf.type.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").match(/\b[A-Z][a-zA-Z0-9]*\b/g);
3291
- if (!typeNames) continue;
3292
- for (const tn of typeNames) {
3293
- if (TS_BUILTINS.has(tn)) continue;
3294
- if (knownNames.has(tn)) continue;
3295
- return true;
3296
- }
3297
- }
3298
- return false;
3347
+ /** Returns the relative file path containing the function, if known. */
3348
+ function liveSurfaceFunctionPath(name) {
3349
+ return activeSurface?.functions.get(name);
3299
3350
  }
3300
- function modelFingerprint(model) {
3301
- return model.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`).sort().join("|");
3302
- }
3303
- /**
3304
- * Find structurally identical models and build a deduplication map.
3305
- */
3306
- function buildDeduplicationMap(models, ctx, reachable) {
3307
- const dedup = /* @__PURE__ */ new Map();
3308
- const fingerprints = /* @__PURE__ */ new Map();
3309
- for (const model of models) {
3310
- if (model.fields.length === 0) continue;
3311
- const fp = modelFingerprint(model);
3312
- const existing = fingerprints.get(fp);
3313
- if (existing) if (reachable && !reachable.has(existing) && reachable.has(model.name)) {
3314
- dedup.delete(existing);
3315
- dedup.set(existing, model.name);
3316
- fingerprints.set(fp, model.name);
3317
- } else dedup.set(model.name, existing);
3318
- else fingerprints.set(fp, model.name);
3319
- }
3320
- if (ctx) {
3321
- const byDomainName = /* @__PURE__ */ new Map();
3322
- for (const model of models) {
3323
- if (model.fields.length === 0) continue;
3324
- if (dedup.has(model.name)) continue;
3325
- const domainName = resolveInterfaceName(model.name, ctx);
3326
- const group = byDomainName.get(domainName);
3327
- if (group) group.push(model);
3328
- else byDomainName.set(domainName, [model]);
3329
- }
3330
- for (const [, group] of byDomainName) {
3331
- if (group.length < 2) continue;
3332
- group.sort((a, b) => {
3333
- if (reachable) {
3334
- const aReach = reachable.has(a.name) ? 0 : 1;
3335
- const bReach = reachable.has(b.name) ? 0 : 1;
3336
- if (aReach !== bReach) return aReach - bReach;
3337
- }
3338
- return b.fields.length - a.fields.length || a.name.localeCompare(b.name);
3339
- });
3340
- const canonical = group[0];
3341
- for (let i = 1; i < group.length; i++) dedup.set(group[i].name, canonical.name);
3342
- }
3343
- }
3344
- return dedup;
3345
- }
3346
- /**
3347
- * Check whether a service's endpoints are already fully covered by existing
3348
- * hand-written service classes.
3349
- */
3350
- function isServiceCoveredByExisting(service, ctx) {
3351
- if (getMountTarget(service, ctx) !== toPascalCase(service.name)) return true;
3352
- const overlay = ctx.overlayLookup?.methodByOperation;
3353
- if (!overlay || overlay.size === 0) return false;
3354
- if (service.operations.length === 0) return false;
3355
- const baselineClasses = ctx.apiSurface?.classes;
3356
- if (!baselineClasses) return false;
3357
- const existingClassNames = new Set(Object.keys(baselineClasses));
3358
- return service.operations.every((op) => {
3359
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
3360
- const match = overlay.get(httpKey);
3361
- if (!match) return false;
3362
- return existingClassNames.has(match.className);
3363
- });
3364
- }
3365
- /**
3366
- * Check whether a fully-covered service has operations whose overlay-mapped
3367
- * methods are missing from the baseline class.
3368
- */
3369
- function hasMethodsAbsentFromBaseline(service, ctx) {
3370
- const baselineClasses = ctx.apiSurface?.classes;
3371
- if (!baselineClasses) return false;
3372
- const mountTarget = getMountTarget(service, ctx);
3373
- if (mountTarget !== toPascalCase(service.name)) {
3374
- const cls = baselineClasses[mountTarget];
3375
- if (!cls) return true;
3376
- for (const op of service.operations) {
3377
- const method = resolveMethodName$6(op, service, ctx);
3378
- if (!cls.methods?.[method]) return true;
3379
- }
3380
- return false;
3381
- }
3382
- const overlay = ctx.overlayLookup?.methodByOperation;
3383
- if (!overlay) return false;
3384
- for (const op of service.operations) {
3385
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
3386
- const match = overlay.get(httpKey);
3387
- if (!match) continue;
3388
- const cls = baselineClasses[match.className];
3389
- if (!cls) continue;
3390
- if (!cls.methods?.[match.methodName]) return true;
3391
- }
3392
- return false;
3393
- }
3394
- /**
3395
- * Check whether an IR model has fields not present in the baseline interface.
3396
- *
3397
- * When the live SDK exposes the same name as a type alias (e.g.
3398
- * `type Role = EnvironmentRole | OrganizationRole;`), treat it as already
3399
- * fully covered — generating an interface against an existing alias would
3400
- * collide. The alias's referenced types still get generated independently
3401
- * and serve as the canonical implementation.
3402
- */
3403
- function modelHasNewFields(model, ctx) {
3404
- if (!ctx.apiSurface?.interfaces && !ctx.apiSurface?.typeAliases) return true;
3405
- const domainName = resolveInterfaceName(model.name, ctx);
3406
- if (ctx.apiSurface?.typeAliases?.[domainName]) return false;
3407
- const baseline = ctx.apiSurface?.interfaces?.[domainName];
3408
- if (!baseline?.fields) return true;
3409
- for (const field of model.fields) {
3410
- const camelName = fieldName$6(field.name);
3411
- if (!baseline.fields[camelName]) return true;
3412
- }
3413
- return false;
3414
- }
3415
- /**
3416
- * Return operations in a service that are NOT covered by existing hand-written
3417
- * service classes.
3418
- */
3419
- function uncoveredOperations(service, ctx) {
3420
- const overlay = ctx.overlayLookup?.methodByOperation;
3421
- if (!overlay || overlay.size === 0) return service.operations;
3422
- const baselineClasses = ctx.apiSurface?.classes;
3423
- if (!baselineClasses) return service.operations;
3424
- const existingClassNames = new Set(Object.keys(baselineClasses));
3425
- return service.operations.filter((op) => {
3426
- const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
3427
- const match = overlay.get(httpKey);
3428
- if (!match) return true;
3429
- return !existingClassNames.has(match.className);
3430
- });
3431
- }
3432
- /**
3433
- * Compute the set of model names reachable from non-event service operations.
3434
- */
3435
- function computeNonEventReachable(services, models) {
3436
- const seeds = /* @__PURE__ */ new Set();
3437
- for (const svc of services) {
3438
- if (svc.name.toLowerCase() === "events") continue;
3439
- for (const op of svc.operations) {
3440
- const collectFromRef = (t) => {
3441
- if (!t) return;
3442
- if (t.kind === "model") seeds.add(t.name);
3443
- if (t.kind === "array") collectFromRef(t.items);
3444
- if (t.kind === "nullable") collectFromRef(t.inner);
3445
- if (t.kind === "union") t.variants.forEach(collectFromRef);
3446
- };
3447
- collectFromRef(op.response);
3448
- collectFromRef(op.requestBody);
3449
- if (op.pagination?.itemType) collectFromRef(op.pagination.itemType);
3450
- }
3451
- }
3452
- const modelMap = new Map(models.map((m) => [m.name, m]));
3453
- const reachable = /* @__PURE__ */ new Set();
3454
- const queue = [...seeds];
3455
- while (queue.length > 0) {
3456
- const name = queue.pop();
3457
- if (reachable.has(name)) continue;
3458
- reachable.add(name);
3459
- const m = modelMap.get(name);
3460
- if (!m) continue;
3461
- for (const field of m.fields) {
3462
- const walk = (t) => {
3463
- if (t.kind === "model" && !reachable.has(t.name)) queue.push(t.name);
3464
- if (t.kind === "array") walk(t.items);
3465
- if (t.kind === "nullable") walk(t.inner);
3466
- if (t.kind === "union") t.variants.forEach(walk);
3467
- };
3468
- walk(field.type);
3469
- }
3470
- }
3471
- return reachable;
3472
- }
3473
- //#endregion
3474
- //#region src/node/live-surface.ts
3475
- const SRC_DIR = "src";
3476
- /**
3477
- * Walk `${rootDir}/src/` and build a live-surface snapshot.
3478
- *
3479
- * If `${rootDir}/src/` does not exist, returns an empty surface (greenfield).
3480
- * Reads each `.ts` file once; ignores `.spec.ts` and `.test.ts`.
3481
- */
3482
- function buildLiveSurface(rootDir) {
3483
- const surface = {
3484
- rootDir,
3485
- files: /* @__PURE__ */ new Set(),
3486
- trackedFiles: loadGitTrackedFiles(rootDir),
3487
- protectedFiles: /* @__PURE__ */ new Set(),
3488
- autogenFiles: /* @__PURE__ */ new Set(),
3489
- classes: /* @__PURE__ */ new Map(),
3490
- interfaces: /* @__PURE__ */ new Map(),
3491
- functions: /* @__PURE__ */ new Map(),
3492
- constObjectEnums: /* @__PURE__ */ new Map()
3493
- };
3494
- const srcRoot = path$1.join(rootDir, SRC_DIR);
3495
- if (!fs$1.existsSync(srcRoot) || !fs$1.statSync(srcRoot).isDirectory()) return surface;
3496
- for (const absPath of walk$1(srcRoot)) {
3497
- const rel = toPosix(path$1.relative(rootDir, absPath));
3498
- surface.files.add(rel);
3499
- if (!rel.endsWith(".ts")) continue;
3500
- if (surface.trackedFiles.size > 0 && !surface.trackedFiles.has(rel)) continue;
3501
- let text;
3502
- try {
3503
- text = fs$1.readFileSync(absPath, "utf8");
3504
- } catch {
3505
- continue;
3506
- }
3507
- if (isProtected(text)) surface.protectedFiles.add(rel);
3508
- else if (isAutogen(text)) surface.autogenFiles.add(rel);
3509
- if (rel.endsWith(".spec.ts") || rel.endsWith(".test.ts")) continue;
3510
- extractDeclarations(text, rel, surface);
3511
- }
3512
- return surface;
3513
- }
3514
- /**
3515
- * Module-level snapshot of the active live-surface, set by `index.ts` once per
3516
- * generation run. Used by callers that don't have a clean way to thread
3517
- * `EmitterContext` through (e.g. deeply-nested helpers in `resources.ts`
3518
- * that decide whether to inline a serializer call).
3519
- */
3520
- let activeSurface = null;
3521
- function setActiveLiveSurface(surface) {
3522
- activeSurface = surface;
3523
- }
3524
- function liveSurfaceHasFunction(name) {
3525
- return activeSurface?.functions.has(name) ?? false;
3526
- }
3527
- /** Returns the relative file path containing the function, if known. */
3528
- function liveSurfaceFunctionPath(name) {
3529
- return activeSurface?.functions.get(name);
3530
- }
3531
- function liveSurfaceHasFile(relPath) {
3532
- return activeSurface?.files.has(relPath) ?? false;
3351
+ function liveSurfaceHasFile(relPath) {
3352
+ return activeSurface?.files.has(relPath) ?? false;
3533
3353
  }
3534
3354
  function liveSurfaceHasManagedFile(relPath) {
3535
3355
  const surface = activeSurface;
@@ -3659,68 +3479,741 @@ function extractDeclarations(text, relPath, surface) {
3659
3479
  const value = memberMatch[4];
3660
3480
  if (memberName && value !== void 0) members.set(value, memberName);
3661
3481
  }
3662
- if (members.size > 0) surface.constObjectEnums.set(enumName, members);
3482
+ if (members.size > 0) surface.constObjectEnums.set(enumName, members);
3483
+ }
3484
+ }
3485
+ const CONST_OBJECT_ENUM_RE = /^\s*export\s+const\s+([A-Z][\w$]*)\s*=\s*\{([\s\S]*?)\}\s*as\s+const\s*;/gm;
3486
+ const CONST_OBJECT_MEMBER_RE = /(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*:\s*'([^']*)'/g;
3487
+ const METHOD_KEYWORD_BLOCKLIST = new Set([
3488
+ "if",
3489
+ "for",
3490
+ "while",
3491
+ "switch",
3492
+ "catch",
3493
+ "return",
3494
+ "throw",
3495
+ "constructor"
3496
+ ]);
3497
+ const CLASS_MEMBER_RE = /^ {2}(?:(?:public|private|protected|readonly|static)\s+)*(?:async\s+)?([a-zA-Z_$][\w$]*)\s*(?:<[^>]*>)?\s*\(/;
3498
+ /**
3499
+ * Advance a brace-balance scan across one `line`, mutating `s`. Counts only
3500
+ * braces that live in *code* — including template `${ … }` interpolation,
3501
+ * whose braces are self-balancing — and ignores braces inside string/template
3502
+ * text, line comments, and block comments. State threads across lines so block
3503
+ * comments and multi-line template literals are tracked correctly.
3504
+ *
3505
+ * Naive character counting (the prior approach) would clip a method block at a
3506
+ * `}` that merely sat inside a comment (`// returns when done }`) or a string
3507
+ * (`'closing brace: }'`), appending a truncated, unbalanced body. Regex
3508
+ * literals are intentionally not modelled: distinguishing `/` division from a
3509
+ * regex needs full tokenization, and a regex whose braces are net-unbalanced
3510
+ * does not occur in generated or hand-edited SDK code (`{n,m}` quantifiers are
3511
+ * always balanced).
3512
+ */
3513
+ function advanceBraceScan(line, s) {
3514
+ for (let i = 0; i < line.length; i++) {
3515
+ const c = line[i];
3516
+ if (s.inBlockComment) {
3517
+ if (c === "*" && line[i + 1] === "/") {
3518
+ s.inBlockComment = false;
3519
+ i++;
3520
+ }
3521
+ continue;
3522
+ }
3523
+ const tdepth = s.templates.length;
3524
+ if (tdepth > 0 && s.templates[tdepth - 1] === 0) {
3525
+ if (c === "\\") i++;
3526
+ else if (c === "`") s.templates.pop();
3527
+ else if (c === "$" && line[i + 1] === "{") {
3528
+ s.templates[tdepth - 1] = 1;
3529
+ s.depth++;
3530
+ s.opened = true;
3531
+ i++;
3532
+ }
3533
+ continue;
3534
+ }
3535
+ if (c === "/" && line[i + 1] === "/") break;
3536
+ if (c === "/" && line[i + 1] === "*") {
3537
+ s.inBlockComment = true;
3538
+ i++;
3539
+ continue;
3540
+ }
3541
+ if (c === "'" || c === "\"") {
3542
+ const quote = c;
3543
+ i++;
3544
+ while (i < line.length && line[i] !== quote) {
3545
+ if (line[i] === "\\") i++;
3546
+ i++;
3547
+ }
3548
+ continue;
3549
+ }
3550
+ if (c === "`") {
3551
+ s.templates.push(0);
3552
+ continue;
3553
+ }
3554
+ if (c === "{") {
3555
+ s.depth++;
3556
+ s.opened = true;
3557
+ if (tdepth > 0) s.templates[tdepth - 1]++;
3558
+ } else if (c === "}") {
3559
+ s.depth--;
3560
+ if (tdepth > 0) s.templates[tdepth - 1]--;
3561
+ }
3562
+ }
3563
+ }
3564
+ function parseClassesByLine(lines) {
3565
+ const classes = [];
3566
+ for (let i = 0; i < lines.length; i++) {
3567
+ const decl = lines[i].match(/^export\s+(?:abstract\s+)?class\s+([A-Z][\w$]*)/);
3568
+ if (!decl) continue;
3569
+ const scan = {
3570
+ depth: 0,
3571
+ opened: false,
3572
+ inBlockComment: false,
3573
+ templates: []
3574
+ };
3575
+ let closeLine = -1;
3576
+ for (let j = i; j < lines.length; j++) {
3577
+ advanceBraceScan(lines[j], scan);
3578
+ if (scan.opened && scan.depth <= 0) {
3579
+ closeLine = j;
3580
+ break;
3581
+ }
3582
+ }
3583
+ if (closeLine < 0) continue;
3584
+ const methods = [];
3585
+ for (let j = i + 1; j < closeLine; j++) {
3586
+ const member = lines[j].match(CLASS_MEMBER_RE);
3587
+ if (!member) continue;
3588
+ const name = member[1];
3589
+ if (name === "constructor" || METHOD_KEYWORD_BLOCKLIST.has(name)) continue;
3590
+ if (!methods.includes(name)) methods.push(name);
3591
+ }
3592
+ classes.push({
3593
+ name: decl[1],
3594
+ declLine: i,
3595
+ closeLine,
3596
+ methods
3597
+ });
3598
+ i = closeLine;
3599
+ }
3600
+ return classes;
3601
+ }
3602
+ /**
3603
+ * Extract the full source block of `method` (attached comments included) from
3604
+ * a parsed class. Returns null if the block cannot be delimited.
3605
+ */
3606
+ function extractMethodBlock(lines, cls, method) {
3607
+ for (let j = cls.declLine + 1; j < cls.closeLine; j++) {
3608
+ const member = lines[j].match(CLASS_MEMBER_RE);
3609
+ if (!member || member[1] !== method) continue;
3610
+ let start = j;
3611
+ while (start > cls.declLine + 1) {
3612
+ const above = lines[start - 1].trim();
3613
+ if (above.startsWith("//") || above.startsWith("/*") || above.startsWith("*")) start--;
3614
+ else break;
3615
+ }
3616
+ const scan = {
3617
+ depth: 0,
3618
+ opened: false,
3619
+ inBlockComment: false,
3620
+ templates: []
3621
+ };
3622
+ for (let end = j; end < cls.closeLine; end++) {
3623
+ advanceBraceScan(lines[end], scan);
3624
+ if (scan.opened && scan.depth <= 0) return lines.slice(start, end + 1);
3625
+ }
3626
+ return null;
3627
+ }
3628
+ return null;
3629
+ }
3630
+ const IMPORT_STMT_RE = /^import\s+(type\s+)?(?:([A-Za-z_$][\w$]*)\s*,?\s*)?(?:\{([\s\S]*?)\}\s*)?(?:\*\s+as\s+([A-Za-z_$][\w$]*)\s+)?from\s+['"]([^'"]+)['"];?/gm;
3631
+ function importLocalName(entry) {
3632
+ const asMatch = entry.match(/\s+as\s+([A-Za-z_$][\w$]*)\s*$/);
3633
+ if (asMatch) return asMatch[1];
3634
+ return entry.replace(/^type\s+/, "").trim();
3635
+ }
3636
+ function collectImportedLocalNames(text) {
3637
+ const names = /* @__PURE__ */ new Set();
3638
+ for (const m of text.matchAll(IMPORT_STMT_RE)) {
3639
+ if (m[2]) names.add(m[2]);
3640
+ if (m[4]) names.add(m[4]);
3641
+ for (const entry of (m[3] ?? "").split(",")) {
3642
+ const trimmed = entry.trim();
3643
+ if (trimmed) names.add(importLocalName(trimmed));
3644
+ }
3645
+ }
3646
+ return names;
3647
+ }
3648
+ /** Import statements from `generatedText` whose names `existingText` lacks. */
3649
+ function missingImportStatements(existingText, generatedText) {
3650
+ const existingNames = collectImportedLocalNames(existingText);
3651
+ const statements = [];
3652
+ for (const m of generatedText.matchAll(IMPORT_STMT_RE)) {
3653
+ const [stmt, typeOnly, defaultName, named, namespaceName, moduleSpec] = m;
3654
+ if (defaultName || namespaceName) {
3655
+ const local = defaultName ?? namespaceName;
3656
+ if (local && !existingNames.has(local)) statements.push(stmt.endsWith(";") ? stmt : `${stmt};`);
3657
+ continue;
3658
+ }
3659
+ const missing = (named ?? "").split(",").map((entry) => entry.trim()).filter((entry) => entry && !existingNames.has(importLocalName(entry)));
3660
+ if (missing.length === 0) continue;
3661
+ statements.push(`import ${typeOnly ? "type " : ""}{ ${missing.join(", ")} } from '${moduleSpec}';`);
3662
+ }
3663
+ return statements;
3664
+ }
3665
+ /**
3666
+ * Merge a partial class emission into the existing on-disk file content.
3667
+ *
3668
+ * Acts only when a class declared in BOTH texts has methods on disk that the
3669
+ * generated content drops — the signature of a "new operations only" partial
3670
+ * emission. Returns the existing text with the generated-only methods appended
3671
+ * to the class body (and missing imports added), preserving every existing
3672
+ * method and import verbatim.
3673
+ *
3674
+ * Returns null when the generated content does not drop any existing method;
3675
+ * callers should then proceed with their normal (overwrite) path so full
3676
+ * regenerations keep propagating emitter improvements and spec renames.
3677
+ */
3678
+ function mergeGeneratedClassMethodsIntoExisting(existingText, generatedText) {
3679
+ const existingLines = existingText.split("\n");
3680
+ const generatedLines = generatedText.split("\n");
3681
+ const existingClasses = parseClassesByLine(existingLines);
3682
+ if (existingClasses.length === 0) return null;
3683
+ const generatedClasses = new Map(parseClassesByLine(generatedLines).map((cls) => [cls.name, cls]));
3684
+ let dropsExistingMethod = false;
3685
+ const insertions = [];
3686
+ for (const existing of existingClasses) {
3687
+ const generated = generatedClasses.get(existing.name);
3688
+ if (!generated) continue;
3689
+ const generatedMethods = new Set(generated.methods);
3690
+ if (!existing.methods.some((method) => !generatedMethods.has(method))) continue;
3691
+ dropsExistingMethod = true;
3692
+ const existingMethods = new Set(existing.methods);
3693
+ const blocks = [];
3694
+ for (const method of generated.methods) {
3695
+ if (existingMethods.has(method)) continue;
3696
+ const block = extractMethodBlock(generatedLines, generated, method);
3697
+ if (block) blocks.push(block);
3698
+ }
3699
+ if (blocks.length > 0) insertions.push({
3700
+ atLine: existing.closeLine,
3701
+ blocks
3702
+ });
3703
+ }
3704
+ if (!dropsExistingMethod) return null;
3705
+ const merged = [...existingLines];
3706
+ insertions.sort((a, b) => b.atLine - a.atLine);
3707
+ for (const insertion of insertions) {
3708
+ const blockLines = [];
3709
+ for (const block of insertion.blocks) blockLines.push("", ...block);
3710
+ merged.splice(insertion.atLine, 0, ...blockLines);
3711
+ }
3712
+ const importStatements = missingImportStatements(existingText, generatedText);
3713
+ if (importStatements.length > 0) {
3714
+ let lastImportLine = -1;
3715
+ for (let i = 0; i < existingLines.length; i++) if (/from\s+['"][^'"]+['"];?\s*$/.test(existingLines[i]) || /^import\s+['"][^'"]+['"];?\s*$/.test(existingLines[i])) lastImportLine = i;
3716
+ if (lastImportLine >= 0) merged.splice(lastImportLine + 1, 0, ...importStatements);
3717
+ else {
3718
+ let insertAt = 0;
3719
+ while (insertAt < existingLines.length) {
3720
+ const line = existingLines[insertAt].trim();
3721
+ if (line === "" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) insertAt++;
3722
+ else break;
3723
+ }
3724
+ merged.splice(insertAt, 0, ...importStatements, "");
3725
+ }
3726
+ }
3727
+ return merged.join("\n");
3728
+ }
3729
+ /**
3730
+ * Given the start index of a class declaration, return the brace-delimited body
3731
+ * as a substring. Returns '' if the body cannot be located.
3732
+ */
3733
+ function sliceClassBody(text, start) {
3734
+ const open = text.indexOf("{", start);
3735
+ if (open < 0) return "";
3736
+ let depth = 0;
3737
+ for (let i = open; i < text.length; i++) {
3738
+ const c = text[i];
3739
+ if (c === "{") depth++;
3740
+ else if (c === "}") {
3741
+ depth--;
3742
+ if (depth === 0) return text.slice(open + 1, i);
3743
+ }
3744
+ }
3745
+ return "";
3746
+ }
3747
+ /**
3748
+ * Pull field names from an interface body. Conservative: only matches simple
3749
+ * declarations of the form `name?: ...;` or `name: ...;`. Generics, methods,
3750
+ * and computed property names are skipped (they don't influence skip decisions).
3751
+ */
3752
+ function extractInterfaceFields(text, start) {
3753
+ const fields = /* @__PURE__ */ new Set();
3754
+ const open = text.indexOf("{", start);
3755
+ if (open < 0) return fields;
3756
+ let depth = 0;
3757
+ let close = -1;
3758
+ for (let i = open; i < text.length; i++) {
3759
+ const c = text[i];
3760
+ if (c === "{") depth++;
3761
+ else if (c === "}") {
3762
+ depth--;
3763
+ if (depth === 0) {
3764
+ close = i;
3765
+ break;
3766
+ }
3767
+ }
3768
+ }
3769
+ if (close < 0) return fields;
3770
+ const body = text.slice(open + 1, close);
3771
+ for (const m of body.matchAll(/^\s*(?:readonly\s+)?(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*\??\s*:/gm)) {
3772
+ const name = m[1] ?? m[2] ?? m[3];
3773
+ if (name) fields.add(name);
3774
+ }
3775
+ return fields;
3776
+ }
3777
+ //#endregion
3778
+ //#region src/node/utils.ts
3779
+ /**
3780
+ * Compute a relative import path between two files within the generated SDK.
3781
+ */
3782
+ function relativeImport(fromFile, toFile) {
3783
+ const fromDir = fromFile.split("/").slice(0, -1);
3784
+ const toFileParts = toFile.split("/");
3785
+ const toDir = toFileParts.slice(0, -1);
3786
+ const toFileName = toFileParts[toFileParts.length - 1];
3787
+ let common = 0;
3788
+ while (common < fromDir.length && common < toDir.length && fromDir[common] === toDir[common]) common++;
3789
+ const ups = fromDir.length - common;
3790
+ const downs = toDir.slice(common);
3791
+ let result = [
3792
+ ...Array(ups).fill(".."),
3793
+ ...downs,
3794
+ toFileName
3795
+ ].join("/");
3796
+ result = result.replace(/\.ts$/, "");
3797
+ if (!result.startsWith(".")) result = "./" + result;
3798
+ return result;
3799
+ }
3800
+ /**
3801
+ * Render a JSDoc comment block from a description string.
3802
+ */
3803
+ function docComment$2(description, indent = 0) {
3804
+ const pad = " ".repeat(indent);
3805
+ const descLines = description.split("\n");
3806
+ if (descLines.length === 1) return [`${pad}/** ${descLines[0]} */`];
3807
+ const lines = [`${pad}/**`];
3808
+ for (const line of descLines) lines.push(line === "" ? `${pad} *` : `${pad} * ${line}`);
3809
+ lines.push(`${pad} */`);
3810
+ return lines;
3811
+ }
3812
+ /**
3813
+ * Build a map from model name -> default type args string for generic models.
3814
+ */
3815
+ function buildGenericModelDefaults(models) {
3816
+ const result = /* @__PURE__ */ new Map();
3817
+ for (const model of models) {
3818
+ if (!model.typeParams?.length) continue;
3819
+ const defaults = model.typeParams.map((tp) => tp.default ? mapTypeRef$7(tp.default) : "unknown");
3820
+ result.set(model.name, `<${defaults.join(", ")}>`);
3821
+ }
3822
+ return result;
3823
+ }
3824
+ /**
3825
+ * Remove unused imports from generated source code.
3826
+ */
3827
+ function pruneUnusedImports(lines) {
3828
+ const importLines = [];
3829
+ const bodyLines = [];
3830
+ let inBody = false;
3831
+ for (const line of lines) if (!inBody && (line.startsWith("import ") || line === "")) importLines.push(line);
3832
+ else {
3833
+ inBody = true;
3834
+ bodyLines.push(line);
3835
+ }
3836
+ const body = bodyLines.join("\n");
3837
+ const kept = [];
3838
+ for (const line of importLines) {
3839
+ if (line === "") {
3840
+ kept.push(line);
3841
+ continue;
3842
+ }
3843
+ const match = line.match(/\{([^}]+)\}/);
3844
+ if (!match) {
3845
+ kept.push(line);
3846
+ continue;
3847
+ }
3848
+ const names = match[1].split(",").map((n) => n.trim()).filter(Boolean);
3849
+ const usedNames = names.filter((name) => {
3850
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(body);
3851
+ });
3852
+ if (usedNames.length === 0) continue;
3853
+ if (usedNames.length === names.length) kept.push(line);
3854
+ else {
3855
+ const isTypeImport = line.startsWith("import type");
3856
+ const fromMatch = line.match(/from\s+['"]([^'"]+)['"]/);
3857
+ if (fromMatch) {
3858
+ const prefix = isTypeImport ? "import type" : "import";
3859
+ kept.push(`${prefix} { ${usedNames.join(", ")} } from '${fromMatch[1]}';`);
3860
+ } else kept.push(line);
3861
+ }
3862
+ }
3863
+ return [...kept, ...bodyLines];
3864
+ }
3865
+ /** Built-in TypeScript types that are always available. */
3866
+ const TS_BUILTINS = new Set([
3867
+ "Record",
3868
+ "Promise",
3869
+ "Array",
3870
+ "Map",
3871
+ "Set",
3872
+ "Date",
3873
+ "string",
3874
+ "number",
3875
+ "boolean",
3876
+ "void",
3877
+ "null",
3878
+ "undefined",
3879
+ "any",
3880
+ "never",
3881
+ "unknown",
3882
+ "true",
3883
+ "false"
3884
+ ]);
3885
+ /**
3886
+ * Build a comprehensive set of all known type names from the IR and baseline.
3887
+ */
3888
+ function buildKnownTypeNames(models, ctx) {
3889
+ const knownNames = /* @__PURE__ */ new Set();
3890
+ for (const m of models) knownNames.add(resolveInterfaceName(m.name, ctx));
3891
+ for (const e of ctx.spec.enums) knownNames.add(e.name);
3892
+ if (ctx.apiSurface?.interfaces) for (const name of Object.keys(ctx.apiSurface.interfaces)) knownNames.add(name);
3893
+ if (ctx.apiSurface?.typeAliases) for (const name of Object.keys(ctx.apiSurface.typeAliases)) knownNames.add(name);
3894
+ if (ctx.apiSurface?.enums) for (const name of Object.keys(ctx.apiSurface.enums)) knownNames.add(name);
3895
+ return knownNames;
3896
+ }
3897
+ /**
3898
+ * Create a service directory resolver bundle.
3899
+ *
3900
+ * When `ctx.apiSurface` is populated, the baseline `sourceFile` of an
3901
+ * existing interface wins over the IR-derived first-reference assignment.
3902
+ * This keeps generated imports pointing at the existing live SDK location
3903
+ * instead of duplicating a model into a different service directory.
3904
+ */
3905
+ function createServiceDirResolver(models, services, ctx) {
3906
+ const modelToService = assignModelsToEmittableServices(models, services, ctx);
3907
+ const serviceNameMap = buildServiceNameMap(services, ctx);
3908
+ const baselineDirByModel = harvestBaselineDirByModel(ctx);
3909
+ for (const [modelName] of modelToService) {
3910
+ const dir = baselineDirByModel.get(modelName);
3911
+ if (!dir) continue;
3912
+ const synthetic = `__baseline_dir__:${dir}`;
3913
+ modelToService.set(modelName, synthetic);
3914
+ if (!serviceNameMap.has(synthetic)) serviceNameMap.set(synthetic, dir);
3915
+ }
3916
+ const resolveDir = (irService) => {
3917
+ if (!irService) return "common";
3918
+ if (irService.startsWith("__baseline_dir__:")) return irService.slice(17);
3919
+ return resolveServiceDir$1(serviceNameMap.get(irService) ?? irService);
3920
+ };
3921
+ return {
3922
+ modelToService,
3923
+ serviceNameMap,
3924
+ resolveDir
3925
+ };
3926
+ }
3927
+ /**
3928
+ * Map baseline interface / type-alias names to the top-level `src/<dir>/`
3929
+ * their `sourceFile` lives in. Both kinds can shadow IR model names — e.g.
3930
+ * `type Role = EnvironmentRole | OrganizationRole;` is the live SDK's
3931
+ * canonical Role definition even though the IR represents Role as a model.
3932
+ */
3933
+ function harvestBaselineDirByModel(ctx) {
3934
+ const baselineDirByModel = /* @__PURE__ */ new Map();
3935
+ const recordSource = (name, info) => {
3936
+ const sourceFile = info?.sourceFile;
3937
+ if (!sourceFile) return;
3938
+ const m = sourceFile.match(/^src\/([^/]+)\//);
3939
+ if (!m) return;
3940
+ baselineDirByModel.set(name, m[1]);
3941
+ };
3942
+ for (const [name, info] of Object.entries(ctx.apiSurface?.interfaces ?? {})) recordSource(name, info);
3943
+ for (const [name, info] of Object.entries(ctx.apiSurface?.typeAliases ?? {})) if (!baselineDirByModel.has(name)) recordSource(name, info);
3944
+ return baselineDirByModel;
3945
+ }
3946
+ /**
3947
+ * `assignModelsToServices` plus an owned-service correction pass.
3948
+ *
3949
+ * The engine's assignment is first-reference-wins: a model referenced by both
3950
+ * Organizations and AuditLogs lands in `organizations/` even when only
3951
+ * AuditLogs is owned this run. Against an existing SDK, `applyLiveSurface`
3952
+ * then drops the model file (a non-owned, non-adopted directory cannot
3953
+ * receive new paths) while the owned resource still imports
3954
+ * `../organizations/interfaces/<model>.interface` — an import that resolves
3955
+ * to nothing (TS2307).
3956
+ *
3957
+ * The correction re-homes such models to the owned service that references
3958
+ * them, so emission and import planning agree on a directory that is allowed
3959
+ * to receive files. It only fires when:
3960
+ * 1. `ownedServices` is configured and the run targets an existing SDK;
3961
+ * 2. the model has no baseline `sourceFile` (otherwise the baseline-dir
3962
+ * override keeps imports pointing at the on-disk location);
3963
+ * 3. the model's computed interface path does not already exist on disk;
3964
+ * 4. the assigned service is neither owned itself nor sharing a directory
3965
+ * with an owned service.
3966
+ */
3967
+ function assignModelsToEmittableServices(models, services, ctx) {
3968
+ const modelToService = assignModelsToServices(models, services, ctx?.modelHints);
3969
+ if (ctx) reassignOwnedServiceDependencies(modelToService, models, services, ctx);
3970
+ return modelToService;
3971
+ }
3972
+ function reassignOwnedServiceDependencies(modelToService, models, services, ctx) {
3973
+ const serviceNameMap = buildServiceNameMap(services, ctx);
3974
+ const mountGroups = groupByMount(ctx);
3975
+ const ownedServices = (mountGroups.size > 0 ? [...mountGroups].map(([name, group]) => ({
3976
+ name,
3977
+ operations: group.operations
3978
+ })) : services).filter((s) => isNodeOwnedService(ctx, s.name, serviceNameMap.get(s.name)));
3979
+ if (ownedServices.length === 0) return;
3980
+ if (!liveSurfaceHasExistingSdk()) return;
3981
+ const dirOf = (irService) => irService ? resolveServiceDir$1(serviceNameMap.get(irService) ?? irService) : "common";
3982
+ const ownedDirs = new Set(ownedServices.map((s) => dirOf(s.name)));
3983
+ const baselineDirByModel = harvestBaselineDirByModel(ctx);
3984
+ const modelsByName = new Map(models.map((m) => [m.name, m]));
3985
+ for (const service of ownedServices) for (const name of collectServiceModelClosure(service, modelsByName)) {
3986
+ if (!modelsByName.has(name)) continue;
3987
+ if (baselineDirByModel.has(name)) continue;
3988
+ const assigned = modelToService.get(name);
3989
+ if (assigned && isNodeOwnedService(ctx, assigned, serviceNameMap.get(assigned))) continue;
3990
+ const assignedDir = dirOf(assigned);
3991
+ if (ownedDirs.has(assignedDir)) continue;
3992
+ if (liveSurfaceHasFile(`src/${assignedDir}/interfaces/${fileName$3(name)}.interface.ts`)) continue;
3993
+ modelToService.set(name, service.name);
3994
+ }
3995
+ }
3996
+ /** Model names referenced by a service's operations, expanded through fields. */
3997
+ function collectServiceModelClosure(service, modelsByName) {
3998
+ const referenced = /* @__PURE__ */ new Set();
3999
+ const add = (ref) => {
4000
+ if (!ref) return;
4001
+ for (const name of collectModelRefs(ref)) referenced.add(name);
4002
+ };
4003
+ for (const op of service.operations) {
4004
+ add(op.requestBody);
4005
+ add(op.response);
4006
+ for (const param of [
4007
+ ...op.pathParams,
4008
+ ...op.queryParams,
4009
+ ...op.headerParams,
4010
+ ...op.cookieParams ?? []
4011
+ ]) add(param.type);
4012
+ if (op.pagination) add(op.pagination.itemType);
4013
+ }
4014
+ const queue = [...referenced];
4015
+ while (queue.length > 0) {
4016
+ const name = queue.pop();
4017
+ const model = modelsByName.get(name);
4018
+ if (!model) continue;
4019
+ for (const dep of collectFieldDependencies(model).models) if (!referenced.has(dep)) {
4020
+ referenced.add(dep);
4021
+ queue.push(dep);
4022
+ }
4023
+ }
4024
+ return referenced;
4025
+ }
4026
+ /**
4027
+ * Check if baseline interface fields appear to contain generic type parameters.
4028
+ *
4029
+ * Heuristic: strip string literals first (so `'GoogleSAML'` is not mistaken
4030
+ * for a type name), then look for any PascalCase token that isn't a known
4031
+ * type — those indicate an unbound generic parameter like `TCustomAttributes`.
4032
+ */
4033
+ function isBaselineGeneric(fields, knownNames) {
4034
+ for (const [, bf] of Object.entries(fields)) {
4035
+ const typeNames = bf.type.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").match(/\b[A-Z][a-zA-Z0-9]*\b/g);
4036
+ if (!typeNames) continue;
4037
+ for (const tn of typeNames) {
4038
+ if (TS_BUILTINS.has(tn)) continue;
4039
+ if (knownNames.has(tn)) continue;
4040
+ return true;
4041
+ }
4042
+ }
4043
+ return false;
4044
+ }
4045
+ function modelFingerprint(model) {
4046
+ return model.fields.map((f) => `${f.name}:${JSON.stringify(f.type)}:${f.required}`).sort().join("|");
4047
+ }
4048
+ /**
4049
+ * Find structurally identical models and build a deduplication map.
4050
+ */
4051
+ function buildDeduplicationMap(models, ctx, reachable) {
4052
+ const dedup = /* @__PURE__ */ new Map();
4053
+ const fingerprints = /* @__PURE__ */ new Map();
4054
+ for (const model of models) {
4055
+ if (model.fields.length === 0) continue;
4056
+ const fp = modelFingerprint(model);
4057
+ const existing = fingerprints.get(fp);
4058
+ if (existing) if (reachable && !reachable.has(existing) && reachable.has(model.name)) {
4059
+ dedup.delete(existing);
4060
+ dedup.set(existing, model.name);
4061
+ fingerprints.set(fp, model.name);
4062
+ } else dedup.set(model.name, existing);
4063
+ else fingerprints.set(fp, model.name);
4064
+ }
4065
+ if (ctx) {
4066
+ const byDomainName = /* @__PURE__ */ new Map();
4067
+ for (const model of models) {
4068
+ if (model.fields.length === 0) continue;
4069
+ if (dedup.has(model.name)) continue;
4070
+ const domainName = resolveInterfaceName(model.name, ctx);
4071
+ const group = byDomainName.get(domainName);
4072
+ if (group) group.push(model);
4073
+ else byDomainName.set(domainName, [model]);
4074
+ }
4075
+ for (const [, group] of byDomainName) {
4076
+ if (group.length < 2) continue;
4077
+ group.sort((a, b) => {
4078
+ if (reachable) {
4079
+ const aReach = reachable.has(a.name) ? 0 : 1;
4080
+ const bReach = reachable.has(b.name) ? 0 : 1;
4081
+ if (aReach !== bReach) return aReach - bReach;
4082
+ }
4083
+ return b.fields.length - a.fields.length || a.name.localeCompare(b.name);
4084
+ });
4085
+ const canonical = group[0];
4086
+ for (let i = 1; i < group.length; i++) dedup.set(group[i].name, canonical.name);
4087
+ }
3663
4088
  }
4089
+ return dedup;
3664
4090
  }
3665
- const CONST_OBJECT_ENUM_RE = /^\s*export\s+const\s+([A-Z][\w$]*)\s*=\s*\{([\s\S]*?)\}\s*as\s+const\s*;/gm;
3666
- const CONST_OBJECT_MEMBER_RE = /(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*:\s*'([^']*)'/g;
3667
- const METHOD_KEYWORD_BLOCKLIST = new Set([
3668
- "if",
3669
- "for",
3670
- "while",
3671
- "switch",
3672
- "catch",
3673
- "return",
3674
- "throw",
3675
- "constructor"
3676
- ]);
3677
4091
  /**
3678
- * Given the start index of a class declaration, return the brace-delimited body
3679
- * as a substring. Returns '' if the body cannot be located.
4092
+ * Check whether a service's endpoints are already fully covered by existing
4093
+ * hand-written service classes.
3680
4094
  */
3681
- function sliceClassBody(text, start) {
3682
- const open = text.indexOf("{", start);
3683
- if (open < 0) return "";
3684
- let depth = 0;
3685
- for (let i = open; i < text.length; i++) {
3686
- const c = text[i];
3687
- if (c === "{") depth++;
3688
- else if (c === "}") {
3689
- depth--;
3690
- if (depth === 0) return text.slice(open + 1, i);
4095
+ function isServiceCoveredByExisting(service, ctx) {
4096
+ if (getMountTarget(service, ctx) !== toPascalCase(service.name)) return true;
4097
+ const overlay = ctx.overlayLookup?.methodByOperation;
4098
+ if (!overlay || overlay.size === 0) return false;
4099
+ if (service.operations.length === 0) return false;
4100
+ const baselineClasses = ctx.apiSurface?.classes;
4101
+ if (!baselineClasses) return false;
4102
+ const existingClassNames = new Set(Object.keys(baselineClasses));
4103
+ return service.operations.every((op) => {
4104
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
4105
+ const match = overlay.get(httpKey);
4106
+ if (!match) return false;
4107
+ return existingClassNames.has(match.className);
4108
+ });
4109
+ }
4110
+ /**
4111
+ * Check whether a fully-covered service has operations whose overlay-mapped
4112
+ * methods are missing from the baseline class.
4113
+ */
4114
+ function hasMethodsAbsentFromBaseline(service, ctx) {
4115
+ const baselineClasses = ctx.apiSurface?.classes;
4116
+ if (!baselineClasses) return false;
4117
+ const mountTarget = getMountTarget(service, ctx);
4118
+ if (mountTarget !== toPascalCase(service.name)) {
4119
+ const cls = baselineClasses[mountTarget];
4120
+ if (!cls) return true;
4121
+ for (const op of service.operations) {
4122
+ const method = resolveMethodName$6(op, service, ctx);
4123
+ if (!cls.methods?.[method]) return true;
3691
4124
  }
4125
+ return false;
3692
4126
  }
3693
- return "";
4127
+ const overlay = ctx.overlayLookup?.methodByOperation;
4128
+ if (!overlay) return false;
4129
+ for (const op of service.operations) {
4130
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
4131
+ const match = overlay.get(httpKey);
4132
+ if (!match) continue;
4133
+ const cls = baselineClasses[match.className];
4134
+ if (!cls) continue;
4135
+ if (!cls.methods?.[match.methodName]) return true;
4136
+ }
4137
+ return false;
3694
4138
  }
3695
4139
  /**
3696
- * Pull field names from an interface body. Conservative: only matches simple
3697
- * declarations of the form `name?: ...;` or `name: ...;`. Generics, methods,
3698
- * and computed property names are skipped (they don't influence skip decisions).
4140
+ * Check whether an IR model has fields not present in the baseline interface.
4141
+ *
4142
+ * When the live SDK exposes the same name as a type alias (e.g.
4143
+ * `type Role = EnvironmentRole | OrganizationRole;`), treat it as already
4144
+ * fully covered — generating an interface against an existing alias would
4145
+ * collide. The alias's referenced types still get generated independently
4146
+ * and serve as the canonical implementation.
3699
4147
  */
3700
- function extractInterfaceFields(text, start) {
3701
- const fields = /* @__PURE__ */ new Set();
3702
- const open = text.indexOf("{", start);
3703
- if (open < 0) return fields;
3704
- let depth = 0;
3705
- let close = -1;
3706
- for (let i = open; i < text.length; i++) {
3707
- const c = text[i];
3708
- if (c === "{") depth++;
3709
- else if (c === "}") {
3710
- depth--;
3711
- if (depth === 0) {
3712
- close = i;
3713
- break;
3714
- }
4148
+ function modelHasNewFields(model, ctx) {
4149
+ if (!ctx.apiSurface?.interfaces && !ctx.apiSurface?.typeAliases) return true;
4150
+ const domainName = resolveInterfaceName(model.name, ctx);
4151
+ if (ctx.apiSurface?.typeAliases?.[domainName]) return false;
4152
+ const baseline = ctx.apiSurface?.interfaces?.[domainName];
4153
+ if (!baseline?.fields) return true;
4154
+ for (const field of model.fields) {
4155
+ const camelName = fieldName$6(field.name);
4156
+ if (!baseline.fields[camelName]) return true;
4157
+ }
4158
+ return false;
4159
+ }
4160
+ /**
4161
+ * Return operations in a service that are NOT covered by existing hand-written
4162
+ * service classes.
4163
+ */
4164
+ function uncoveredOperations(service, ctx) {
4165
+ const overlay = ctx.overlayLookup?.methodByOperation;
4166
+ if (!overlay || overlay.size === 0) return service.operations;
4167
+ const baselineClasses = ctx.apiSurface?.classes;
4168
+ if (!baselineClasses) return service.operations;
4169
+ const existingClassNames = new Set(Object.keys(baselineClasses));
4170
+ return service.operations.filter((op) => {
4171
+ const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
4172
+ const match = overlay.get(httpKey);
4173
+ if (!match) return true;
4174
+ return !existingClassNames.has(match.className);
4175
+ });
4176
+ }
4177
+ /**
4178
+ * Compute the set of model names reachable from non-event service operations.
4179
+ */
4180
+ function computeNonEventReachable(services, models) {
4181
+ const seeds = /* @__PURE__ */ new Set();
4182
+ for (const svc of services) {
4183
+ if (svc.name.toLowerCase() === "events") continue;
4184
+ for (const op of svc.operations) {
4185
+ const collectFromRef = (t) => {
4186
+ if (!t) return;
4187
+ if (t.kind === "model") seeds.add(t.name);
4188
+ if (t.kind === "array") collectFromRef(t.items);
4189
+ if (t.kind === "nullable") collectFromRef(t.inner);
4190
+ if (t.kind === "union") t.variants.forEach(collectFromRef);
4191
+ };
4192
+ collectFromRef(op.response);
4193
+ collectFromRef(op.requestBody);
4194
+ if (op.pagination?.itemType) collectFromRef(op.pagination.itemType);
3715
4195
  }
3716
4196
  }
3717
- if (close < 0) return fields;
3718
- const body = text.slice(open + 1, close);
3719
- for (const m of body.matchAll(/^\s*(?:readonly\s+)?(?:'([^']+)'|"([^"]+)"|([a-zA-Z_$][\w$]*))\s*\??\s*:/gm)) {
3720
- const name = m[1] ?? m[2] ?? m[3];
3721
- if (name) fields.add(name);
4197
+ const modelMap = new Map(models.map((m) => [m.name, m]));
4198
+ const reachable = /* @__PURE__ */ new Set();
4199
+ const queue = [...seeds];
4200
+ while (queue.length > 0) {
4201
+ const name = queue.pop();
4202
+ if (reachable.has(name)) continue;
4203
+ reachable.add(name);
4204
+ const m = modelMap.get(name);
4205
+ if (!m) continue;
4206
+ for (const field of m.fields) {
4207
+ const walk = (t) => {
4208
+ if (t.kind === "model" && !reachable.has(t.name)) queue.push(t.name);
4209
+ if (t.kind === "array") walk(t.items);
4210
+ if (t.kind === "nullable") walk(t.inner);
4211
+ if (t.kind === "union") t.variants.forEach(walk);
4212
+ };
4213
+ walk(field.type);
4214
+ }
3722
4215
  }
3723
- return fields;
4216
+ return reachable;
3724
4217
  }
3725
4218
  //#endregion
3726
4219
  //#region src/node/enums.ts
@@ -3732,13 +4225,15 @@ function generateEnums$7(enums, ctx) {
3732
4225
  const files = [];
3733
4226
  for (const enumDef of enums) {
3734
4227
  if (isInlineEnum(enumDef.name)) continue;
3735
- const dirName = resolveDir(enumToService.get(enumDef.name));
4228
+ const service = enumToService.get(enumDef.name);
4229
+ const dirName = resolveDir(service);
3736
4230
  const baselineEnum = ctx.apiSurface?.enums?.[enumDef.name];
3737
4231
  const baselineAlias = ctx.apiSurface?.typeAliases?.[enumDef.name];
3738
4232
  const generatedPath = `src/${dirName}/interfaces/${fileName$3(enumDef.name)}.interface.ts`;
3739
4233
  const baselineSourceFile = baselineEnum?.sourceFile ?? baselineAlias?.sourceFile ?? liveSurfaceInterfacePath(enumDef.name);
3740
4234
  if (dirName === "common" && !baselineSourceFile && (ctx.outputDir || ctx.targetDir || ctx.apiSurface)) continue;
3741
- if (baselineSourceFile && baselineSourceFile !== generatedPath) continue;
4235
+ const isOwnedEnumService = isNodeOwnedService(ctx, service, service ? serviceNameMap.get(service) : void 0);
4236
+ if (baselineSourceFile && baselineSourceFile !== generatedPath && !isOwnedEnumService) continue;
3742
4237
  const lines = [];
3743
4238
  let hasNewValues = false;
3744
4239
  if (baselineEnum?.members) {
@@ -3824,7 +4319,7 @@ function assignEnumsToServices(enums, services, models = [], ctx) {
3824
4319
  for (const name of refs) if (enumNames.has(name) && !enumToService.has(name)) enumToService.set(name, service.name);
3825
4320
  }
3826
4321
  if (models.length > 0) {
3827
- const modelToService = assignModelsToServices(models, services, ctx?.modelHints);
4322
+ const modelToService = assignModelsToEmittableServices(models, services, ctx);
3828
4323
  for (const model of models) {
3829
4324
  const service = modelToService.get(model.name);
3830
4325
  if (!service) continue;
@@ -3834,25 +4329,6 @@ function assignEnumsToServices(enums, services, models = [], ctx) {
3834
4329
  return enumToService;
3835
4330
  }
3836
4331
  //#endregion
3837
- //#region src/node/options.ts
3838
- function nodeOptions(ctx) {
3839
- return ctx.emitterOptions ?? {};
3840
- }
3841
- function normalizeServiceName(name) {
3842
- return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
3843
- }
3844
- function ownedLookupNames(name) {
3845
- const names = [name];
3846
- if (name.startsWith("__baseline_dir__:")) names.push(name.slice(17));
3847
- return names;
3848
- }
3849
- function isNodeOwnedService(ctx, ...names) {
3850
- const configured = nodeOptions(ctx).ownedServices ?? [];
3851
- if (configured.length === 0) return false;
3852
- const owned = new Set(configured.map(normalizeServiceName));
3853
- return names.some((name) => name !== void 0 ? ownedLookupNames(name).some((candidate) => owned.has(normalizeServiceName(candidate))) : false);
3854
- }
3855
- //#endregion
3856
4332
  //#region src/node/field-plan.ts
3857
4333
  /**
3858
4334
  * Decide whether a `deserialize${X}` / `serialize${X}` helper will be
@@ -4718,7 +5194,7 @@ function ignoredResourceMethodNames(ctx, resourcePath) {
4718
5194
  }
4719
5195
  const methods = /* @__PURE__ */ new Set();
4720
5196
  for (const block of content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)) for (const line of block[0].split("\n")) {
4721
- const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/);
5197
+ const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*[<(]/);
4722
5198
  if (match) methods.add(match[1]);
4723
5199
  }
4724
5200
  return methods;
@@ -4780,6 +5256,17 @@ function optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resol
4780
5256
  function renderOptionsParam(param) {
4781
5257
  return `options${param.optional ? "?" : ""}: ${param.type}`;
4782
5258
  }
5259
+ /**
5260
+ * Whether a baseline-derived type reference is a plain TypeScript identifier
5261
+ * that can appear in a named import. Baseline (live-SDK) method params can
5262
+ * carry inline object-literal TYPES (e.g. `{ intent: GenerateLinkIntent; ... }`);
5263
+ * treating that literal text as a type NAME would slugify it into a filename
5264
+ * and emit a named import of a brace-expression — both invalid. Literal types
5265
+ * are kept inline in the emitted signature instead and never imported.
5266
+ */
5267
+ function isValidTypeIdentifier(name) {
5268
+ return /^[A-Za-z_$][\w$]*$/.test(name);
5269
+ }
4783
5270
  function autoPaginatableItemType$1(returnType) {
4784
5271
  return returnType?.match(/\b(?:AutoPaginatable|List)<\s*([A-Za-z_$][\w$]*)/)?.[1];
4785
5272
  }
@@ -5108,6 +5595,7 @@ function generateResourceClass(service, ctx) {
5108
5595
  if (hasIdempotentPost || hasCustomEncoding) lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
5109
5596
  const importedTypeNames = /* @__PURE__ */ new Set();
5110
5597
  for (const optionType of optionObjectTypes) {
5598
+ if (!isValidTypeIdentifier(optionType)) continue;
5111
5599
  if (importedTypeNames.has(optionType)) continue;
5112
5600
  importedTypeNames.add(optionType);
5113
5601
  const sourceFile = baselineTypeSourceFile(ctx, optionType);
@@ -5546,7 +6034,8 @@ function renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelM
5546
6034
  if (!itemType) return false;
5547
6035
  const wireType = wireInterfaceName(itemType);
5548
6036
  const needsWireSerializer = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name)).some((p) => fieldName$6(p.name) !== wireFieldName(p.name));
5549
- const paginationType = needsWireSerializer ? "PaginationOptions" : optionParam.type;
6037
+ const restOptionsType = pathBindings.length > 0 ? `Omit<${optionParam.type}, ${pathBindings.map((b) => `'${b}'`).join(" | ")}>` : optionParam.type;
6038
+ const paginationType = needsWireSerializer ? "PaginationOptions" : restOptionsType;
5550
6039
  const returnType = needsWireSerializer ? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>` : preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}, ${paginationType}>>`;
5551
6040
  const listOptionsExpr = needsWireSerializer ? `options ? serialize${optionParam.type}(options) : undefined` : "paginationOptions";
5552
6041
  lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
@@ -5641,7 +6130,8 @@ function renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelM
5641
6130
  const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
5642
6131
  lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
5643
6132
  renderOptionsObjectDestructure(lines, pathBindings);
5644
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${queryOptionsArg});`);
6133
+ const emptyBodyArg = httpMethodNeedsBody(op.httpMethod) ? ", {}" : "";
6134
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${emptyBodyArg}${queryOptionsArg});`);
5645
6135
  if (override?.returnExpression) {
5646
6136
  lines.push(` const result = ${returnExpr};`);
5647
6137
  lines.push(` return ${override.returnExpression};`);
@@ -6509,10 +6999,11 @@ function generateModels$7(models, ctx, shared) {
6509
6999
  if (unresolvableNames.has(name)) continue;
6510
7000
  const irEnumName = resolvedEnumNames.get(name);
6511
7001
  if (irEnumName && !deps.enums.has(irEnumName)) {
6512
- const eDir = resolveDir(enumToService.get(irEnumName));
7002
+ const eService = enumToService.get(irEnumName);
7003
+ const eDir = resolveDir(eService);
6513
7004
  const bEnum = ctx.apiSurface?.enums?.[irEnumName];
6514
7005
  const bAlias = ctx.apiSurface?.typeAliases?.[irEnumName];
6515
- const bSrc = bEnum?.sourceFile ?? bAlias?.sourceFile;
7006
+ const bSrc = isNodeOwnedService(ctx, eService) ? void 0 : bEnum?.sourceFile ?? bAlias?.sourceFile;
6516
7007
  const gPath = `src/${eDir}/interfaces/${fileName$3(irEnumName)}.interface.ts`;
6517
7008
  const cPath = `src/${dirName}/interfaces/${fileName$3(model.name)}.interface.ts`;
6518
7009
  if (bSrc === cPath) {
@@ -6556,8 +7047,9 @@ function generateModels$7(models, ctx, shared) {
6556
7047
  if (isInlineEnum(dep)) continue;
6557
7048
  const baselineEnum = ctx.apiSurface?.enums?.[dep];
6558
7049
  const baselineAlias = ctx.apiSurface?.typeAliases?.[dep];
6559
- const baselineSrc = baselineEnum?.sourceFile ?? baselineAlias?.sourceFile;
6560
- const depDir = resolveDir(enumToService.get(dep));
7050
+ const depService = enumToService.get(dep);
7051
+ const baselineSrc = isNodeOwnedService(ctx, depService) ? void 0 : baselineEnum?.sourceFile ?? baselineAlias?.sourceFile ?? liveSurfaceInterfacePath(dep);
7052
+ const depDir = resolveDir(depService);
6561
7053
  const generatedPath = `src/${depDir}/interfaces/${fileName$3(dep)}.interface.ts`;
6562
7054
  const currentFilePath = `src/${dirName}/interfaces/${fileName$3(model.name)}.interface.ts`;
6563
7055
  if (baselineSrc === currentFilePath) {
@@ -6648,6 +7140,7 @@ function generateModels$7(models, ctx, shared) {
6648
7140
  if (generatedNames.has(name)) continue;
6649
7141
  const sepPath = `src/${dirName}/interfaces/${fileName$3(name)}.interface.ts`;
6650
7142
  if (sepPath !== filePath && files.some((f) => f.path === sepPath)) continue;
7143
+ if (!isInlineEnum(name) && ctx.spec.enums.some((e) => e.name === name) && isNodeOwnedService(ctx, enumToService.get(name))) continue;
6651
7144
  inlineNames.add(name);
6652
7145
  }
6653
7146
  };
@@ -7020,6 +7513,7 @@ function baselineTypeResolvable(typeStr, importableNames) {
7020
7513
  return true;
7021
7514
  }
7022
7515
  function baselineFieldCompatible(baselineField, irField) {
7516
+ if (baselineTypeIsDegradedAny(baselineField.type) && hasNamedTypeReference(irField.type)) return false;
7023
7517
  const irNullable = irField.type.kind === "nullable";
7024
7518
  const baselineHasNull = baselineField.type.includes("null");
7025
7519
  if (irNullable && !baselineHasNull && irField.required) return false;
@@ -7027,6 +7521,21 @@ function baselineFieldCompatible(baselineField, irField) {
7027
7521
  if (baselineField.type === "Record<string, unknown>" && hasSpecificIRType(irField.type)) return false;
7028
7522
  return true;
7029
7523
  }
7524
+ /** `any`, `any[]`, `any | null`, … — shapes api-surface extraction degrades to. */
7525
+ function baselineTypeIsDegradedAny(typeStr) {
7526
+ return typeStr.replace(/\s*\|\s*(?:null|undefined)\b/g, "").replace(/\[\]$/, "").trim() === "any";
7527
+ }
7528
+ /** Does the IR type reference a named model/enum anywhere (incl. arrays)? */
7529
+ function hasNamedTypeReference(ref) {
7530
+ switch (ref.kind) {
7531
+ case "model":
7532
+ case "enum": return true;
7533
+ case "array": return hasNamedTypeReference(ref.items);
7534
+ case "nullable": return hasNamedTypeReference(ref.inner);
7535
+ case "union": return ref.variants.some((v) => hasNamedTypeReference(v));
7536
+ default: return false;
7537
+ }
7538
+ }
7030
7539
  function hasSpecificIRType(ref) {
7031
7540
  switch (ref.kind) {
7032
7541
  case "model":
@@ -8841,6 +9350,22 @@ function getEmittedPaths(ctx) {
8841
9350
  }
8842
9351
  return set;
8843
9352
  }
9353
+ /**
9354
+ * Every `GeneratedFile` the node emitter has produced so far in this ctx,
9355
+ * keyed by path. All emitter hooks share one ctx (and the engine only reads
9356
+ * file contents after the last hook returns), so the final hook can run a
9357
+ * whole-run pass over files emitted by earlier hooks — see
9358
+ * `enforceEmittedImportInvariant`.
9359
+ */
9360
+ const emittedFilesCache = /* @__PURE__ */ new WeakMap();
9361
+ function getEmittedFiles(ctx) {
9362
+ let map = emittedFilesCache.get(ctx);
9363
+ if (!map) {
9364
+ map = /* @__PURE__ */ new Map();
9365
+ emittedFilesCache.set(ctx, map);
9366
+ }
9367
+ return map;
9368
+ }
8844
9369
  function getSurface(ctx) {
8845
9370
  let surface = surfaceCache.get(ctx);
8846
9371
  if (surface) return surface;
@@ -8861,6 +9386,9 @@ function getSurface(ctx) {
8861
9386
  if (ifaces) for (const name of Object.keys(ifaces)) allInterfaces.add(name);
8862
9387
  for (const name of surface.interfaces.keys()) allInterfaces.add(name);
8863
9388
  setBaselineInterfaceNames(allInterfaces);
9389
+ const declaredNames = new Set(allInterfaces);
9390
+ for (const name of Object.keys(ctx.apiSurface?.typeAliases ?? {})) declaredNames.add(name);
9391
+ setBaselineDeclaredNames(declaredNames);
8864
9392
  setInlineEnumUnions(/* @__PURE__ */ new Map());
8865
9393
  setAdoptedModelNames(computeAdoptedModelNames(ctx, surface));
8866
9394
  const renamed = /* @__PURE__ */ new Set();
@@ -9026,6 +9554,15 @@ function isOwnedPath(relPath, policy) {
9026
9554
  const dir = topLevelDir(relPath);
9027
9555
  return dir !== void 0 && policy.ownedServiceDirs.has(dir);
9028
9556
  }
9557
+ /** Read the current on-disk content of a live-surface file, if present. */
9558
+ function readExistingSurfaceFile(surface, relPath) {
9559
+ if (!surface.rootDir) return null;
9560
+ try {
9561
+ return fs$1.readFileSync(path$1.join(surface.rootDir, relPath), "utf8");
9562
+ } catch {
9563
+ return null;
9564
+ }
9565
+ }
9029
9566
  function extractRelativeImportPaths(content, fromPath) {
9030
9567
  const dir = path$1.dirname(fromPath);
9031
9568
  const paths = [];
@@ -9072,6 +9609,15 @@ function applyLiveSurface(files, ctx, surface) {
9072
9609
  if (isManagedDir && !policy.regenerateOwnedTests) continue;
9073
9610
  }
9074
9611
  if (surface.autogenFiles.has(f.path) || ownedPath) {
9612
+ const dir = topLevelDir(f.path);
9613
+ const isAdoptedDirPath = dir !== void 0 && policy.adoptedServiceDirs.has(dir);
9614
+ if (!ownedPath && !isAdoptedDirPath && surface.autogenFiles.has(f.path)) {
9615
+ const existingText = readExistingSurfaceFile(surface, f.path);
9616
+ if (existingText !== null) {
9617
+ const merged = mergeGeneratedClassMethodsIntoExisting(existingText, f.content);
9618
+ if (merged !== null) f.content = merged;
9619
+ }
9620
+ }
9075
9621
  f.overwriteExisting = true;
9076
9622
  f.skipIfExists = false;
9077
9623
  }
@@ -9079,10 +9625,136 @@ function applyLiveSurface(files, ctx, surface) {
9079
9625
  out.push(f);
9080
9626
  }
9081
9627
  const emitted = getEmittedPaths(ctx);
9082
- for (const f of out) emitted.add(f.path);
9628
+ const emittedFiles = getEmittedFiles(ctx);
9629
+ for (const f of out) {
9630
+ emitted.add(f.path);
9631
+ emittedFiles.set(f.path, f);
9632
+ }
9083
9633
  return out;
9084
9634
  }
9085
9635
  /**
9636
+ * Matches single-line `import`/`export … from './relative'` statements — the
9637
+ * only form the node emitter produces. Captures: keyword, optional ` type`
9638
+ * modifier, the binding clause (`* [as ns]` or `{ … }`), and the module path.
9639
+ */
9640
+ const RELATIVE_FROM_STMT_RE = /^(import|export)(\s+type)?\s+(\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s+from\s+['"](\.[^'"]+)['"];?\s*$/;
9641
+ const EXPORTED_DECL_RE = /^export\s+(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:interface|class|enum|function|const|let|var|type)\s+([A-Za-z_$][\w$]*)/gm;
9642
+ const EXPORTED_CLAUSE_RE = /^export\s+(?:type\s+)?\{([^}]*)\}/gm;
9643
+ /** Repo-relative paths a relative import specifier may resolve to. */
9644
+ function importTargetCandidates(fromPath, spec) {
9645
+ const base = path$1.posix.normalize(path$1.posix.join(path$1.posix.dirname(fromPath), spec));
9646
+ return [
9647
+ base,
9648
+ `${base}.ts`,
9649
+ `${base}/index.ts`
9650
+ ];
9651
+ }
9652
+ /** Index exported symbol → file path across this run's emitted contents. */
9653
+ function indexEmittedExports(files) {
9654
+ const index = /* @__PURE__ */ new Map();
9655
+ for (const f of files) {
9656
+ if (!f.path.endsWith(".ts") || !f.content) continue;
9657
+ for (const m of f.content.matchAll(EXPORTED_DECL_RE)) if (!index.has(m[1])) index.set(m[1], f.path);
9658
+ for (const m of f.content.matchAll(EXPORTED_CLAUSE_RE)) for (const raw of m[1].split(",")) {
9659
+ const entry = raw.trim();
9660
+ if (!entry) continue;
9661
+ const parts = entry.split(/\s+as\s+/);
9662
+ const exported = (parts[1] ?? parts[0]).replace(/^type\s+/, "").trim();
9663
+ if (exported && !index.has(exported)) index.set(exported, f.path);
9664
+ }
9665
+ }
9666
+ return index;
9667
+ }
9668
+ /**
9669
+ * Enforce: every relative import/re-export path in emitted code resolves to
9670
+ * either (i) a file emitted in the same run, or (ii) a file that already
9671
+ * exists on disk in the target SDK.
9672
+ *
9673
+ * Violations observed in real generations (all TS2307 in otherwise-valid
9674
+ * output): serializer imports pointing at canonical paths while the function
9675
+ * lives in a legacy hand serializer under a different filename; barrels
9676
+ * re-exporting a module-local enum file whose declaration lives under
9677
+ * `src/common/interfaces`; barrels exporting interface files that no run
9678
+ * emits at all.
9679
+ *
9680
+ * Repair strategy, per statement whose target is neither emitted nor on
9681
+ * disk:
9682
+ * 1. Locate each imported symbol (this run's emissions first, then the
9683
+ * live-surface declaration maps) and rewrite the path to where the
9684
+ * symbol actually lives — splitting into one statement per location
9685
+ * when symbols are spread across files.
9686
+ * 2. `export * from` / namespace imports carry no symbol list, so derive
9687
+ * the expected symbol from the file stem (`foo-bar.interface` →
9688
+ * `FooBar`, `foo.serializer` → `deserializeFoo`/`serializeFoo`).
9689
+ * 3. When a symbol exists nowhere, drop the statement and warn — a missing
9690
+ * named export fails loudly at the usage site instead of as a phantom
9691
+ * module, and a barrel line for a never-emitted file is pure noise.
9692
+ *
9693
+ * Mutates `f.content` in place and returns the warnings issued.
9694
+ */
9695
+ function enforceEmittedImportInvariant(files, emittedPaths, surface) {
9696
+ const fileList = [...files];
9697
+ const emittedSymbols = indexEmittedExports(fileList);
9698
+ const warnings = [];
9699
+ const targetExists = (fromPath, spec) => importTargetCandidates(fromPath, spec).some((p) => emittedPaths.has(p) || surface.files.has(p));
9700
+ const locateSymbol = (name) => emittedSymbols.get(name) ?? surface.functions.get(name) ?? surface.interfaces.get(name)?.filePath ?? surface.classes.get(name)?.filePath;
9701
+ for (const f of fileList) {
9702
+ if (!f.path.endsWith(".ts") || !f.content) continue;
9703
+ let changed = false;
9704
+ const outLines = [];
9705
+ for (const line of f.content.split("\n")) {
9706
+ const m = line.match(RELATIVE_FROM_STMT_RE);
9707
+ if (!m || targetExists(f.path, m[4])) {
9708
+ outLines.push(line);
9709
+ continue;
9710
+ }
9711
+ const [, keyword, typeMod, clause, spec] = m;
9712
+ const repaired = repairUnresolvableStatement(f.path, keyword, typeMod ?? "", clause, spec, locateSymbol);
9713
+ changed = true;
9714
+ outLines.push(...repaired.lines);
9715
+ if (repaired.warning) warnings.push(repaired.warning);
9716
+ }
9717
+ if (changed) f.content = outLines.join("\n");
9718
+ }
9719
+ for (const w of warnings) console.warn(w);
9720
+ return warnings;
9721
+ }
9722
+ function repairUnresolvableStatement(fromPath, keyword, typeMod, clause, spec, locateSymbol) {
9723
+ if (clause.startsWith("{")) {
9724
+ const entries = clause.slice(1, -1).split(",").map((e) => e.trim()).filter(Boolean);
9725
+ const byLocation = /* @__PURE__ */ new Map();
9726
+ const missing = [];
9727
+ for (const entry of entries) {
9728
+ const source = entry.split(/\s+as\s+/)[0].replace(/^type\s+/, "").trim();
9729
+ const location = locateSymbol(source);
9730
+ if (!location) {
9731
+ missing.push(source);
9732
+ continue;
9733
+ }
9734
+ if (location === fromPath) continue;
9735
+ const group = byLocation.get(location);
9736
+ if (group) group.push(entry);
9737
+ else byLocation.set(location, [entry]);
9738
+ }
9739
+ return {
9740
+ lines: [...byLocation].map(([location, group]) => `${keyword}${typeMod} { ${group.join(", ")} } from '${relativeImport(fromPath, location)}';`),
9741
+ warning: missing.length > 0 ? `oagen(node): dropped unresolvable symbol(s) from ${keyword} in ${fromPath}: '${spec}' — found neither in this run's output nor in the target SDK: ${missing.join(", ")}` : void 0
9742
+ };
9743
+ }
9744
+ const stem = spec.split("/").pop() ?? "";
9745
+ let location;
9746
+ if (stem.endsWith(".interface")) location = locateSymbol(toPascalCase(stem.slice(0, -10)));
9747
+ else if (stem.endsWith(".serializer")) {
9748
+ const base = toPascalCase(stem.slice(0, -11));
9749
+ location = locateSymbol(`deserialize${base}`) ?? locateSymbol(`serialize${base}`);
9750
+ }
9751
+ if (location && location !== fromPath) return { lines: [`${keyword}${typeMod} ${clause} from '${relativeImport(fromPath, location)}';`] };
9752
+ return {
9753
+ lines: [],
9754
+ warning: `oagen(node): dropped unresolvable ${keyword} in ${fromPath}: '${spec}' — module is neither emitted this run nor present in the target SDK`
9755
+ };
9756
+ }
9757
+ /**
9086
9758
  * Re-declare prior-manifest paths that we did not emit this run so manifest
9087
9759
  * pruning can tell "intentionally removed" from "untouched but still managed."
9088
9760
  *
@@ -9193,7 +9865,9 @@ const nodeEmitter = {
9193
9865
  generateTests(spec, ctx) {
9194
9866
  const nodeCtx = withNodeOperationOverrides(ctx);
9195
9867
  const surface = getSurface(nodeCtx);
9196
- return [...nodeOptions(nodeCtx).regenerateOwnedTests ? applyLiveSurface(generateTests$7(spec, nodeCtx), nodeCtx, surface) : [], ...carryForwardManagedFiles(nodeCtx, surface)];
9868
+ const result = [...nodeOptions(nodeCtx).regenerateOwnedTests ? applyLiveSurface(generateTests$7(spec, nodeCtx), nodeCtx, surface) : [], ...carryForwardManagedFiles(nodeCtx, surface)];
9869
+ if (managedPathsFor(nodeCtx, surface).size > 0) enforceEmittedImportInvariant(getEmittedFiles(nodeCtx).values(), getEmittedPaths(nodeCtx), surface);
9870
+ return result;
9197
9871
  },
9198
9872
  buildOperationsMap() {
9199
9873
  return {};
@@ -28470,4 +29144,4 @@ const workosEmittersPlugin = {
28470
29144
  //#endregion
28471
29145
  export { fieldName$2 as A, servicePropertyName$2 as B, apiClassName as C, dotnetEmitter as D, propertyName as E, fieldName$3 as F, fieldName$5 as H, methodName$3 as I, trimMountedResourceFromMethod$2 as L, trimMountedResourceFromMethod$1 as M, goEmitter as N, appendAsyncSuffix as O, className$3 as P, phpEmitter as R, kotlinEmitter as S, packageSegment as T, safeParamName$1 as U, pythonEmitter as V, nodeEmitter as W, rubyEmitter as _, rustExtractor as a, resolveServiceTarget as b, pythonExtractor as c, rustEmitter as d, fieldName as f, typeName as g, resourceAccessorName as h, kotlinExtractor as i, methodName$2 as j, className$2 as k, rubyExtractor as l, moduleName as m, elixirExtractor as n, goExtractor as o, methodName as p, dotnetExtractor as r, phpExtractor as s, workosEmittersPlugin as t, nodeExtractor as u, buildExportedClassNameSet as v, methodName$1 as w, safeParamName as x, fieldName$1 as y, fieldName$4 as z };
28472
29146
 
28473
- //# sourceMappingURL=plugin-DuB1UozS.mjs.map
29147
+ //# sourceMappingURL=plugin-CpO8rePT.mjs.map