@workos/oagen-emitters 0.19.0 → 0.19.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.
@@ -117,6 +117,55 @@ function isEnumInScope(enumName, ctx) {
117
117
  const scope = ctx.scopedEnumNames;
118
118
  return !scope || scope.has(enumName);
119
119
  }
120
+ /** True when a scoped (`--services`) run is active. */
121
+ function isScopedRun(ctx) {
122
+ return !!ctx.scopedServices && ctx.scopedServices.size > 0;
123
+ }
124
+ /**
125
+ * Barrel/index inclusion gate for one item (model, enum, fixture) under a
126
+ * scoped run. A barrel/index may only reference an item whose per-item FILE
127
+ * EXISTS ON DISK after the run — otherwise the reference dangles and the SDK
128
+ * fails to compile (the bug this guards: a brand-new model belonging to an
129
+ * out-of-scope service gets declared in `mod.rs`/`__init__.py` but its source
130
+ * file is never emitted). That on-disk set is:
131
+ * in-scope items (freshly emitted this run) ∪ items already on disk
132
+ * (recorded in the prior manifest, left untouched because scoped runs never
133
+ * prune).
134
+ * Inactive scoping ⇒ always true (a full run emits and declares everything).
135
+ *
136
+ * `relPath` is the per-item file path the emitter writes (e.g.
137
+ * `src/models/foo.rs`); `inScope` is the per-item scope predicate result
138
+ * (`isModelInScope` / `isEnumInScope`).
139
+ */
140
+ function fileExistsAfterRun(relPath, inScope, ctx) {
141
+ if (!isScopedRun(ctx)) return true;
142
+ return inScope || (ctx.priorTargetManifestPaths?.has(relPath) ?? false);
143
+ }
144
+ /**
145
+ * Per-item basenames recorded in the prior manifest directly under `dir` (e.g.
146
+ * `src/models`) with extension `ext` (e.g. `.rs`), EXCLUDING `reserved` names
147
+ * (barrels such as `mod`, `_unions`, `__init__`). A scoped run uses this to
148
+ * RETAIN barrel declarations for items that were renamed/removed from the spec
149
+ * but whose files still sit on disk — and may still be referenced by
150
+ * out-of-scope code the scoped run did not regenerate (e.g. a stale resource
151
+ * file). Returns `[]` for a full run or when no prior manifest is available;
152
+ * the caller is responsible for de-duping against items it already emitted.
153
+ */
154
+ function priorManifestBasenames(ctx, dir, ext, reserved = /* @__PURE__ */ new Set()) {
155
+ if (!isScopedRun(ctx)) return [];
156
+ const paths = ctx.priorTargetManifestPaths;
157
+ if (!paths) return [];
158
+ const prefix = dir.endsWith("/") ? dir : `${dir}/`;
159
+ const out = [];
160
+ for (const p of paths) {
161
+ if (!p.startsWith(prefix) || !p.endsWith(ext)) continue;
162
+ const base = p.slice(prefix.length, p.length - ext.length);
163
+ if (base.includes("/")) continue;
164
+ if (reserved.has(base)) continue;
165
+ out.push(base);
166
+ }
167
+ return out;
168
+ }
120
169
  /**
121
170
  * Get the mount target for an IR service.
122
171
  * Checks the first resolved operation that belongs to this service.
@@ -11830,23 +11879,26 @@ function generateModels$6(models, ctx) {
11830
11879
  dispLines.push(" if dispatch_cls is not None:");
11831
11880
  dispLines.push(` return cast("${variantTypeName}", dispatch_cls.from_dict(data))`);
11832
11881
  dispLines.push(` return ${unknownClassName}.from_dict(data)`);
11833
- if (isModelInScope(model.name, ctx)) files.push({
11882
+ const dispInScope = isModelInScope(model.name, ctx);
11883
+ if (dispInScope) files.push({
11834
11884
  path: `src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`,
11835
11885
  content: dispLines.join("\n"),
11836
11886
  integrateTarget: true,
11837
11887
  overwriteExisting: true
11838
11888
  });
11839
- if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
11840
- emittedModelSymbolsByDir.get(dirName).push(model.name);
11841
- emittedModelSymbolsByDir.get(dirName).push(variantTypeName);
11842
- symbolToFile.set(variantTypeName, fileName$2(model.name));
11843
- emittedModelSymbolsByDir.get(dirName).push(unknownClassName);
11844
- symbolToFile.set(unknownClassName, fileName$2(model.name));
11845
- const dispatcherNatural = originalModelToService.get(model.name);
11846
- if (dispatcherNatural) {
11847
- symbolToOriginalService.set(model.name, dispatcherNatural);
11848
- symbolToOriginalService.set(variantTypeName, dispatcherNatural);
11849
- symbolToOriginalService.set(unknownClassName, dispatcherNatural);
11889
+ if (fileExistsAfterRun(`src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`, dispInScope, ctx)) {
11890
+ if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
11891
+ emittedModelSymbolsByDir.get(dirName).push(model.name);
11892
+ emittedModelSymbolsByDir.get(dirName).push(variantTypeName);
11893
+ symbolToFile.set(variantTypeName, fileName$2(model.name));
11894
+ emittedModelSymbolsByDir.get(dirName).push(unknownClassName);
11895
+ symbolToFile.set(unknownClassName, fileName$2(model.name));
11896
+ const dispatcherNatural = originalModelToService.get(model.name);
11897
+ if (dispatcherNatural) {
11898
+ symbolToOriginalService.set(model.name, dispatcherNatural);
11899
+ symbolToOriginalService.set(variantTypeName, dispatcherNatural);
11900
+ symbolToOriginalService.set(unknownClassName, dispatcherNatural);
11901
+ }
11850
11902
  }
11851
11903
  continue;
11852
11904
  }
@@ -11861,16 +11913,20 @@ function generateModels$6(models, ctx) {
11861
11913
  else lines.push(`from ${ctx.namespace}.${dirToModule(canonicalDir)}.models.${fileName$2(canonicalName)} import ${canonicalClassName}`);
11862
11914
  lines.push("");
11863
11915
  lines.push(`${modelClassName}: TypeAlias = ${canonicalClassName}`);
11864
- if (isModelInScope(model.name, ctx)) files.push({
11865
- path: `src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`,
11916
+ const aliasInScope = isModelInScope(model.name, ctx);
11917
+ const aliasPath = `src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`;
11918
+ if (aliasInScope) files.push({
11919
+ path: aliasPath,
11866
11920
  content: lines.join("\n"),
11867
11921
  integrateTarget: true,
11868
11922
  overwriteExisting: true
11869
11923
  });
11870
- if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
11871
- emittedModelSymbolsByDir.get(dirName).push(model.name);
11872
- const aliasNatural = originalModelToService.get(model.name);
11873
- if (aliasNatural) symbolToOriginalService.set(model.name, aliasNatural);
11924
+ if (fileExistsAfterRun(aliasPath, aliasInScope, ctx)) {
11925
+ if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
11926
+ emittedModelSymbolsByDir.get(dirName).push(model.name);
11927
+ const aliasNatural = originalModelToService.get(model.name);
11928
+ if (aliasNatural) symbolToOriginalService.set(model.name, aliasNatural);
11929
+ }
11874
11930
  continue;
11875
11931
  }
11876
11932
  const seenFieldNames = /* @__PURE__ */ new Set();
@@ -12015,16 +12071,19 @@ function generateModels$6(models, ctx) {
12015
12071
  }
12016
12072
  }
12017
12073
  lines.push(" return result");
12018
- if (isModelInScope(model.name, ctx)) files.push({
12019
- path: `src/${ctx.namespace}/${dirName}/models/${fileName$2(model.name)}.py`,
12074
+ const regularInScope = isModelInScope(model.name, ctx);
12075
+ if (regularInScope) files.push({
12076
+ path: modelFilePath,
12020
12077
  content: lines.join("\n"),
12021
12078
  integrateTarget: true,
12022
12079
  overwriteExisting: true
12023
12080
  });
12024
- if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
12025
- emittedModelSymbolsByDir.get(dirName).push(model.name);
12026
- const regularNatural = originalModelToService.get(model.name);
12027
- if (regularNatural) symbolToOriginalService.set(model.name, regularNatural);
12081
+ if (fileExistsAfterRun(modelFilePath, regularInScope, ctx)) {
12082
+ if (!emittedModelSymbolsByDir.has(dirName)) emittedModelSymbolsByDir.set(dirName, []);
12083
+ emittedModelSymbolsByDir.get(dirName).push(model.name);
12084
+ const regularNatural = originalModelToService.get(model.name);
12085
+ if (regularNatural) symbolToOriginalService.set(model.name, regularNatural);
12086
+ }
12028
12087
  }
12029
12088
  const symbolsByDir = /* @__PURE__ */ new Map();
12030
12089
  for (const [dirName, names] of emittedModelSymbolsByDir) {
@@ -12033,11 +12092,35 @@ function generateModels$6(models, ctx) {
12033
12092
  for (const name of names) symbolsByDir.get(key).push({ name });
12034
12093
  }
12035
12094
  const reachableEnumNames = collectReachableEnumNames(ctx);
12036
- const enumSymbolsByDir = collectGeneratedEnumSymbolsByDir(ctx.spec.enums.filter((enumDef) => reachableEnumNames.has(enumDef.name)), ctx);
12095
+ const emittedEnums = ctx.spec.enums.filter((enumDef) => reachableEnumNames.has(enumDef.name));
12096
+ const enumSymbolsByDir = collectGeneratedEnumSymbolsByDir(emittedEnums, ctx);
12097
+ const aliasToCanonicalEnum = /* @__PURE__ */ new Map();
12098
+ for (const [canonical, aliasNames] of collectCompatEnumAliases(emittedEnums, ctx)) for (const aliasName of aliasNames) aliasToCanonicalEnum.set(aliasName, canonical);
12037
12099
  for (const [dirName, names] of enumSymbolsByDir) {
12038
12100
  const key = `src/${ctx.namespace}/${dirName}/models`;
12039
12101
  if (!symbolsByDir.has(key)) symbolsByDir.set(key, []);
12040
- for (const name of names) symbolsByDir.get(key).push({ name });
12102
+ for (const name of names) if (fileExistsAfterRun(`${key}/${fileName$2(name)}.py`, isEnumInScope(aliasToCanonicalEnum.get(name) ?? name, ctx), ctx)) symbolsByDir.get(key).push({ name });
12103
+ }
12104
+ const manifestModelDirs = /* @__PURE__ */ new Set();
12105
+ for (const p of ctx.priorTargetManifestPaths ?? []) {
12106
+ const m = p.match(new RegExp(`^(src/${ctx.namespace}/[^/]+/models)/[^/]+\\.py$`));
12107
+ if (m) manifestModelDirs.add(m[1]);
12108
+ }
12109
+ for (const dirPath of manifestModelDirs) {
12110
+ if (!symbolsByDir.has(dirPath)) symbolsByDir.set(dirPath, []);
12111
+ const referencedBasenames = /* @__PURE__ */ new Set();
12112
+ for (const sym of symbolsByDir.get(dirPath)) {
12113
+ if (sym.reExport || sym.retainBasename) continue;
12114
+ referencedBasenames.add(symbolToFile.get(sym.name) ?? fileName$2(sym.name));
12115
+ }
12116
+ for (const base of priorManifestBasenames(ctx, dirPath, ".py", new Set(["__init__"]))) {
12117
+ if (referencedBasenames.has(base)) continue;
12118
+ referencedBasenames.add(base);
12119
+ symbolsByDir.get(dirPath).push({
12120
+ name: base,
12121
+ retainBasename: base
12122
+ });
12123
+ }
12041
12124
  }
12042
12125
  const commonDirName = "common";
12043
12126
  const addReExport = (naturalService, name, sourceFile) => {
@@ -12082,6 +12165,10 @@ function generateModels$6(models, ctx) {
12082
12165
  const uniqueSymbols = [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
12083
12166
  const importLines = [];
12084
12167
  for (const sym of uniqueSymbols) {
12168
+ if (sym.retainBasename) {
12169
+ importLines.push(`from .${sym.retainBasename} import * # noqa: F401,F403`);
12170
+ continue;
12171
+ }
12085
12172
  const cls = className$5(sym.name);
12086
12173
  if (sym.reExport) importLines.push(`from ${ctx.namespace}.${dirToModule(sym.reExport.fromDir)}.models.${sym.reExport.file} import ${cls} as ${cls}`);
12087
12174
  else {
@@ -12098,7 +12185,7 @@ function generateModels$6(models, ctx) {
12098
12185
  });
12099
12186
  if (!serviceDirModelPaths.has(dirPath)) {
12100
12187
  const parentDir = dirPath.replace(/\/models$/, "");
12101
- const reExports = uniqueSymbols.map((sym) => `from .models import ${className$5(sym.name)} as ${className$5(sym.name)}`).join("\n");
12188
+ const reExports = [...new Set(uniqueSymbols.map((sym) => sym.retainBasename ? "from .models import * # noqa: F401,F403" : `from .models import ${className$5(sym.name)} as ${className$5(sym.name)}`))].join("\n");
12102
12189
  files.push({
12103
12190
  path: `${parentDir}/__init__.py`,
12104
12191
  content: reExports,
@@ -13818,18 +13905,30 @@ const ID_PREFIXES$3 = {
13818
13905
  };
13819
13906
  /**
13820
13907
  * Generate JSON fixture files for test data.
13821
- */
13822
- function generateFixtures$4(spec) {
13908
+ *
13909
+ * `ctx` is optional so unit tests can call with a bare spec; when supplied, a
13910
+ * scoped (`--services`) run only emits a fixture for a model whose per-model
13911
+ * file will exist on disk after the run (in-scope, or already present from a
13912
+ * prior manifest). Brand-new out-of-scope models are skipped: the round-trip
13913
+ * test that consumes these fixtures is gated the same way, and the per-service
13914
+ * tests only reference fixtures for their (in-scope) models.
13915
+ */
13916
+ function generateFixtures$4(spec, ctx) {
13823
13917
  if (spec.models.length === 0) return [];
13824
13918
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
13825
13919
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
13826
13920
  const files = [];
13921
+ const fixtureEmitted = (path, modelName) => {
13922
+ if (!ctx) return true;
13923
+ return fileExistsAfterRun(path, isModelInScope(modelName, ctx), ctx);
13924
+ };
13827
13925
  const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
13828
13926
  const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
13829
13927
  for (const model of spec.models) {
13830
13928
  if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
13831
13929
  if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
13832
13930
  if (model.fields.length === 0) continue;
13931
+ if (!fixtureEmitted(`tests/fixtures/${fileName$2(model.name)}.json`, model.name)) continue;
13833
13932
  const fixture = generateModelFixture$4(model, modelMap, enumMap);
13834
13933
  files.push({
13835
13934
  path: `tests/fixtures/${fileName$2(model.name)}.json`,
@@ -13842,6 +13941,7 @@ function generateFixtures$4(spec) {
13842
13941
  const unwrapped = unwrapListModel$3(itemModel, modelMap);
13843
13942
  if (unwrapped) itemModel = unwrapped;
13844
13943
  if (itemModel.fields.length === 0) continue;
13944
+ if (!fixtureEmitted(`tests/fixtures/list_${fileName$2(itemModel.name)}.json`, itemModel.name)) continue;
13845
13945
  const listFixture = {
13846
13946
  data: [generateModelFixture$4(itemModel, modelMap, enumMap)],
13847
13947
  list_metadata: {
@@ -13984,7 +14084,7 @@ function buildDeleteSuccessResponseSetup(op) {
13984
14084
  */
13985
14085
  function generateTests$6(spec, ctx) {
13986
14086
  const files = [];
13987
- const fixtures = generateFixtures$4(spec);
14087
+ const fixtures = generateFixtures$4(spec, ctx);
13988
14088
  for (const fixture of fixtures) files.push({
13989
14089
  path: fixture.path,
13990
14090
  content: fixture.content,
@@ -14961,11 +15061,17 @@ function generateModelRoundTripTests(spec, ctx) {
14961
15061
  for (const name of responseModelNames) requestOnlyModelNames.delete(name);
14962
15062
  const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
14963
15063
  const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
14964
- const models = spec.models.filter((m) => !(isListWrapperModel(m) && !nonPaginatedRefs.has(m.name)) && !(isListMetadataModel(m) && !listMetadataNeeded.has(m.name)) && !requestOnlyModelNames.has(m.name));
14965
- if (models.length === 0) return null;
14966
- const modelToService = computeSchemaPlacement(spec, ctx).originalModelToService;
15064
+ const placement = computeSchemaPlacement(spec, ctx);
15065
+ const modelToService = placement.originalModelToService;
15066
+ const relocatedModelToService = placement.modelToService;
14967
15067
  const roundTripDirMap = buildMountDirMap$1(ctx);
14968
15068
  const resolveDir = (irService) => irService ? roundTripDirMap.get(irService) ?? "common" : "common";
15069
+ const modelFileExists = (name) => {
15070
+ const dir = resolveDir(relocatedModelToService.get(name));
15071
+ return fileExistsAfterRun(`src/${ctx.namespace}/${dir}/models/${fileName$2(name)}.py`, isModelInScope(name, ctx), ctx);
15072
+ };
15073
+ const models = spec.models.filter((m) => !(isListWrapperModel(m) && !nonPaginatedRefs.has(m.name)) && !(isListMetadataModel(m) && !listMetadataNeeded.has(m.name)) && !requestOnlyModelNames.has(m.name) && modelFileExists(m.name));
15074
+ if (models.length === 0) return null;
14969
15075
  const lines = [];
14970
15076
  lines.push("\"\"\"Model round-trip tests: from_dict(to_dict()) preserves data.\"\"\"");
14971
15077
  lines.push("");
@@ -17298,6 +17404,169 @@ function unionResolverName(ref) {
17298
17404
  return null;
17299
17405
  }
17300
17406
  //#endregion
17407
+ //#region src/go/flat-merge.ts
17408
+ /** Read the prior on-disk content of a generated file, or null when unavailable. */
17409
+ function readPriorFile(relPath, ctx) {
17410
+ if (!ctx.outputDir) return null;
17411
+ const abs = resolve(ctx.outputDir, relPath);
17412
+ if (!existsSync(abs)) return null;
17413
+ try {
17414
+ return readFileSync(abs, "utf-8");
17415
+ } catch {
17416
+ return null;
17417
+ }
17418
+ }
17419
+ /**
17420
+ * Parse a flat Go file into the set of top-level type names it declares, mapped
17421
+ * to the verbatim text of each declaration block (including its leading doc
17422
+ * comment). Used to recover the per-type "present before" signal a scoped run
17423
+ * needs. Recognizes the exact shapes the Go emitter produces:
17424
+ * - `type Name struct { ... }` (brace-balanced)
17425
+ * - `type Name = Other` (single-line alias)
17426
+ * - `type Name string` + `const ( ... )` (string enum)
17427
+ * - `type ( A = X\n B = X )` (batched alias block — declares many)
17428
+ * The leading `package` clause and any standalone trailing `const (...)` block
17429
+ * (e.g. the events file) are returned separately by {@link parseFlatGoBlocks}.
17430
+ */
17431
+ function parseFlatGoBlocks(content) {
17432
+ const lines = content.split("\n");
17433
+ const blocks = [];
17434
+ let i = 0;
17435
+ while (i < lines.length) {
17436
+ const t = lines[i].trim();
17437
+ if (t.startsWith("package ") || t === "" || t.startsWith("// Code generated")) {
17438
+ i++;
17439
+ continue;
17440
+ }
17441
+ break;
17442
+ }
17443
+ while (i < lines.length) {
17444
+ const start = i;
17445
+ while (i < lines.length && lines[i].trim().startsWith("//")) i++;
17446
+ if (i >= lines.length) break;
17447
+ const trimmed = lines[i].trim();
17448
+ if (trimmed === "type (") {
17449
+ const names = [];
17450
+ i++;
17451
+ while (i < lines.length && lines[i].trim() !== ")") {
17452
+ const m = lines[i].trim().match(/^(\w+)\s*=/);
17453
+ if (m) names.push(m[1]);
17454
+ i++;
17455
+ }
17456
+ i++;
17457
+ blocks.push({
17458
+ names,
17459
+ text: lines.slice(start, i).join("\n")
17460
+ });
17461
+ } else if (/^type\s+(\w+)\s+struct\s*\{/.test(trimmed)) {
17462
+ const name = trimmed.match(/^type\s+(\w+)/)[1];
17463
+ let depth = 0;
17464
+ let sawOpen = false;
17465
+ while (i < lines.length) {
17466
+ for (const ch of lines[i]) if (ch === "{") {
17467
+ depth++;
17468
+ sawOpen = true;
17469
+ } else if (ch === "}") depth--;
17470
+ i++;
17471
+ if (sawOpen && depth === 0) break;
17472
+ }
17473
+ blocks.push({
17474
+ names: [name],
17475
+ text: lines.slice(start, i).join("\n")
17476
+ });
17477
+ } else if (/^type\s+(\w+)\s+\w+\s+string\b/.test(trimmed) || /^type\s+(\w+)\s+string\b/.test(trimmed)) {
17478
+ const name = trimmed.match(/^type\s+(\w+)/)[1];
17479
+ i++;
17480
+ let j = i;
17481
+ while (j < lines.length && lines[j].trim() === "") j++;
17482
+ if (j < lines.length && lines[j].trim() === "const (") {
17483
+ i = j;
17484
+ while (i < lines.length && lines[i].trim() !== ")") i++;
17485
+ i++;
17486
+ }
17487
+ blocks.push({
17488
+ names: [name],
17489
+ text: lines.slice(start, i).join("\n")
17490
+ });
17491
+ } else if (/^type\s+(\w+)\s*=/.test(trimmed)) {
17492
+ const name = trimmed.match(/^type\s+(\w+)/)[1];
17493
+ i++;
17494
+ blocks.push({
17495
+ names: [name],
17496
+ text: lines.slice(start, i).join("\n")
17497
+ });
17498
+ } else i++;
17499
+ while (i < lines.length && lines[i].trim() === "") i++;
17500
+ }
17501
+ const byName = /* @__PURE__ */ new Map();
17502
+ for (const b of blocks) for (const n of b.names) byName.set(n, b);
17503
+ return {
17504
+ blocks,
17505
+ byName
17506
+ };
17507
+ }
17508
+ /**
17509
+ * Reconcile freshly generated named blocks against the prior on-disk file for a
17510
+ * scoped run. Returns the ordered list of block texts to emit. See file-level
17511
+ * docs for the keep/carry-over rules. In a scoped run a block is emitted as:
17512
+ * - IN-SCOPE → the freshly generated text (apply the new spec).
17513
+ * - out-of-scope, existed before → the PRIOR on-disk text, FROZEN, so an
17514
+ * unrelated change to that type in the same spec delta doesn't leak into a
17515
+ * scoped batch (Option B). (Falls back to fresh text only when the new
17516
+ * block's names span multiple prior blocks — a batched-alias regrouping —
17517
+ * which can't be frozen 1:1.)
17518
+ * - out-of-scope, brand-new → dropped (the addition that broke the build).
17519
+ * Then any prior block the new spec no longer produces at all is carried over
17520
+ * verbatim (renamed/removed types still referenced by un-regenerated code).
17521
+ *
17522
+ * @param newBlocks Per-type blocks the current spec produced (in emit order),
17523
+ * each tagged with `inScope`.
17524
+ * @param relPath Flat file path (e.g. `models.go`) for reading the prior file.
17525
+ * @param ctx Emitter context (provides `outputDir` + scope sets).
17526
+ * @param alsoEmitted Names this file emits OUTSIDE `newBlocks` (e.g. the fixed
17527
+ * `PaginationParams` struct). They must be excluded from the
17528
+ * carry-over, or the prior copy would be re-appended and
17529
+ * redeclare the separately-emitted one.
17530
+ */
17531
+ function reconcileFlatBlocks(newBlocks, relPath, ctx, alsoEmitted = /* @__PURE__ */ new Set()) {
17532
+ if (!isScopedRun(ctx)) return newBlocks.map((b) => b.text);
17533
+ const prior = readPriorFile(relPath, ctx);
17534
+ const priorParsed = prior ? parseFlatGoBlocks(prior) : {
17535
+ blocks: [],
17536
+ byName: /* @__PURE__ */ new Map()
17537
+ };
17538
+ const out = [];
17539
+ const emittedNames = /* @__PURE__ */ new Set();
17540
+ for (const block of newBlocks) {
17541
+ if (block.inScope) {
17542
+ out.push(block.text);
17543
+ for (const n of block.names) emittedNames.add(n);
17544
+ continue;
17545
+ }
17546
+ const priorBlocks = block.names.map((n) => priorParsed.byName.get(n)).filter((b) => !!b);
17547
+ const uniquePrior = new Set(priorBlocks);
17548
+ if (uniquePrior.size === 1) {
17549
+ const pb = priorBlocks[0];
17550
+ if (!pb.names.some((n) => emittedNames.has(n))) {
17551
+ out.push(pb.text);
17552
+ for (const n of pb.names) emittedNames.add(n);
17553
+ }
17554
+ } else if (uniquePrior.size > 1) for (const pb of uniquePrior) {
17555
+ if (pb.names.some((n) => emittedNames.has(n))) continue;
17556
+ out.push(pb.text);
17557
+ for (const n of pb.names) emittedNames.add(n);
17558
+ }
17559
+ }
17560
+ for (const block of priorParsed.blocks) {
17561
+ if (block.names.some((n) => alsoEmitted.has(n))) continue;
17562
+ if (block.names.every((n) => !emittedNames.has(n))) {
17563
+ out.push(block.text);
17564
+ for (const n of block.names) emittedNames.add(n);
17565
+ }
17566
+ }
17567
+ return out;
17568
+ }
17569
+ //#endregion
17301
17570
  //#region src/go/models.ts
17302
17571
  /**
17303
17572
  * Collect names of models that are referenced **only** as a named request body
@@ -17365,6 +17634,8 @@ function generateModels$4(models, ctx) {
17365
17634
  if (!hashGroups.has(hash)) hashGroups.set(hash, []);
17366
17635
  hashGroups.get(hash).push(model.name);
17367
17636
  }
17637
+ const priorModelNames = isScopedRun(ctx) ? new Set(parseFlatGoBlocks(readPriorFile("models.go", ctx) ?? "").blocks.flatMap((b) => b.names)) : /* @__PURE__ */ new Set();
17638
+ const aliasEmitted = (name) => !isScopedRun(ctx) || isModelInScope(name, ctx) || priorModelNames.has(className$3(name));
17368
17639
  const aliasOf = /* @__PURE__ */ new Map();
17369
17640
  for (const [hash, names] of hashGroups) {
17370
17641
  if (names.length <= 1) continue;
@@ -17373,6 +17644,11 @@ function generateModels$4(models, ctx) {
17373
17644
  const canonical = sorted[0];
17374
17645
  for (let i = 1; i < sorted.length; i++) aliasOf.set(sorted[i], canonical);
17375
17646
  }
17647
+ const forcedCanonicals = /* @__PURE__ */ new Set();
17648
+ if (isScopedRun(ctx)) {
17649
+ for (const [aliasName, canonical] of aliasOf) if (isModelInScope(aliasName, ctx)) forcedCanonicals.add(canonical);
17650
+ }
17651
+ const modelBlocks = [];
17376
17652
  const batchedAliases = /* @__PURE__ */ new Set();
17377
17653
  for (const model of models) {
17378
17654
  if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
@@ -17385,29 +17661,44 @@ function generateModels$4(models, ctx) {
17385
17661
  if (structName === canonicalStruct) continue;
17386
17662
  const hash = modelHashMap.get(model.name);
17387
17663
  const aliases = (hashGroups.get(hash) ?? []).filter((n) => aliasOf.has(n) && className$3(n) !== className$3(aliasOf.get(n)));
17664
+ const blockLines = [];
17665
+ const blockNames = [];
17666
+ let blockInScope = false;
17388
17667
  if (aliases.length >= 5) {
17389
17668
  for (const aliasName of aliases) batchedAliases.add(aliasName);
17390
- lines.push(`// The following types are structurally identical to ${canonicalStruct}.`);
17391
- lines.push("type (");
17392
- for (const aliasName of aliases) lines.push(`\t${className$3(aliasName)} = ${canonicalStruct}`);
17393
- lines.push(")");
17394
- lines.push("");
17669
+ const emittedAliases = aliases.filter(aliasEmitted);
17670
+ if (emittedAliases.length === 0) continue;
17671
+ blockLines.push(`// The following types are structurally identical to ${canonicalStruct}.`);
17672
+ blockLines.push("type (");
17673
+ for (const aliasName of emittedAliases) {
17674
+ blockLines.push(`\t${className$3(aliasName)} = ${canonicalStruct}`);
17675
+ blockNames.push(className$3(aliasName));
17676
+ if (isModelInScope(aliasName, ctx)) blockInScope = true;
17677
+ }
17678
+ blockLines.push(")");
17395
17679
  } else {
17396
- lines.push(`// ${structName} is an alias for ${canonicalStruct}.`);
17397
- lines.push(`type ${structName} = ${canonicalStruct}`);
17398
- lines.push("");
17399
- }
17680
+ blockLines.push(`// ${structName} is an alias for ${canonicalStruct}.`);
17681
+ blockLines.push(`type ${structName} = ${canonicalStruct}`);
17682
+ blockNames.push(structName);
17683
+ if (isModelInScope(model.name, ctx)) blockInScope = true;
17684
+ }
17685
+ modelBlocks.push({
17686
+ names: blockNames,
17687
+ text: blockLines.join("\n"),
17688
+ inScope: blockInScope
17689
+ });
17400
17690
  continue;
17401
17691
  }
17692
+ const blockLines = [];
17402
17693
  if (model.description) {
17403
17694
  const descLines = model.description.split("\n").filter((l) => l.trim());
17404
- lines.push(`// ${structName} ${lowerFirst$1(descLines[0])}`);
17405
- for (let i = 1; i < descLines.length; i++) lines.push(`// ${descLines[i].trim()}`);
17695
+ blockLines.push(`// ${structName} ${lowerFirst$1(descLines[0])}`);
17696
+ for (let i = 1; i < descLines.length; i++) blockLines.push(`// ${descLines[i].trim()}`);
17406
17697
  } else {
17407
17698
  const humanized = humanize$3(model.name);
17408
- lines.push(`// ${structName} represents ${articleFor$1(humanized)} ${humanized}.`);
17699
+ blockLines.push(`// ${structName} represents ${articleFor$1(humanized)} ${humanized}.`);
17409
17700
  }
17410
- lines.push(`type ${structName} struct {`);
17701
+ blockLines.push(`type ${structName} struct {`);
17411
17702
  const seenFieldNames = /* @__PURE__ */ new Set();
17412
17703
  for (const field of model.fields) {
17413
17704
  const goFieldName = domainFieldName$3(field);
@@ -17417,17 +17708,26 @@ function generateModels$4(models, ctx) {
17417
17708
  const jsonTag = field.required ? `json:"${field.name}"` : `json:"${field.name},omitempty"`;
17418
17709
  if (field.description) {
17419
17710
  const fdLines = field.description.split("\n").filter((l) => l.trim());
17420
- lines.push(`\t// ${fieldDocComment(goFieldName, fdLines[0])}`);
17421
- for (let i = 1; i < fdLines.length; i++) lines.push(`\t// ${fdLines[i].trim()}`);
17711
+ blockLines.push(`\t// ${fieldDocComment(goFieldName, fdLines[0])}`);
17712
+ for (let i = 1; i < fdLines.length; i++) blockLines.push(`\t// ${fdLines[i].trim()}`);
17422
17713
  }
17423
17714
  if (field.deprecated) {
17424
- if (field.description) lines.push(`\t//`);
17715
+ if (field.description) blockLines.push(`\t//`);
17425
17716
  const deprecationReason = extractDeprecationReason(field.description);
17426
- lines.push(`\t// Deprecated: ${deprecationReason}`);
17717
+ blockLines.push(`\t// Deprecated: ${deprecationReason}`);
17427
17718
  }
17428
- lines.push(`\t${goFieldName} ${goType} \`${jsonTag}\``);
17719
+ blockLines.push(`\t${goFieldName} ${goType} \`${jsonTag}\``);
17429
17720
  }
17430
- lines.push("}");
17721
+ blockLines.push("}");
17722
+ modelBlocks.push({
17723
+ names: [structName],
17724
+ text: blockLines.join("\n"),
17725
+ inScope: isModelInScope(model.name, ctx) || forcedCanonicals.has(model.name)
17726
+ });
17727
+ }
17728
+ const reconciled = reconcileFlatBlocks(modelBlocks, "models.go", ctx, new Set(["PaginationParams"]));
17729
+ for (const text of reconciled) {
17730
+ lines.push(text);
17431
17731
  lines.push("");
17432
17732
  }
17433
17733
  const orderEnumType = detectSharedOrderEnum(ctx.spec.services);
@@ -17590,27 +17890,36 @@ function extractDeprecationReason(description) {
17590
17890
  function generateEnums$4(enums, ctx) {
17591
17891
  if (enums.length === 0) return [];
17592
17892
  const aliasOf = collectEnumAliasOf$2(enums);
17893
+ const forcedCanonicals = /* @__PURE__ */ new Set();
17894
+ if (isScopedRun(ctx)) {
17895
+ for (const [aliasName, canonical] of aliasOf) if (isEnumInScope(aliasName, ctx)) forcedCanonicals.add(canonical);
17896
+ }
17593
17897
  const files = [];
17594
17898
  const lines = [];
17595
17899
  lines.push(`package ${ctx.namespace}`);
17596
17900
  lines.push("");
17901
+ const enumBlocks = [];
17597
17902
  for (const enumDef of enums) {
17598
17903
  const canonicalName = aliasOf.get(enumDef.name);
17599
17904
  if (canonicalName) {
17600
17905
  const aliasType = className$3(enumDef.name);
17601
17906
  const canonicalType = className$3(canonicalName);
17602
17907
  if (aliasType === canonicalType) continue;
17603
- lines.push(`// ${aliasType} is an alias for ${canonicalType}.`);
17604
- lines.push(`type ${aliasType} = ${canonicalType}`);
17605
- lines.push("");
17908
+ enumBlocks.push({
17909
+ names: [aliasType],
17910
+ text: [`// ${aliasType} is an alias for ${canonicalType}.`, `type ${aliasType} = ${canonicalType}`].join("\n"),
17911
+ inScope: isEnumInScope(enumDef.name, ctx)
17912
+ });
17606
17913
  continue;
17607
17914
  }
17608
17915
  const typeName = className$3(enumDef.name);
17609
17916
  if (enumDef.values.length === 0) {
17610
17917
  const humanized = humanize$2(enumDef.name);
17611
- lines.push(`// ${typeName} represents ${humanized} values.`);
17612
- lines.push(`type ${typeName} = string`);
17613
- lines.push("");
17918
+ enumBlocks.push({
17919
+ names: [typeName],
17920
+ text: [`// ${typeName} represents ${humanized} values.`, `type ${typeName} = string`].join("\n"),
17921
+ inScope: isEnumInScope(enumDef.name, ctx) || forcedCanonicals.has(enumDef.name)
17922
+ });
17614
17923
  continue;
17615
17924
  }
17616
17925
  const seenValues = /* @__PURE__ */ new Set();
@@ -17623,10 +17932,11 @@ function generateEnums$4(enums, ctx) {
17623
17932
  }
17624
17933
  }
17625
17934
  const humanized = humanize$2(enumDef.name);
17626
- lines.push(`// ${typeName} represents ${humanized} values.`);
17627
- lines.push(`type ${typeName} string`);
17628
- lines.push("");
17629
- lines.push("const (");
17935
+ const blockLines = [];
17936
+ blockLines.push(`// ${typeName} represents ${humanized} values.`);
17937
+ blockLines.push(`type ${typeName} string`);
17938
+ blockLines.push("");
17939
+ blockLines.push("const (");
17630
17940
  const usedNames = /* @__PURE__ */ new Set();
17631
17941
  for (const v of uniqueValues) {
17632
17942
  let constSuffix = className$3(String(v.value));
@@ -17638,14 +17948,23 @@ function generateEnums$4(enums, ctx) {
17638
17948
  const constName = `${typeName}${constSuffix}`;
17639
17949
  usedNames.add(constName);
17640
17950
  const valueStr = typeof v.value === "string" ? `"${v.value}"` : String(v.value);
17641
- if (v.description) lines.push(`\t// ${constName} is ${v.description}.`);
17951
+ if (v.description) blockLines.push(`\t// ${constName} is ${v.description}.`);
17642
17952
  if (v.deprecated) {
17643
- if (v.description) lines.push(`\t//`);
17644
- lines.push(`\t// Deprecated: this value is deprecated.`);
17953
+ if (v.description) blockLines.push(`\t//`);
17954
+ blockLines.push(`\t// Deprecated: this value is deprecated.`);
17645
17955
  }
17646
- lines.push(`\t${constName} ${typeName} = ${valueStr}`);
17956
+ blockLines.push(`\t${constName} ${typeName} = ${valueStr}`);
17647
17957
  }
17648
- lines.push(")");
17958
+ blockLines.push(")");
17959
+ enumBlocks.push({
17960
+ names: [typeName],
17961
+ text: blockLines.join("\n"),
17962
+ inScope: isEnumInScope(enumDef.name, ctx) || forcedCanonicals.has(enumDef.name)
17963
+ });
17964
+ }
17965
+ const reconciled = reconcileFlatBlocks(enumBlocks, "enums.go", ctx);
17966
+ for (const text of reconciled) {
17967
+ lines.push(text);
17649
17968
  lines.push("");
17650
17969
  }
17651
17970
  files.push({
@@ -17653,7 +17972,7 @@ function generateEnums$4(enums, ctx) {
17653
17972
  content: lines.join("\n"),
17654
17973
  overwriteExisting: true
17655
17974
  });
17656
- const eventConstantsFile = generateEventConstantsFile(enums);
17975
+ const eventConstantsFile = generateEventConstantsFile(enums, ctx);
17657
17976
  if (eventConstantsFile) files.push(eventConstantsFile);
17658
17977
  return files;
17659
17978
  }
@@ -17715,9 +18034,15 @@ function collectEnumAliasOf$2(enums) {
17715
18034
  }
17716
18035
  return aliasOf;
17717
18036
  }
17718
- function generateEventConstantsFile(enums) {
18037
+ const EVENTS_FILE_PATH = "pkg/events/events.go";
18038
+ function generateEventConstantsFile(enums, ctx) {
17719
18039
  const enumDef = findWebhookEventEnum(enums);
17720
18040
  if (!enumDef) return null;
18041
+ let priorEventConsts = null;
18042
+ if (isScopedRun(ctx)) {
18043
+ const prior = readPriorFile(EVENTS_FILE_PATH, ctx);
18044
+ if (prior !== null) priorEventConsts = collectPriorEventConstNames(prior);
18045
+ }
17721
18046
  const lines = [];
17722
18047
  lines.push("package events");
17723
18048
  lines.push("");
@@ -17732,6 +18057,7 @@ function generateEventConstantsFile(enums) {
17732
18057
  if (seenValues.has(valueStr)) continue;
17733
18058
  seenValues.add(valueStr);
17734
18059
  const constName = uniqueEventConstantName(valueStr, usedNames);
18060
+ if (priorEventConsts !== null && !priorEventConsts.has(constName)) continue;
17735
18061
  usedNames.add(constName);
17736
18062
  if (value.description) lines.push(`\t// ${constName} is ${value.description}.`);
17737
18063
  if (value.deprecated) {
@@ -17743,11 +18069,25 @@ function generateEventConstantsFile(enums) {
17743
18069
  lines.push(")");
17744
18070
  lines.push("");
17745
18071
  return {
17746
- path: "pkg/events/events.go",
18072
+ path: EVENTS_FILE_PATH,
17747
18073
  content: lines.join("\n"),
17748
18074
  overwriteExisting: true
17749
18075
  };
17750
18076
  }
18077
+ /**
18078
+ * Collect the constant names declared in a prior `pkg/events/events.go`. Each
18079
+ * event constant is emitted as `\tConstName = "wire.value"` inside the single
18080
+ * `const (...)` block, so a simple line scan recovers the prior name set used
18081
+ * to keep the file byte-stable across scoped runs.
18082
+ */
18083
+ function collectPriorEventConstNames(content) {
18084
+ const names = /* @__PURE__ */ new Set();
18085
+ for (const raw of content.split("\n")) {
18086
+ const m = raw.trim().match(/^(\w+)\s*=\s*"/);
18087
+ if (m) names.add(m[1]);
18088
+ }
18089
+ return names;
18090
+ }
17751
18091
  function findWebhookEventEnum(enums) {
17752
18092
  return enums.find((enumDef) => enumDef.name === "CreateWebhookEndpointEvents") ?? enums.find((enumDef) => isWebhookEventEnumName(enumDef.name) && enumDef.values.length > 0 && enumDef.values.every((value) => typeof value.value === "string" && value.value.includes("."))) ?? null;
17753
18093
  }
@@ -18960,8 +19300,15 @@ const ID_PREFIXES$1 = {
18960
19300
  };
18961
19301
  /**
18962
19302
  * Generate JSON fixture files for test data.
18963
- */
18964
- function generateFixtures$2(spec) {
19303
+ *
19304
+ * Scoped runs only emit a fixture for a model whose per-model fixture FILE will
19305
+ * exist on disk after the run (in-scope, or already present from a prior run —
19306
+ * the fixture path is per-model so the prior manifest records it directly).
19307
+ * This drops brand-new out-of-scope fixtures (the round-trip ADDITION case) and
19308
+ * keeps prior fixtures untouched, mirroring the Rust fixtures fix. `ctx` is
19309
+ * optional so unit tests that assert raw content can call it for a full run.
19310
+ */
19311
+ function generateFixtures$2(spec, ctx) {
18965
19312
  if (spec.models.length === 0) return {
18966
19313
  files: [],
18967
19314
  pathRewrites: /* @__PURE__ */ new Map()
@@ -18971,12 +19318,15 @@ function generateFixtures$2(spec) {
18971
19318
  const files = [];
18972
19319
  const nonPaginatedRefs = collectNonPaginatedResponseModelNames(spec.services);
18973
19320
  const listMetadataNeeded = collectReferencedListMetadataModels(spec.models, nonPaginatedRefs);
19321
+ const fixtureInScope = (relPath, modelName) => !ctx || fileExistsAfterRun(relPath, isModelInScope(modelName, ctx), ctx);
18974
19322
  for (const model of spec.models) {
18975
19323
  if (isListMetadataModel(model) && !listMetadataNeeded.has(model.name)) continue;
18976
19324
  if (isListWrapperModel(model) && !nonPaginatedRefs.has(model.name)) continue;
19325
+ const path = `testdata/${fileName$1(model.name)}.json`;
19326
+ if (!fixtureInScope(path, model.name)) continue;
18977
19327
  const fixture = model.fields.length === 0 ? {} : generateModelFixture$2(model, modelMap, enumMap);
18978
19328
  files.push({
18979
- path: `testdata/${fileName$1(model.name)}.json`,
19329
+ path,
18980
19330
  content: JSON.stringify(fixture, null, 2)
18981
19331
  });
18982
19332
  }
@@ -18990,6 +19340,7 @@ function generateFixtures$2(spec) {
18990
19340
  const path = `testdata/list_${fileName$1(itemModel.name)}.json`;
18991
19341
  if (seenListPaths.has(path)) continue;
18992
19342
  seenListPaths.add(path);
19343
+ if (!fixtureInScope(path, itemModel.name)) continue;
18993
19344
  const listFixture = {
18994
19345
  data: [generateModelFixture$2(itemModel, modelMap, enumMap)],
18995
19346
  list_metadata: {
@@ -19151,7 +19502,7 @@ function generateTests$4(spec, ctx) {
19151
19502
  content: helperLines.join("\n"),
19152
19503
  overwriteExisting: true
19153
19504
  });
19154
- const { files: fixtures, pathRewrites: fixtureRewrites } = generateFixtures$2(spec);
19505
+ const { files: fixtures, pathRewrites: fixtureRewrites } = generateFixtures$2(spec, ctx);
19155
19506
  for (const fixture of fixtures) files.push({
19156
19507
  path: fixture.path,
19157
19508
  content: fixture.content,
@@ -21451,6 +21802,13 @@ function generateClientFile(spec, ctx) {
21451
21802
  //#endregion
21452
21803
  //#region src/dotnet/fixtures.ts
21453
21804
  /**
21805
+ * Prefix the per-model fixture path with the .NET test project layout so the
21806
+ * scoped-run gate can compare it against the prefixed paths recorded in the
21807
+ * prior manifest. Must mirror `TEST_PREFIX` in index.ts; the fixture path
21808
+ * itself (`testdata/<name>.json`) is what `prefixTestPaths` later prepends to.
21809
+ */
21810
+ const TEST_PREFIX$2 = "test/WorkOSTests/";
21811
+ /**
21454
21812
  * Prefix mapping for generating realistic ID fixture values.
21455
21813
  */
21456
21814
  const ID_PREFIXES = {
@@ -21471,7 +21829,7 @@ const ID_PREFIXES = {
21471
21829
  /**
21472
21830
  * Generate JSON fixture files for test data.
21473
21831
  */
21474
- function generateFixtures$1(spec) {
21832
+ function generateFixtures$1(spec, ctx) {
21475
21833
  if (spec.models.length === 0) return [];
21476
21834
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
21477
21835
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
@@ -21480,9 +21838,11 @@ function generateFixtures$1(spec) {
21480
21838
  for (const model of spec.models) {
21481
21839
  if (isListMetadataModel(model)) continue;
21482
21840
  if (isListWrapperModel(model) && !nonPaginatedWrapperRefs.has(model.name)) continue;
21841
+ const fixturePath = `testdata/${fixtureFileName(model.name)}.json`;
21842
+ if (!fileExistsAfterRun(`${TEST_PREFIX$2}${fixturePath}`, isModelInScope(model.name, ctx), ctx)) continue;
21483
21843
  const fixture = model.fields.length === 0 ? {} : generateModelFixture$1(model, modelMap, enumMap);
21484
21844
  files.push({
21485
- path: `testdata/${fixtureFileName(model.name)}.json`,
21845
+ path: fixturePath,
21486
21846
  content: JSON.stringify(fixture, null, 2)
21487
21847
  });
21488
21848
  if (model.fields.some((f) => !f.required || f.type.kind === "nullable") && model.fields.length > 0) {
@@ -21609,7 +21969,7 @@ function generatePrimitiveValue(type, format, name, modelName) {
21609
21969
  */
21610
21970
  function generateTests$3(spec, ctx) {
21611
21971
  const files = [];
21612
- const fixtures = generateFixtures$1(spec);
21972
+ const fixtures = generateFixtures$1(spec, ctx);
21613
21973
  for (const fixture of fixtures) files.push({
21614
21974
  path: fixture.path,
21615
21975
  content: fixture.content,
@@ -22141,6 +22501,26 @@ function prefixTestPaths(files) {
22141
22501
  for (const f of files) f.path = `${TEST_PREFIX$1}${f.path}`;
22142
22502
  return files;
22143
22503
  }
22504
+ /**
22505
+ * Scoped-run gate for a discriminated-union variant referenced by a generated
22506
+ * converter (the event-registry / polymorphic dispatch aggregate). A converter
22507
+ * may only name a variant whose per-model `Entities/<Class>.cs` file will exist
22508
+ * on disk after the run — otherwise the `new <Class>()` / `ToObject<<Class>>`
22509
+ * arm references a type whose file is never emitted (CS0246). The set that
22510
+ * exists after the run is: in-scope variants (emitted this run) ∪ variants
22511
+ * already on disk from a prior manifest (scoped runs never prune), so a
22512
+ * renamed/removed-but-still-on-disk variant is RETAINED while a brand-new
22513
+ * out-of-scope variant is EXCLUDED. A full run includes everything.
22514
+ *
22515
+ * The variant name is resolved through the model-alias map first, exactly as
22516
+ * the converter emits it (`modelClassName(resolveModelName(name))`), and the
22517
+ * candidate path carries the `src/WorkOS.net/` prefix so it matches the
22518
+ * prefixed paths recorded in `priorTargetManifestPaths`.
22519
+ */
22520
+ function variantFileExistsAfterRun(variantModelName, ctx) {
22521
+ const canonical = resolveModelName(variantModelName);
22522
+ return fileExistsAfterRun(`${SRC_PREFIX}Entities/${modelClassName(canonical)}.cs`, isModelInScope(canonical, ctx), ctx);
22523
+ }
22144
22524
  const dotnetEmitter = {
22145
22525
  language: "dotnet",
22146
22526
  generateModels(models, ctx) {
@@ -22200,6 +22580,7 @@ const dotnetEmitter = {
22200
22580
  lines.push(" switch (discriminatorValue)");
22201
22581
  lines.push(" {");
22202
22582
  for (const [value, modelName] of Object.entries(disc.mapping)) {
22583
+ if (!variantFileExistsAfterRun(modelName, c)) continue;
22203
22584
  const csName = modelClassName(resolveModelName(modelName));
22204
22585
  lines.push(` case "${value}": return jObject.ToObject<${csName}>(serializer);`);
22205
22586
  }
@@ -22220,6 +22601,7 @@ const dotnetEmitter = {
22220
22601
  });
22221
22602
  }
22222
22603
  for (const [baseName, disc] of modelDiscriminators) {
22604
+ if (!variantFileExistsAfterRun(baseName, c)) continue;
22223
22605
  const baseClass = modelClassName(baseName);
22224
22606
  const converterName = `${baseClass}DiscriminatorConverter`;
22225
22607
  const lines = [];
@@ -22248,6 +22630,7 @@ const dotnetEmitter = {
22248
22630
  lines.push(" switch (discriminatorValue)");
22249
22631
  lines.push(" {");
22250
22632
  for (const [value, variantModelName] of Object.entries(disc.mapping)) {
22633
+ if (!variantFileExistsAfterRun(variantModelName, c)) continue;
22251
22634
  const csName = modelClassName(variantModelName);
22252
22635
  lines.push(` case "${value}": target = new ${csName}(); break;`);
22253
22636
  }
@@ -22805,6 +23188,16 @@ const KOTLIN_SRC_PREFIX$2 = "src/main/kotlin/";
22805
23188
  const MODELS_PACKAGE = "com.workos.models";
22806
23189
  const MODELS_DIR = "com/workos/models";
22807
23190
  /**
23191
+ * The relative path (target-root-relative, matching the prior manifest) of the
23192
+ * per-model `.kt` FILE the emitter writes for a model. The aggregate gate
23193
+ * ({@link fileExistsAfterRun}) checks this exact path against the freshly-emitted
23194
+ * in-scope set and the prior manifest. Must stay in sync with the path used in
23195
+ * {@link emitDataClass} / {@link emitSealedUnion} / the typealias branch.
23196
+ */
23197
+ function modelFilePath$1(modelName) {
23198
+ return `${KOTLIN_SRC_PREFIX$2}${MODELS_DIR}/${className$1(modelName)}.kt`;
23199
+ }
23200
+ /**
22808
23201
  * Some specs leave string fields without `format: date-time` even though the
22809
23202
  * description (or the example) makes clear they carry an ISO-8601 timestamp.
22810
23203
  * Detect that here so we can promote the type to `OffsetDateTime` in the
@@ -22899,7 +23292,7 @@ function generateModels$2(models, ctx) {
22899
23292
  const typeName = className$1(model.name);
22900
23293
  const modelInScope = isModelInScope(model.name, ctx);
22901
23294
  if (model.fields.length === 0 && discriminatedUnions.has(typeName)) {
22902
- if (modelInScope) files.push(emitSealedUnion(typeName, discriminatedUnions.get(typeName)));
23295
+ if (modelInScope) files.push(emitSealedUnion(typeName, discriminatedUnions.get(typeName), ctx));
22903
23296
  continue;
22904
23297
  }
22905
23298
  const canonical = aliasOf.get(model.name);
@@ -22927,6 +23320,7 @@ function generateModels$2(models, ctx) {
22927
23320
  if (skipAsListWrapper(model) || skipAsListMetadata(model)) continue;
22928
23321
  if (aliasOf.has(model.name)) continue;
22929
23322
  if (!isEventEnvelopeModel(model)) continue;
23323
+ if (!fileExistsAfterRun(modelFilePath$1(model.name), isModelInScope(model.name, ctx), ctx)) continue;
22930
23324
  const eventField = model.fields.find((f) => f.name === "event");
22931
23325
  if (eventField && eventField.type.kind === "literal" && typeof eventField.type.value === "string") eventMapping.push({
22932
23326
  wireValue: eventField.type.value,
@@ -22984,15 +23378,15 @@ function emitDataClass(model) {
22984
23378
  overwriteExisting: true
22985
23379
  };
22986
23380
  }
22987
- function emitSealedUnion(typeName, disc) {
23381
+ function emitSealedUnion(typeName, disc, ctx) {
22988
23382
  const lines = [];
22989
23383
  lines.push(`package ${MODELS_PACKAGE}`);
22990
23384
  lines.push("");
22991
23385
  lines.push("import com.fasterxml.jackson.annotation.JsonSubTypes");
22992
23386
  lines.push("import com.fasterxml.jackson.annotation.JsonTypeInfo");
22993
23387
  lines.push("");
22994
- const exampleVariantWire = Object.keys(disc.mapping)[0];
22995
- const exampleVariantType = exampleVariantWire ? className$1(disc.mapping[exampleVariantWire]) : null;
23388
+ const entries = Object.entries(disc.mapping).filter(([, modelName]) => fileExistsAfterRun(modelFilePath$1(modelName), isModelInScope(modelName, ctx), ctx));
23389
+ const exampleVariantType = entries.length > 0 ? className$1(entries[0][1]) : null;
22996
23390
  lines.push("/**");
22997
23391
  lines.push(` * Discriminated union over ${typeName} variants. Selected by \`${disc.property}\`.`);
22998
23392
  if (exampleVariantType) {
@@ -23021,7 +23415,6 @@ function emitSealedUnion(typeName, disc) {
23021
23415
  lines.push(" visible = true");
23022
23416
  lines.push(")");
23023
23417
  lines.push("@JsonSubTypes(");
23024
- const entries = Object.entries(disc.mapping);
23025
23418
  for (let i = 0; i < entries.length; i++) {
23026
23419
  const [wireValue, modelName] = entries[i];
23027
23420
  const variantType = className$1(modelName);
@@ -24478,6 +24871,14 @@ function deduplicateByMount(services, ctx) {
24478
24871
  //#endregion
24479
24872
  //#region src/kotlin/tests.ts
24480
24873
  const TEST_PREFIX = "src/test/kotlin/";
24874
+ const MODELS_FILE_PREFIX = "src/main/kotlin/com/workos/models/";
24875
+ const ENUMS_FILE_PREFIX = "src/main/kotlin/com/workos/types/";
24876
+ function modelFilePath(modelName) {
24877
+ return `${MODELS_FILE_PREFIX}${className$1(modelName)}.kt`;
24878
+ }
24879
+ function enumFilePath(enumName) {
24880
+ return `${ENUMS_FILE_PREFIX}${className$1(enumName)}.kt`;
24881
+ }
24481
24882
  /**
24482
24883
  * Mirror the ISO-8601 hint promotion the resource/model emitters use so tests
24483
24884
  * synthesize values whose Kotlin type matches the generated method signature.
@@ -25228,6 +25629,7 @@ function generateModelRoundTripTest$1(spec, ctx) {
25228
25629
  for (const m of spec.models) {
25229
25630
  if (isListWrapperModel(m) || isListMetadataModel(m)) continue;
25230
25631
  if (m.fields.length === 0) continue;
25632
+ if (!fileExistsAfterRun(modelFilePath(m.name), isModelInScope(m.name, ctx), ctx)) continue;
25231
25633
  const cls = className$1(m.name);
25232
25634
  if (seenModelClassNames.has(cls)) continue;
25233
25635
  seenModelClassNames.add(cls);
@@ -25276,10 +25678,11 @@ function generateModelRoundTripTest$1(spec, ctx) {
25276
25678
  */
25277
25679
  const MAX_ENUM_FORWARD_COMPAT = 15;
25278
25680
  function generateForwardCompatTest(spec, ctx) {
25279
- const enumTargets = spec.enums.filter((e) => e.values.length > 0).slice(0, MAX_ENUM_FORWARD_COMPAT);
25681
+ const enumTargets = spec.enums.filter((e) => e.values.length > 0).filter((e) => fileExistsAfterRun(enumFilePath(e.name), isEnumInScope(e.name, ctx), ctx)).slice(0, MAX_ENUM_FORWARD_COMPAT);
25280
25682
  const modelTarget = spec.models.find((m) => {
25281
25683
  if (isListWrapperModel(m) || isListMetadataModel(m)) return false;
25282
25684
  if (m.fields.length === 0) return false;
25685
+ if (!fileExistsAfterRun(modelFilePath(m.name), isModelInScope(m.name, ctx), ctx)) return false;
25283
25686
  return synthJsonForModelName(m.name, ctx, /* @__PURE__ */ new Set()) !== null;
25284
25687
  });
25285
25688
  if (enumTargets.length === 0 && !modelTarget) return null;
@@ -27221,15 +27624,26 @@ function generateTests$1(spec, ctx) {
27221
27624
  overwriteExisting: true
27222
27625
  });
27223
27626
  }
27224
- files.push(generateModelRoundTripTest(spec));
27627
+ files.push(generateModelRoundTripTest(spec, ctx));
27225
27628
  return files;
27226
27629
  }
27227
27630
  /**
27228
27631
  * Emit test/workos/model_round_trip_test.rb that round-trips every non-wrapper
27229
27632
  * model through `.new(json)` and `.to_json`, asserting the result is a Hash and
27230
27633
  * that required fields appear with the seeded values.
27231
- */
27232
- function generateModelRoundTripTest(spec) {
27634
+ *
27635
+ * This is a whole-suite AGGREGATE: it references `WorkOS::<Class>` for every
27636
+ * model. Under a scoped (`--services`) run only the selected services' per-model
27637
+ * `.rb` files are (re)written, so a test referencing a brand-new out-of-scope
27638
+ * model whose file was never emitted would raise `NameError: uninitialized
27639
+ * constant`. Each per-model test is therefore gated by {@link fileExistsAfterRun}
27640
+ * on the SAME path `models.ts` writes (`lib/workos/<dir>/<file>.rb`): emit only
27641
+ * when that file will exist on disk after the run — in-scope (emitted this run)
27642
+ * OR already present from a prior run (`priorTargetManifestPaths`). Brand-new
27643
+ * out-of-scope models are excluded; renamed/removed-but-on-disk models are
27644
+ * retained. A full run (scoping inert) keeps every model.
27645
+ */
27646
+ function generateModelRoundTripTest(spec, ctx) {
27233
27647
  const lines = [];
27234
27648
  lines.push(`require 'test_helper'`);
27235
27649
  lines.push("");
@@ -27237,7 +27651,15 @@ function generateModelRoundTripTest(spec) {
27237
27651
  const models = spec.models.filter((m) => !isListWrapperModel(m) && !isListMetadataModel(m));
27238
27652
  const enumNames = new Set(spec.enums.map((e) => e.name));
27239
27653
  const emitted = /* @__PURE__ */ new Set();
27654
+ const modelToService = assignModelsToServices(models, ctx.spec.services, ctx.modelHints);
27655
+ const mountDirMap = buildMountDirMap(ctx);
27656
+ const dirFor = (modelName) => {
27657
+ const service = modelToService.get(modelName);
27658
+ if (!service) return classifyUnassignedModel(modelName);
27659
+ return mountDirMap.get(service) ?? classifyUnassignedModel(modelName);
27660
+ };
27240
27661
  for (const model of models) {
27662
+ if (!fileExistsAfterRun(`lib/workos/${dirFor(model.name)}/${fileName(model.name)}.rb`, isModelInScope(model.name, ctx), ctx)) continue;
27241
27663
  const fileBase = fileName(model.name);
27242
27664
  if (emitted.has(fileBase)) continue;
27243
27665
  emitted.add(fileBase);
@@ -28224,14 +28646,19 @@ function generateModels(models, ctx, registry) {
28224
28646
  const mod = moduleName(model.name);
28225
28647
  if (seen.has(mod)) continue;
28226
28648
  seen.add(mod);
28227
- moduleNames.push(mod);
28649
+ const inScope = isModelInScope(model.name, ctx);
28228
28650
  const path = ctx.overlayLookup?.fileBySymbol?.get(model.name) ?? `src/models/${mod}.rs`;
28229
28651
  const content = renderModel(model, registry, taggedVariantFields.get(model.name));
28230
- if (isModelInScope(model.name, ctx)) files.push({
28652
+ if (inScope) files.push({
28231
28653
  path,
28232
28654
  content,
28233
28655
  overwriteExisting: true
28234
28656
  });
28657
+ if (fileExistsAfterRun(path, inScope, ctx)) moduleNames.push(mod);
28658
+ }
28659
+ for (const base of priorManifestBasenames(ctx, "src/models", ".rs", new Set([UNIONS_MODULE, "mod"]))) if (!seen.has(base)) {
28660
+ seen.add(base);
28661
+ moduleNames.push(base);
28235
28662
  }
28236
28663
  moduleNames.push(UNIONS_MODULE);
28237
28664
  files.push({
@@ -28397,13 +28824,18 @@ function generateEnums(enums, ctx) {
28397
28824
  const mod = moduleName(e.name);
28398
28825
  if (seen.has(mod)) continue;
28399
28826
  seen.add(mod);
28400
- moduleNames.push(mod);
28401
- if (!isEnumInScope(e.name, ctx)) continue;
28402
- files.push({
28403
- path: `src/enums/${mod}.rs`,
28827
+ const inScope = isEnumInScope(e.name, ctx);
28828
+ const path = `src/enums/${mod}.rs`;
28829
+ if (inScope) files.push({
28830
+ path,
28404
28831
  content: renderEnum(e),
28405
28832
  overwriteExisting: true
28406
28833
  });
28834
+ if (fileExistsAfterRun(path, inScope, ctx)) moduleNames.push(mod);
28835
+ }
28836
+ for (const base of priorManifestBasenames(ctx, "src/enums", ".rs", new Set(["mod"]))) if (!seen.has(base)) {
28837
+ seen.add(base);
28838
+ moduleNames.push(base);
28407
28839
  }
28408
28840
  files.push({
28409
28841
  path: "src/enums/mod.rs",
@@ -29543,8 +29975,14 @@ function renderResourcesApi(ctx) {
29543
29975
  /**
29544
29976
  * Generate JSON test fixture files under `tests/fixtures/`. The Rust tests
29545
29977
  * pull these in via `include_str!` so no I/O is required at test time.
29978
+ *
29979
+ * Scoped runs only emit a fixture for a model whose file will exist on disk
29980
+ * after the run (in-scope, or already present from a prior run). Emitting a
29981
+ * fixture for a brand-new out-of-scope model would add a stray file for a model
29982
+ * the SDK can't even reference yet; in-scope tests only `include_str!` fixtures
29983
+ * for in-scope models, so gating here is safe.
29546
29984
  */
29547
- function generateFixtures(spec) {
29985
+ function generateFixtures(spec, ctx) {
29548
29986
  const files = [];
29549
29987
  const modelMap = new Map(spec.models.map((m) => [m.name, m]));
29550
29988
  const enumMap = new Map(spec.enums.map((e) => [e.name, e]));
@@ -29553,8 +29991,9 @@ function generateFixtures(spec) {
29553
29991
  if (seen.has(model.name)) continue;
29554
29992
  if (model.fields.length === 0) continue;
29555
29993
  seen.add(model.name);
29556
- const fixture = generateModelFixture(model, modelMap, enumMap, /* @__PURE__ */ new Set());
29557
29994
  const path = `tests/fixtures/${moduleName(model.name)}.json`;
29995
+ if (!fileExistsAfterRun(path, isModelInScope(model.name, ctx), ctx)) continue;
29996
+ const fixture = generateModelFixture(model, modelMap, enumMap, /* @__PURE__ */ new Set());
29558
29997
  files.push({
29559
29998
  path,
29560
29999
  content: JSON.stringify(fixture, null, 2) + "\n",
@@ -29701,7 +30140,7 @@ function matchPrimitive(value, primitive) {
29701
30140
  */
29702
30141
  function generateTests(spec, ctx) {
29703
30142
  const files = [];
29704
- files.push(...generateFixtures(spec));
30143
+ files.push(...generateFixtures(spec, ctx));
29705
30144
  files.push({
29706
30145
  path: "tests/common/mod.rs",
29707
30146
  content: renderCommon(ctx),
@@ -30339,4 +30778,4 @@ const workosEmittersPlugin = {
30339
30778
  //#endregion
30340
30779
  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 };
30341
30780
 
30342
- //# sourceMappingURL=plugin-BXDPA9pJ.mjs.map
30781
+ //# sourceMappingURL=plugin-DXIciTnN.mjs.map