@workos/oagen 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cli/index.d.mts +2 -0
  2. package/dist/cli/index.mjs +6233 -0
  3. package/dist/cli/index.mjs.map +1 -0
  4. package/dist/compat/{index.d.ts → index.d.mts} +43 -114
  5. package/dist/compat/index.d.mts.map +1 -0
  6. package/dist/compat/index.mjs +4562 -0
  7. package/dist/compat/index.mjs.map +1 -0
  8. package/dist/diff-Cn6S74vV.mjs +2432 -0
  9. package/dist/diff-Cn6S74vV.mjs.map +1 -0
  10. package/dist/differ-B6472Yef.mjs +1100 -0
  11. package/dist/differ-B6472Yef.mjs.map +1 -0
  12. package/dist/index.d.mts +324 -0
  13. package/dist/index.d.mts.map +1 -0
  14. package/dist/index.mjs +1553 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/scripts/smoke/baseline.d.mts +1 -0
  17. package/dist/scripts/smoke/baseline.mjs +1951 -0
  18. package/dist/scripts/smoke/baseline.mjs.map +1 -0
  19. package/dist/scripts/smoke/sdk-test.d.mts +1 -0
  20. package/dist/scripts/smoke/sdk-test.mjs +111 -0
  21. package/dist/scripts/smoke/sdk-test.mjs.map +1 -0
  22. package/dist/smoke.d.mts +331 -0
  23. package/dist/smoke.d.mts.map +1 -0
  24. package/dist/smoke.mjs +2119 -0
  25. package/dist/smoke.mjs.map +1 -0
  26. package/dist/tree-sitter-DFRXfWg9.mjs +394 -0
  27. package/dist/tree-sitter-DFRXfWg9.mjs.map +1 -0
  28. package/dist/types-O25qZxMl.d.mts +397 -0
  29. package/dist/types-O25qZxMl.d.mts.map +1 -0
  30. package/dist/verify/index.d.mts +82 -0
  31. package/dist/verify/index.d.mts.map +1 -0
  32. package/dist/verify/index.mjs +327 -0
  33. package/dist/verify/index.mjs.map +1 -0
  34. package/package.json +19 -16
  35. package/dist/chunk-2JXFTDTO.js +0 -1669
  36. package/dist/chunk-CIX5NOBF.js +0 -263
  37. package/dist/chunk-IJ5E5AE7.js +0 -1040
  38. package/dist/cli/index.js +0 -5322
  39. package/dist/compat/index.js +0 -4491
  40. package/dist/index.d.ts +0 -277
  41. package/dist/index.js +0 -1565
  42. package/dist/scripts/smoke/baseline.js +0 -1813
  43. package/dist/scripts/smoke/sdk-test.js +0 -93
  44. package/dist/smoke.js +0 -2025
  45. package/dist/types-B5t9O9Wm.d.ts +0 -367
  46. package/dist/verify/index.d.ts +0 -76
  47. package/dist/verify/index.js +0 -336
@@ -0,0 +1,2432 @@
1
+ import { A as walkTypeRef, C as assertNever, f as toSnakeCase, i as normalizeJsExtension, t as safeParse, x as RegistryError } from "./tree-sitter-DFRXfWg9.mjs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path$1 from "node:path";
4
+ import path from "node:path";
5
+ import Parser from "tree-sitter";
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ //#region src/engine/merge-adapters/url-fingerprint.ts
9
+ /**
10
+ * Extract a URL path fingerprint from a method body using tree-sitter AST traversal.
11
+ *
12
+ * Walks the AST subtree of the member node looking for the first string literal
13
+ * that starts with '/'. Normalizes interpolation expressions to '{}' and format
14
+ * specifiers (%s, %d, etc.) to '{}' for cross-language comparison.
15
+ *
16
+ * @returns A normalized URL fingerprint like "/organizations/{}" or undefined if none found.
17
+ */
18
+ function extractUrlFingerprint(memberNode, config) {
19
+ const stringTypes = new Set(config.stringNodeTypes);
20
+ const contentTypes = new Set(config.contentNodeTypes);
21
+ const interpolationTypes = new Set(config.interpolationNodeTypes);
22
+ const formatFns = config.formatFunctionNames ? new Set(config.formatFunctionNames) : null;
23
+ const stack = [memberNode];
24
+ while (stack.length > 0) {
25
+ const node = stack.pop();
26
+ if (stringTypes.has(node.type)) {
27
+ const fp = resolveFingerprint(node, contentTypes, interpolationTypes);
28
+ if (fp?.startsWith("/")) return fp;
29
+ }
30
+ if (formatFns && isFormatCall(node, formatFns)) {
31
+ const fp = extractFromFormatCall(node, stringTypes, contentTypes, interpolationTypes);
32
+ if (fp?.startsWith("/")) return fp;
33
+ }
34
+ for (let i = node.childCount - 1; i >= 0; i--) {
35
+ const child = node.child(i);
36
+ if (child) stack.push(child);
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * Resolve the text content of a string node, normalizing interpolations to '{}'.
42
+ */
43
+ function resolveFingerprint(stringNode, contentTypes, interpolationTypes) {
44
+ if (stringNode.namedChildCount === 0) return normalizeFormatSpecifiers(stripQuotes(stringNode.text));
45
+ let result = "";
46
+ for (let i = 0; i < stringNode.childCount; i++) {
47
+ const child = stringNode.child(i);
48
+ if (contentTypes.has(child.type)) result += child.text;
49
+ else if (interpolationTypes.has(child.type)) result += "{}";
50
+ }
51
+ return result ? normalizeFormatSpecifiers(result) : normalizeFormatSpecifiers(stripQuotes(stringNode.text));
52
+ }
53
+ /** Check if a node is a call to one of the format functions (e.g., fmt.Sprintf, format!). */
54
+ function isFormatCall(node, formatFns) {
55
+ if (node.type === "macro_invocation") {
56
+ const macroName = node.childForFieldName("macro")?.text;
57
+ return !!(macroName && formatFns.has(macroName.replace(/!$/, "")));
58
+ }
59
+ if (node.type !== "call_expression") return false;
60
+ const fnNode = node.childForFieldName("function");
61
+ if (!fnNode) return false;
62
+ if (formatFns.has(fnNode.text)) return true;
63
+ if (fnNode.type === "selector_expression" || fnNode.type === "member_expression") {
64
+ const prop = fnNode.childForFieldName("field") ?? fnNode.childForFieldName("property");
65
+ if (prop && formatFns.has(prop.text)) return true;
66
+ }
67
+ return false;
68
+ }
69
+ /** Extract the first string argument from a format function call. */
70
+ function extractFromFormatCall(callNode, stringTypes, contentTypes, interpolationTypes) {
71
+ const args = callNode.childForFieldName("arguments");
72
+ if (!args) return void 0;
73
+ for (let i = 0; i < args.childCount; i++) {
74
+ const arg = args.child(i);
75
+ if (stringTypes.has(arg.type)) return resolveFingerprint(arg, contentTypes, interpolationTypes);
76
+ }
77
+ }
78
+ /** Strip surrounding quote characters from a string literal. */
79
+ function stripQuotes(text) {
80
+ const match = text.match(/^(?:f|r|b|@|\$)?(?:"""([\s\S]*)"""|'''([\s\S]*)'''|"(.*)"|'(.*)'|`(.*)`)$/s);
81
+ if (match) return match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5] ?? text;
82
+ return text;
83
+ }
84
+ /** Replace printf-style format specifiers (%s, %d, %v, %f) with {}. */
85
+ function normalizeFormatSpecifiers(s) {
86
+ return s.replace(/%[sdvf]/g, "{}");
87
+ }
88
+ //#endregion
89
+ //#region src/engine/merge-adapters/dotnet.ts
90
+ const DOTNET_URL_FINGERPRINT_CONFIG = {
91
+ stringNodeTypes: ["string_literal", "interpolated_string_expression"],
92
+ contentNodeTypes: ["string_content", "string_literal_content"],
93
+ interpolationNodeTypes: ["interpolation"]
94
+ };
95
+ const DECLARATION_TYPES = new Set([
96
+ "class_declaration",
97
+ "struct_declaration",
98
+ "interface_declaration",
99
+ "enum_declaration",
100
+ "record_declaration"
101
+ ]);
102
+ function extractDeclarationName$1(node) {
103
+ if (DECLARATION_TYPES.has(node.type)) return (node.childForFieldName("name") ?? node.children.find((c) => c.type === "identifier"))?.text ?? null;
104
+ if (node.type === "namespace_declaration" || node.type === "file_scoped_namespace_declaration") {
105
+ const nameNode = node.children.find((c) => c.type === "qualified_name") ?? node.children.find((c) => c.type === "identifier");
106
+ return nameNode ? `__namespace:${nameNode.text}` : null;
107
+ }
108
+ return null;
109
+ }
110
+ function collectPrecedingXmlDocComments(children, index, source) {
111
+ let lastIdx = -1;
112
+ let firstIdx = -1;
113
+ for (let k = index - 1; k >= 0; k--) {
114
+ const prev = children[k];
115
+ if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
116
+ if (lastIdx === -1) lastIdx = k;
117
+ firstIdx = k;
118
+ } else break;
119
+ else break;
120
+ }
121
+ if (firstIdx === -1) return null;
122
+ const first = children[firstIdx];
123
+ const last = children[lastIdx];
124
+ return {
125
+ text: source.slice(first.startIndex, last.endIndex),
126
+ startIndex: first.startIndex,
127
+ endIndex: last.endIndex
128
+ };
129
+ }
130
+ /**
131
+ * Collect declarations from a list of sibling nodes (root level or namespace body).
132
+ */
133
+ function collectDeclarations(children, source, statements, imports, importAnchors, importInsertionAnchor) {
134
+ for (const child of children) {
135
+ if (child.type === "comment") continue;
136
+ if (child.type === "using_directive") {
137
+ const text = source.slice(child.startIndex, child.endIndex);
138
+ imports.push({
139
+ key: text.trim(),
140
+ text
141
+ });
142
+ importAnchors.push(text);
143
+ continue;
144
+ }
145
+ if (child.type === "file_scoped_namespace_declaration") {
146
+ importInsertionAnchor.value = source.slice(child.startIndex, child.endIndex);
147
+ continue;
148
+ }
149
+ if (child.type === "namespace_declaration") {
150
+ importInsertionAnchor.value = source.slice(child.startIndex, child.endIndex);
151
+ const body = child.children.find((c) => c.type === "declaration_list");
152
+ if (body) collectDeclarations(body.namedChildren, source, statements, imports, importAnchors, importInsertionAnchor);
153
+ continue;
154
+ }
155
+ const name = extractDeclarationName$1(child);
156
+ statements.push({
157
+ key: name,
158
+ kind: name ? "declaration" : "other",
159
+ text: source.slice(child.startIndex, child.endIndex)
160
+ });
161
+ }
162
+ }
163
+ const dotnetMergeAdapter = {
164
+ language: "dotnet",
165
+ grammarModule: "tree-sitter-c-sharp",
166
+ testFilePatterns: [/Tests?\.cs$/],
167
+ urlFingerprintConfig: DOTNET_URL_FINGERPRINT_CONFIG,
168
+ parseStatements(tree, source) {
169
+ const imports = [];
170
+ const importAnchors = [];
171
+ const statements = [];
172
+ const importInsertionAnchor = {};
173
+ collectDeclarations(tree.rootNode.children, source, statements, imports, importAnchors, importInsertionAnchor);
174
+ return {
175
+ imports,
176
+ importAnchors,
177
+ importInsertionAnchor: importInsertionAnchor.value,
178
+ statements
179
+ };
180
+ },
181
+ renderImports(imports) {
182
+ return imports.map((entry) => entry.text);
183
+ },
184
+ extractDocstrings(tree, source) {
185
+ const result = /* @__PURE__ */ new Map();
186
+ function processChildren(children) {
187
+ for (let i = 0; i < children.length; i++) {
188
+ const child = children[i];
189
+ if (child.type === "namespace_declaration") {
190
+ const body = child.children.find((c) => c.type === "declaration_list");
191
+ if (body) processChildren(body.children);
192
+ continue;
193
+ }
194
+ const name = extractDeclarationName$1(child);
195
+ if (!name || name.startsWith("__namespace:")) continue;
196
+ const docstring = collectPrecedingXmlDocComments(children, i, source);
197
+ const members = /* @__PURE__ */ new Map();
198
+ const body = child.children.find((c) => c.type === "declaration_list");
199
+ if (body) {
200
+ const bodyChildren = body.children;
201
+ for (let j = 0; j < bodyChildren.length; j++) {
202
+ const member = bodyChildren[j];
203
+ if (member.type !== "method_declaration" && member.type !== "property_declaration") continue;
204
+ const identifiers = member.children.filter((c) => c.type === "identifier");
205
+ const memberName = member.type === "method_declaration" ? identifiers.length >= 2 ? identifiers[1].text : identifiers[0]?.text : identifiers[0]?.text;
206
+ if (!memberName) continue;
207
+ const memberDoc = collectPrecedingXmlDocComments(bodyChildren, j, source);
208
+ members.set(memberName, {
209
+ docstring: memberDoc,
210
+ declStartIndex: member.startIndex,
211
+ declColumn: member.startPosition.column,
212
+ urlFingerprint: extractUrlFingerprint(member, DOTNET_URL_FINGERPRINT_CONFIG)
213
+ });
214
+ }
215
+ }
216
+ result.set(name, {
217
+ docstring,
218
+ declStartIndex: child.startIndex,
219
+ declColumn: child.startPosition.column,
220
+ members
221
+ });
222
+ }
223
+ }
224
+ processChildren(tree.rootNode.children);
225
+ return result;
226
+ }
227
+ };
228
+ //#endregion
229
+ //#region src/engine/merge-adapters/elixir.ts
230
+ const ELIXIR_URL_FINGERPRINT_CONFIG = {
231
+ stringNodeTypes: ["string"],
232
+ contentNodeTypes: ["quoted_content"],
233
+ interpolationNodeTypes: ["interpolation"]
234
+ };
235
+ function isDefmodule(node) {
236
+ return node.type === "call" && node.children[0]?.text === "defmodule";
237
+ }
238
+ function getModuleName(node) {
239
+ if (!isDefmodule(node)) return null;
240
+ const args = node.children.find((c) => c.type === "arguments");
241
+ if (!args) return null;
242
+ return args.children.find((c) => c.type === "alias")?.text ?? null;
243
+ }
244
+ function getDoBlock(node) {
245
+ return node.children.find((c) => c.type === "do_block") ?? null;
246
+ }
247
+ /** Check if a node is an @moduledoc or @doc attribute. */
248
+ function isDocAttribute(node) {
249
+ if (node.type !== "unary_operator") return null;
250
+ const call = node.children.find((c) => c.type === "call");
251
+ if (!call) return null;
252
+ const id = call.children.find((c) => c.type === "identifier");
253
+ if (id?.text === "moduledoc" || id?.text === "doc") return id.text;
254
+ return null;
255
+ }
256
+ /** Check if a node is a def/defp call. */
257
+ function getDefName(node) {
258
+ if (node.type !== "call") return null;
259
+ const id = node.children[0];
260
+ if (!id || id.text !== "def" && id.text !== "defp") return null;
261
+ const args = node.children.find((c) => c.type === "arguments");
262
+ if (!args) return null;
263
+ for (const arg of args.namedChildren) {
264
+ if (arg.type === "identifier") return arg.text;
265
+ if (arg.type === "call") return arg.children.find((c) => c.type === "identifier")?.text ?? null;
266
+ }
267
+ return null;
268
+ }
269
+ /** Check if a node is an alias/import/require/use call. */
270
+ function isImportLike(node) {
271
+ if (node.type !== "call") return false;
272
+ const id = node.children[0];
273
+ return id?.type === "identifier" && [
274
+ "alias",
275
+ "import",
276
+ "require",
277
+ "use"
278
+ ].includes(id.text);
279
+ }
280
+ const elixirMergeAdapter = {
281
+ language: "elixir",
282
+ grammarModule: "tree-sitter-elixir",
283
+ testFilePatterns: [/_test\.exs$/],
284
+ urlFingerprintConfig: ELIXIR_URL_FINGERPRINT_CONFIG,
285
+ parseStatements(tree, source) {
286
+ const imports = [];
287
+ const importAnchors = [];
288
+ const statements = [];
289
+ for (const child of tree.rootNode.children) {
290
+ if (child.type === "comment") continue;
291
+ if (isImportLike(child)) {
292
+ const text = source.slice(child.startIndex, child.endIndex);
293
+ imports.push({
294
+ key: text.trim(),
295
+ text
296
+ });
297
+ importAnchors.push(text);
298
+ continue;
299
+ }
300
+ const moduleName = getModuleName(child);
301
+ statements.push({
302
+ key: moduleName,
303
+ kind: moduleName ? "declaration" : "other",
304
+ text: source.slice(child.startIndex, child.endIndex)
305
+ });
306
+ }
307
+ return {
308
+ imports,
309
+ importAnchors,
310
+ statements
311
+ };
312
+ },
313
+ renderImports(imports) {
314
+ return imports.map((entry) => entry.text);
315
+ },
316
+ extractDocstrings(tree, source) {
317
+ const result = /* @__PURE__ */ new Map();
318
+ const rootChildren = tree.rootNode.children;
319
+ for (let i = 0; i < rootChildren.length; i++) {
320
+ const child = rootChildren[i];
321
+ const moduleName = getModuleName(child);
322
+ if (!moduleName) continue;
323
+ let docstring = null;
324
+ const doBlock = getDoBlock(child);
325
+ const members = /* @__PURE__ */ new Map();
326
+ if (doBlock) {
327
+ const bodyChildren = doBlock.children;
328
+ for (const bodyChild of bodyChildren) if (isDocAttribute(bodyChild) === "moduledoc") {
329
+ docstring = {
330
+ text: source.slice(bodyChild.startIndex, bodyChild.endIndex),
331
+ startIndex: bodyChild.startIndex,
332
+ endIndex: bodyChild.endIndex
333
+ };
334
+ break;
335
+ }
336
+ for (let j = 0; j < bodyChildren.length; j++) {
337
+ const bodyChild = bodyChildren[j];
338
+ const defName = getDefName(bodyChild);
339
+ if (!defName) continue;
340
+ let memberDoc = null;
341
+ for (let k = j - 1; k >= 0; k--) {
342
+ const prev = bodyChildren[k];
343
+ if (isDocAttribute(prev) === "doc") {
344
+ memberDoc = {
345
+ text: source.slice(prev.startIndex, prev.endIndex),
346
+ startIndex: prev.startIndex,
347
+ endIndex: prev.endIndex
348
+ };
349
+ break;
350
+ }
351
+ if (prev.type === "comment") continue;
352
+ break;
353
+ }
354
+ members.set(defName, {
355
+ docstring: memberDoc,
356
+ declStartIndex: bodyChild.startIndex,
357
+ declColumn: bodyChild.startPosition.column,
358
+ urlFingerprint: extractUrlFingerprint(bodyChild, ELIXIR_URL_FINGERPRINT_CONFIG)
359
+ });
360
+ }
361
+ }
362
+ result.set(moduleName, {
363
+ docstring,
364
+ declStartIndex: child.startIndex,
365
+ declColumn: child.startPosition.column,
366
+ members
367
+ });
368
+ }
369
+ return result;
370
+ }
371
+ };
372
+ //#endregion
373
+ //#region src/engine/merge-adapters/go.ts
374
+ const GO_URL_FINGERPRINT_CONFIG = {
375
+ stringNodeTypes: ["interpreted_string_literal"],
376
+ contentNodeTypes: ["interpreted_string_literal_content"],
377
+ interpolationNodeTypes: [],
378
+ formatFunctionNames: ["Sprintf"]
379
+ };
380
+ function lastIdentifier(text) {
381
+ const matches = [...text.matchAll(/[A-Za-z_][A-Za-z0-9_]*/g)];
382
+ return matches.length > 0 ? matches[matches.length - 1][0] : null;
383
+ }
384
+ function receiverKey(node) {
385
+ const receiver = node.childForFieldName("receiver");
386
+ if (!receiver) return null;
387
+ return lastIdentifier(receiver.text);
388
+ }
389
+ function declarationKey$1(node) {
390
+ switch (node.type) {
391
+ case "package_clause": {
392
+ const pkg = lastIdentifier(node.text);
393
+ return pkg ? `__package:${pkg}` : "__package:";
394
+ }
395
+ case "type_declaration": return ((node.firstNamedChild?.type === "type_spec" ? node.firstNamedChild : node.namedChildren[0])?.childForFieldName("name"))?.text ?? null;
396
+ case "function_declaration": return node.childForFieldName("name")?.text ?? null;
397
+ case "method_declaration": {
398
+ const name = node.childForFieldName("name")?.text ?? null;
399
+ const receiver = receiverKey(node);
400
+ return name && receiver ? `method:${receiver}.${name}` : name;
401
+ }
402
+ case "const_declaration":
403
+ case "var_declaration": return node.text.split(/\s+/).slice(0, 3).join(" ");
404
+ default: return null;
405
+ }
406
+ }
407
+ function importEntries(node, source) {
408
+ const entries = [];
409
+ const collect = (spec) => {
410
+ const path = spec.childForFieldName("path")?.text ?? spec.text;
411
+ const name = spec.childForFieldName("name")?.text ?? "";
412
+ const text = source.slice(spec.startIndex, spec.endIndex);
413
+ entries.push({
414
+ key: `${name}:${path.replace(/^"|"$/g, "")}`,
415
+ text
416
+ });
417
+ };
418
+ for (const child of node.namedChildren) {
419
+ if (child.type === "import_spec") {
420
+ collect(child);
421
+ continue;
422
+ }
423
+ if (child.type === "import_spec_list") {
424
+ for (const spec of child.namedChildren) if (spec.type === "import_spec") collect(spec);
425
+ }
426
+ }
427
+ return entries;
428
+ }
429
+ function collectPrecedingGoComments(children, index, source) {
430
+ let lastIdx = -1;
431
+ let firstIdx = -1;
432
+ for (let k = index - 1; k >= 0; k--) {
433
+ const prev = children[k];
434
+ if (prev.type === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("//")) {
435
+ if (lastIdx === -1) lastIdx = k;
436
+ firstIdx = k;
437
+ } else break;
438
+ else break;
439
+ }
440
+ if (firstIdx === -1) return null;
441
+ const first = children[firstIdx];
442
+ const last = children[lastIdx];
443
+ return {
444
+ text: source.slice(first.startIndex, last.endIndex),
445
+ startIndex: first.startIndex,
446
+ endIndex: last.endIndex
447
+ };
448
+ }
449
+ const goMergeAdapter = {
450
+ language: "go",
451
+ grammarModule: "tree-sitter-go",
452
+ testFilePatterns: [/_test\.go$/],
453
+ urlFingerprintConfig: GO_URL_FINGERPRINT_CONFIG,
454
+ parseStatements(tree, source) {
455
+ const imports = [];
456
+ const importAnchors = [];
457
+ let importInsertionAnchor;
458
+ const statements = [];
459
+ for (const child of tree.rootNode.children) {
460
+ if (child.type === "comment") continue;
461
+ if (child.type === "package_clause") importInsertionAnchor = source.slice(child.startIndex, child.endIndex);
462
+ if (child.type === "import_declaration") {
463
+ imports.push(...importEntries(child, source));
464
+ importAnchors.push(source.slice(child.startIndex, child.endIndex));
465
+ continue;
466
+ }
467
+ const key = declarationKey$1(child);
468
+ statements.push({
469
+ key,
470
+ kind: key ? "declaration" : "other",
471
+ text: source.slice(child.startIndex, child.endIndex)
472
+ });
473
+ }
474
+ return {
475
+ imports,
476
+ importAnchors,
477
+ importInsertionAnchor,
478
+ statements
479
+ };
480
+ },
481
+ renderImports(imports) {
482
+ if (imports.length === 0) return [];
483
+ if (imports.length === 1) return [`import ${imports[0].text}`];
484
+ return [`import (\n${imports.map((entry) => ` ${entry.text}`).join("\n")}\n)`];
485
+ },
486
+ extractDocstrings(tree, source) {
487
+ const result = /* @__PURE__ */ new Map();
488
+ const rootChildren = tree.rootNode.children;
489
+ for (let i = 0; i < rootChildren.length; i++) {
490
+ const child = rootChildren[i];
491
+ const key = declarationKey$1(child);
492
+ if (!key || key.startsWith("__package:")) continue;
493
+ const docstring = collectPrecedingGoComments(rootChildren, i, source);
494
+ result.set(key, {
495
+ docstring,
496
+ declStartIndex: child.startIndex,
497
+ declColumn: child.startPosition.column,
498
+ members: /* @__PURE__ */ new Map()
499
+ });
500
+ }
501
+ return result;
502
+ }
503
+ };
504
+ //#endregion
505
+ //#region src/engine/merge-adapters/kotlin.ts
506
+ const KOTLIN_URL_FINGERPRINT_CONFIG = {
507
+ stringNodeTypes: ["string_literal"],
508
+ contentNodeTypes: ["string_content"],
509
+ interpolationNodeTypes: ["interpolated_expression"]
510
+ };
511
+ function extractDeclarationName(node) {
512
+ if (node.type === "class_declaration") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
513
+ if (node.type === "function_declaration") return node.children.find((c) => c.type === "simple_identifier")?.text ?? null;
514
+ if (node.type === "object_declaration") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
515
+ if (node.type === "type_alias") return node.children.find((c) => c.type === "type_identifier")?.text ?? null;
516
+ if (node.type === "property_declaration") {
517
+ const nameNode = node.children.find((c) => c.type === "variable_declaration");
518
+ if (nameNode) return nameNode.children.find((c) => c.type === "simple_identifier")?.text ?? null;
519
+ }
520
+ return null;
521
+ }
522
+ function findPrecedingKdoc(children, index, source) {
523
+ for (let k = index - 1; k >= 0; k--) {
524
+ const prev = children[k];
525
+ if (prev.type === "multiline_comment") {
526
+ const text = source.slice(prev.startIndex, prev.endIndex);
527
+ if (text.startsWith("/**")) return {
528
+ text,
529
+ startIndex: prev.startIndex,
530
+ endIndex: prev.endIndex
531
+ };
532
+ return null;
533
+ }
534
+ if (prev.type === "line_comment") continue;
535
+ return null;
536
+ }
537
+ return null;
538
+ }
539
+ const kotlinMergeAdapter = {
540
+ language: "kotlin",
541
+ grammarModule: "tree-sitter-kotlin",
542
+ testFilePatterns: [/Test\.kt$/],
543
+ urlFingerprintConfig: KOTLIN_URL_FINGERPRINT_CONFIG,
544
+ parseStatements(tree, source) {
545
+ const imports = [];
546
+ const importAnchors = [];
547
+ const statements = [];
548
+ for (const child of tree.rootNode.children) {
549
+ if (child.type === "multiline_comment" || child.type === "line_comment") continue;
550
+ if (child.type === "import_list") {
551
+ const fullText = source.slice(child.startIndex, child.endIndex);
552
+ importAnchors.push(fullText);
553
+ for (const imp of child.namedChildren) if (imp.type === "import_header") {
554
+ const text = source.slice(imp.startIndex, imp.endIndex).trim();
555
+ const key = imp.children.find((c) => c.type === "identifier")?.text ?? text;
556
+ imports.push({
557
+ key,
558
+ text
559
+ });
560
+ }
561
+ continue;
562
+ }
563
+ const name = extractDeclarationName(child);
564
+ statements.push({
565
+ key: name,
566
+ kind: name ? "declaration" : "other",
567
+ text: source.slice(child.startIndex, child.endIndex)
568
+ });
569
+ }
570
+ return {
571
+ imports,
572
+ importAnchors,
573
+ statements
574
+ };
575
+ },
576
+ renderImports(imports) {
577
+ return imports.map((entry) => entry.text);
578
+ },
579
+ extractDocstrings(tree, source) {
580
+ const result = /* @__PURE__ */ new Map();
581
+ const rootChildren = tree.rootNode.children;
582
+ for (let i = 0; i < rootChildren.length; i++) {
583
+ const child = rootChildren[i];
584
+ const name = extractDeclarationName(child);
585
+ if (!name) continue;
586
+ const docstring = findPrecedingKdoc(rootChildren, i, source);
587
+ const members = /* @__PURE__ */ new Map();
588
+ if (child.type === "class_declaration") {
589
+ const body = child.children.find((c) => c.type === "class_body" || c.type === "enum_class_body");
590
+ if (body) {
591
+ const bodyChildren = body.children;
592
+ for (let j = 0; j < bodyChildren.length; j++) {
593
+ const member = bodyChildren[j];
594
+ const memberName = extractDeclarationName(member);
595
+ if (!memberName) continue;
596
+ const memberDoc = findPrecedingKdoc(bodyChildren, j, source);
597
+ members.set(memberName, {
598
+ docstring: memberDoc,
599
+ declStartIndex: member.startIndex,
600
+ declColumn: member.startPosition.column,
601
+ urlFingerprint: extractUrlFingerprint(member, KOTLIN_URL_FINGERPRINT_CONFIG)
602
+ });
603
+ }
604
+ }
605
+ }
606
+ result.set(name, {
607
+ docstring,
608
+ declStartIndex: child.startIndex,
609
+ declColumn: child.startPosition.column,
610
+ members
611
+ });
612
+ }
613
+ return result;
614
+ }
615
+ };
616
+ //#endregion
617
+ //#region src/engine/merge-adapters/node.ts
618
+ const REEXPORT_PREFIX = "__export:";
619
+ function extractDeclName(node) {
620
+ const nameNode = node.childForFieldName("name");
621
+ if (nameNode) return nameNode.text;
622
+ if (node.type === "lexical_declaration") {
623
+ const declarator = node.firstNamedChild;
624
+ if (declarator?.type === "variable_declarator") {
625
+ const name = declarator.childForFieldName("name");
626
+ if (name) return name.text;
627
+ }
628
+ }
629
+ return null;
630
+ }
631
+ function extractNodeKey(node) {
632
+ if (node.type === "export_statement") {
633
+ const decl = node.childForFieldName("declaration");
634
+ if (decl) return {
635
+ key: extractDeclName(decl),
636
+ kind: "declaration"
637
+ };
638
+ const source = node.childForFieldName("source");
639
+ if (source) return {
640
+ key: `${REEXPORT_PREFIX}${normalizeJsExtension(source.text)}`,
641
+ kind: "reexport"
642
+ };
643
+ return {
644
+ key: null,
645
+ kind: "other"
646
+ };
647
+ }
648
+ return {
649
+ key: extractDeclName(node),
650
+ kind: extractDeclName(node) ? "declaration" : "other"
651
+ };
652
+ }
653
+ function extractClassMembers(classBody, source) {
654
+ const members = [];
655
+ for (const child of classBody.namedChildren) if (child.type === "method_definition") {
656
+ const nameNode = child.childForFieldName("name");
657
+ if (nameNode && nameNode.text !== "constructor") members.push({
658
+ key: nameNode.text,
659
+ text: source.slice(child.startIndex, child.endIndex)
660
+ });
661
+ } else if (child.type === "public_field_definition") {
662
+ const nameNode = child.childForFieldName("name");
663
+ if (nameNode) {
664
+ let endIdx = child.endIndex;
665
+ if (source[endIdx] === ";") endIdx += 1;
666
+ members.push({
667
+ key: nameNode.text,
668
+ text: source.slice(child.startIndex, endIdx)
669
+ });
670
+ }
671
+ }
672
+ return members;
673
+ }
674
+ function extractInterfaceMembers(body, source) {
675
+ const members = [];
676
+ for (const child of body.namedChildren) if (child.type === "property_signature" || child.type === "method_signature") {
677
+ const nameNode = child.childForFieldName("name");
678
+ if (nameNode) members.push({
679
+ key: nameNode.text,
680
+ text: source.slice(child.startIndex, child.endIndex)
681
+ });
682
+ }
683
+ return members;
684
+ }
685
+ function extractEnumMembers(body, source) {
686
+ const members = [];
687
+ for (const child of body.namedChildren) if (child.type === "enum_assignment") {
688
+ const nameNode = child.childForFieldName("name");
689
+ if (nameNode) members.push({
690
+ key: nameNode.text,
691
+ text: source.slice(child.startIndex, child.endIndex)
692
+ });
693
+ } else if (child.type === "property_identifier") members.push({
694
+ key: child.text,
695
+ text: source.slice(child.startIndex, child.endIndex)
696
+ });
697
+ return members;
698
+ }
699
+ function isCommentNode(node) {
700
+ return node.type === "comment" || node.grammarType === "comment";
701
+ }
702
+ function findPrecedingDocstring(children, index, source) {
703
+ for (let k = index - 1; k >= 0; k--) {
704
+ const prev = children[k];
705
+ if (isCommentNode(prev)) {
706
+ const text = source.slice(prev.startIndex, prev.endIndex);
707
+ if (text.startsWith("/**")) return {
708
+ text,
709
+ startIndex: prev.startIndex,
710
+ endIndex: prev.endIndex
711
+ };
712
+ return null;
713
+ }
714
+ if (prev.type === "," || prev.type === ";" || prev.type === "{") continue;
715
+ return null;
716
+ }
717
+ return null;
718
+ }
719
+ const CLASS_MEMBER_TYPES = new Set(["method_definition", "public_field_definition"]);
720
+ const INTERFACE_MEMBER_TYPES = new Set(["property_signature", "method_signature"]);
721
+ const ENUM_MEMBER_TYPES = new Set(["enum_assignment", "property_identifier"]);
722
+ const NODE_URL_FINGERPRINT_CONFIG = {
723
+ stringNodeTypes: ["string", "template_string"],
724
+ contentNodeTypes: ["string_fragment"],
725
+ interpolationNodeTypes: ["template_substitution"]
726
+ };
727
+ function extractBodyMemberDocstrings(body, source, memberTypes) {
728
+ const result = /* @__PURE__ */ new Map();
729
+ const allChildren = body.children;
730
+ for (let i = 0; i < allChildren.length; i++) {
731
+ const child = allChildren[i];
732
+ if (!memberTypes.has(child.type)) continue;
733
+ let memberName = null;
734
+ if (child.type === "property_identifier") memberName = child.text;
735
+ else {
736
+ const nameNode = child.childForFieldName("name");
737
+ if (nameNode) memberName = nameNode.text;
738
+ }
739
+ if (!memberName || memberName === "constructor") continue;
740
+ const docstring = findPrecedingDocstring(allChildren, i, source);
741
+ const fp = extractUrlFingerprint(child, NODE_URL_FINGERPRINT_CONFIG);
742
+ result.set(memberName, {
743
+ docstring,
744
+ declStartIndex: child.startIndex,
745
+ declColumn: child.startPosition.column,
746
+ urlFingerprint: fp
747
+ });
748
+ }
749
+ return result;
750
+ }
751
+ const nodeMergeAdapter = {
752
+ language: "node",
753
+ grammarModule: "tree-sitter-typescript/bindings/node/typescript.js",
754
+ normalizeReexport: normalizeJsExtension,
755
+ testFilePatterns: [/\.(spec|test)\.[jt]sx?$/],
756
+ urlFingerprintConfig: NODE_URL_FINGERPRINT_CONFIG,
757
+ shouldSkipDeepMerge(_symbolName, existingMemberKeys, newMembers) {
758
+ const newText = newMembers.map((m) => m.text).join("\n");
759
+ for (const match of newText.matchAll(/this\.(\w+)/g)) {
760
+ const propName = match[1];
761
+ if (propName && !existingMemberKeys.has(propName)) return true;
762
+ }
763
+ return false;
764
+ },
765
+ parseStatements(tree, source) {
766
+ const imports = [];
767
+ const importAnchors = [];
768
+ const statements = [];
769
+ for (const child of tree.rootNode.children) {
770
+ if (child.type === "comment") continue;
771
+ if (child.type === "import_statement") {
772
+ const text = source.slice(child.startIndex, child.endIndex);
773
+ const sourceNode = child.childForFieldName("source");
774
+ const modulePath = sourceNode ? normalizeJsExtension(sourceNode.text) : normalizeJsExtension(text.trim());
775
+ imports.push({
776
+ key: modulePath,
777
+ text
778
+ });
779
+ importAnchors.push(text);
780
+ continue;
781
+ }
782
+ const { key, kind } = extractNodeKey(child);
783
+ statements.push({
784
+ key,
785
+ kind,
786
+ text: source.slice(child.startIndex, child.endIndex)
787
+ });
788
+ }
789
+ return {
790
+ imports,
791
+ importAnchors,
792
+ statements
793
+ };
794
+ },
795
+ renderImports(imports) {
796
+ return imports.map((entry) => entry.text);
797
+ },
798
+ extractMembers(tree, source) {
799
+ const result = /* @__PURE__ */ new Map();
800
+ for (const child of tree.rootNode.children) {
801
+ if (child.type !== "export_statement") continue;
802
+ const decl = child.childForFieldName("declaration");
803
+ if (!decl) continue;
804
+ const nameNode = decl.childForFieldName("name");
805
+ if (!nameNode) continue;
806
+ const symbolName = nameNode.text;
807
+ if (decl.type === "class_declaration") {
808
+ const body = decl.childForFieldName("body");
809
+ if (!body) continue;
810
+ const members = extractClassMembers(body, source);
811
+ const firstMember = body.firstNamedChild;
812
+ const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
813
+ result.set(symbolName, {
814
+ members,
815
+ bodyEndLine: body.endPosition.row,
816
+ memberIndent
817
+ });
818
+ } else if (decl.type === "interface_declaration") {
819
+ const body = decl.childForFieldName("body");
820
+ if (!body) continue;
821
+ const members = extractInterfaceMembers(body, source);
822
+ const firstMember = body.firstNamedChild;
823
+ const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
824
+ result.set(symbolName, {
825
+ members,
826
+ bodyEndLine: body.endPosition.row,
827
+ memberIndent
828
+ });
829
+ } else if (decl.type === "enum_declaration") {
830
+ const body = decl.childForFieldName("body");
831
+ if (!body) continue;
832
+ const members = extractEnumMembers(body, source);
833
+ const firstMember = body.firstNamedChild;
834
+ const memberIndent = firstMember ? " ".repeat(firstMember.startPosition.column) : void 0;
835
+ result.set(symbolName, {
836
+ members,
837
+ bodyEndLine: body.endPosition.row,
838
+ memberIndent
839
+ });
840
+ }
841
+ }
842
+ return result;
843
+ },
844
+ extractDocstrings(tree, source) {
845
+ const result = /* @__PURE__ */ new Map();
846
+ const rootChildren = tree.rootNode.children;
847
+ for (let i = 0; i < rootChildren.length; i++) {
848
+ const child = rootChildren[i];
849
+ if (child.type !== "export_statement") continue;
850
+ const decl = child.childForFieldName("declaration");
851
+ if (!decl) continue;
852
+ const nameNode = decl.childForFieldName("name");
853
+ if (!nameNode) continue;
854
+ const docstring = findPrecedingDocstring(rootChildren, i, source);
855
+ let members = /* @__PURE__ */ new Map();
856
+ const body = decl.childForFieldName("body");
857
+ if (body) {
858
+ if (decl.type === "class_declaration") members = extractBodyMemberDocstrings(body, source, CLASS_MEMBER_TYPES);
859
+ else if (decl.type === "interface_declaration") members = extractBodyMemberDocstrings(body, source, INTERFACE_MEMBER_TYPES);
860
+ else if (decl.type === "enum_declaration") members = extractBodyMemberDocstrings(body, source, ENUM_MEMBER_TYPES);
861
+ }
862
+ result.set(nameNode.text, {
863
+ docstring,
864
+ declStartIndex: child.startIndex,
865
+ declColumn: child.startPosition.column,
866
+ members
867
+ });
868
+ }
869
+ return result;
870
+ }
871
+ };
872
+ //#endregion
873
+ //#region src/engine/merge-adapters/php.ts
874
+ const PHP_URL_FINGERPRINT_CONFIG = {
875
+ stringNodeTypes: ["encapsed_string", "string"],
876
+ contentNodeTypes: ["string_content"],
877
+ interpolationNodeTypes: ["variable_name"]
878
+ };
879
+ function extractPhpDeclarationName(node) {
880
+ switch (node.type) {
881
+ case "class_declaration":
882
+ case "interface_declaration":
883
+ case "trait_declaration":
884
+ case "enum_declaration":
885
+ case "function_definition": return node.childForFieldName("name")?.text ?? null;
886
+ case "namespace_definition": {
887
+ const nameNode = node.childForFieldName("name");
888
+ return nameNode ? `__namespace:${nameNode.text}` : "__namespace:";
889
+ }
890
+ default: return null;
891
+ }
892
+ }
893
+ function findPhpDocstring(children, index, source) {
894
+ for (let k = index - 1; k >= 0; k--) {
895
+ const prev = children[k];
896
+ if (prev.type === "comment") {
897
+ const text = source.slice(prev.startIndex, prev.endIndex);
898
+ if (text.startsWith("/**")) return {
899
+ text,
900
+ startIndex: prev.startIndex,
901
+ endIndex: prev.endIndex
902
+ };
903
+ return null;
904
+ }
905
+ if (prev.type === ";" || prev.type === "{") continue;
906
+ return null;
907
+ }
908
+ return null;
909
+ }
910
+ const phpMergeAdapter = {
911
+ language: "php",
912
+ grammarModule: "tree-sitter-php",
913
+ testFilePatterns: [/Test\.php$/],
914
+ urlFingerprintConfig: PHP_URL_FINGERPRINT_CONFIG,
915
+ resolveGrammar: (mod) => {
916
+ if (typeof mod === "object" && mod !== null && "php" in mod) return mod.php;
917
+ return mod;
918
+ },
919
+ parseStatements(tree, source) {
920
+ const imports = [];
921
+ const importAnchors = [];
922
+ let importInsertionAnchor;
923
+ const statements = [];
924
+ for (const child of tree.rootNode.children) {
925
+ if (child.type === "comment") continue;
926
+ if (child.type === "namespace_definition") importInsertionAnchor = source.slice(child.startIndex, child.endIndex);
927
+ if (child.type === "namespace_use_declaration") {
928
+ const text = source.slice(child.startIndex, child.endIndex);
929
+ imports.push({
930
+ key: text.trim(),
931
+ text
932
+ });
933
+ importAnchors.push(text);
934
+ continue;
935
+ }
936
+ const declarationName = extractPhpDeclarationName(child);
937
+ const kind = declarationName ? "declaration" : "other";
938
+ statements.push({
939
+ key: declarationName,
940
+ kind,
941
+ text: source.slice(child.startIndex, child.endIndex)
942
+ });
943
+ }
944
+ return {
945
+ imports,
946
+ importAnchors,
947
+ importInsertionAnchor,
948
+ statements
949
+ };
950
+ },
951
+ renderImports(imports) {
952
+ return imports.map((entry) => entry.text);
953
+ },
954
+ extractDocstrings(tree, source) {
955
+ const result = /* @__PURE__ */ new Map();
956
+ const rootChildren = tree.rootNode.children;
957
+ for (let i = 0; i < rootChildren.length; i++) {
958
+ const child = rootChildren[i];
959
+ const name = extractPhpDeclarationName(child);
960
+ if (!name || name.startsWith("__namespace:")) continue;
961
+ const docstring = findPhpDocstring(rootChildren, i, source);
962
+ const members = /* @__PURE__ */ new Map();
963
+ const body = child.childForFieldName("body");
964
+ if (body) {
965
+ const bodyChildren = body.children;
966
+ for (let j = 0; j < bodyChildren.length; j++) {
967
+ const member = bodyChildren[j];
968
+ if (member.type !== "method_declaration") continue;
969
+ const memberName = member.childForFieldName("name")?.text;
970
+ if (!memberName) continue;
971
+ const memberDoc = findPhpDocstring(bodyChildren, j, source);
972
+ members.set(memberName, {
973
+ docstring: memberDoc,
974
+ declStartIndex: member.startIndex,
975
+ declColumn: member.startPosition.column,
976
+ urlFingerprint: extractUrlFingerprint(member, PHP_URL_FINGERPRINT_CONFIG)
977
+ });
978
+ }
979
+ }
980
+ result.set(name, {
981
+ docstring,
982
+ declStartIndex: child.startIndex,
983
+ declColumn: child.startPosition.column,
984
+ members
985
+ });
986
+ }
987
+ return result;
988
+ }
989
+ };
990
+ //#endregion
991
+ //#region src/engine/merge-adapters/python.ts
992
+ const PYTHON_URL_FINGERPRINT_CONFIG = {
993
+ stringNodeTypes: ["string"],
994
+ contentNodeTypes: ["string_content"],
995
+ interpolationNodeTypes: ["interpolation"]
996
+ };
997
+ function extractPythonDeclarationName(node) {
998
+ if (node.type === "class_definition" || node.type === "function_definition") return node.childForFieldName("name")?.text ?? null;
999
+ if (node.type === "expression_statement") {
1000
+ const firstChild = node.firstNamedChild;
1001
+ if (firstChild?.type === "assignment") {
1002
+ const left = firstChild.childForFieldName("left");
1003
+ if (left?.type === "identifier" && left.text === "__all__") return "__all__";
1004
+ }
1005
+ }
1006
+ return null;
1007
+ }
1008
+ function extractPythonDocstring(bodyNode, source) {
1009
+ for (const child of bodyNode.namedChildren) {
1010
+ if (child.type === "expression_statement") {
1011
+ if (child.firstNamedChild?.type === "string") return {
1012
+ text: source.slice(child.startIndex, child.endIndex),
1013
+ startIndex: child.startIndex,
1014
+ endIndex: child.endIndex
1015
+ };
1016
+ }
1017
+ break;
1018
+ }
1019
+ return null;
1020
+ }
1021
+ function collectPrecedingPythonComments(children, index, source) {
1022
+ let lastIdx = -1;
1023
+ let firstIdx = -1;
1024
+ for (let k = index - 1; k >= 0; k--) if (children[k].type === "comment") {
1025
+ if (lastIdx === -1) lastIdx = k;
1026
+ firstIdx = k;
1027
+ } else break;
1028
+ if (firstIdx === -1) return null;
1029
+ const first = children[firstIdx];
1030
+ const last = children[lastIdx];
1031
+ return {
1032
+ text: source.slice(first.startIndex, last.endIndex),
1033
+ startIndex: first.startIndex,
1034
+ endIndex: last.endIndex
1035
+ };
1036
+ }
1037
+ const pythonMergeAdapter = {
1038
+ language: "python",
1039
+ grammarModule: "tree-sitter-python",
1040
+ testFilePatterns: [/(?:^|\/)test_.*\.py$/, /_test\.py$/],
1041
+ urlFingerprintConfig: PYTHON_URL_FINGERPRINT_CONFIG,
1042
+ parseStatements(tree, source) {
1043
+ const imports = [];
1044
+ const importAnchors = [];
1045
+ const statements = [];
1046
+ for (const child of tree.rootNode.children) {
1047
+ if (child.type === "comment") continue;
1048
+ if (child.type === "import_statement" || child.type === "import_from_statement") {
1049
+ const text = source.slice(child.startIndex, child.endIndex);
1050
+ imports.push({
1051
+ key: text.trim(),
1052
+ text
1053
+ });
1054
+ importAnchors.push(text);
1055
+ continue;
1056
+ }
1057
+ const declarationName = extractPythonDeclarationName(child);
1058
+ const kind = declarationName ? "declaration" : "other";
1059
+ statements.push({
1060
+ key: declarationName,
1061
+ kind,
1062
+ text: source.slice(child.startIndex, child.endIndex)
1063
+ });
1064
+ }
1065
+ return {
1066
+ imports,
1067
+ importAnchors,
1068
+ statements
1069
+ };
1070
+ },
1071
+ renderImports(imports) {
1072
+ return imports.map((entry) => entry.text);
1073
+ },
1074
+ extractDocstrings(tree, source) {
1075
+ const result = /* @__PURE__ */ new Map();
1076
+ const rootChildren = tree.rootNode.children;
1077
+ for (let i = 0; i < rootChildren.length; i++) {
1078
+ const child = rootChildren[i];
1079
+ const name = extractPythonDeclarationName(child);
1080
+ if (!name) continue;
1081
+ let docstring = null;
1082
+ const body = child.childForFieldName("body");
1083
+ if (body) docstring = extractPythonDocstring(body, source);
1084
+ if (!docstring) docstring = collectPrecedingPythonComments(rootChildren, i, source);
1085
+ const members = /* @__PURE__ */ new Map();
1086
+ if (child.type === "class_definition" && body) for (const member of body.namedChildren) {
1087
+ if (member.type !== "function_definition") continue;
1088
+ const memberName = member.childForFieldName("name")?.text;
1089
+ if (!memberName) continue;
1090
+ let memberDoc = null;
1091
+ const memberBody = member.childForFieldName("body");
1092
+ if (memberBody) memberDoc = extractPythonDocstring(memberBody, source);
1093
+ members.set(memberName, {
1094
+ docstring: memberDoc,
1095
+ declStartIndex: member.startIndex,
1096
+ declColumn: member.startPosition.column,
1097
+ urlFingerprint: extractUrlFingerprint(member, PYTHON_URL_FINGERPRINT_CONFIG)
1098
+ });
1099
+ }
1100
+ result.set(name, {
1101
+ docstring,
1102
+ declStartIndex: child.startIndex,
1103
+ declColumn: child.startPosition.column,
1104
+ members
1105
+ });
1106
+ }
1107
+ return result;
1108
+ }
1109
+ };
1110
+ //#endregion
1111
+ //#region src/engine/merge-adapters/ruby.ts
1112
+ const RUBY_URL_FINGERPRINT_CONFIG = {
1113
+ stringNodeTypes: ["string"],
1114
+ contentNodeTypes: ["string_content"],
1115
+ interpolationNodeTypes: ["interpolation"]
1116
+ };
1117
+ function isRequireCall(node) {
1118
+ if (node.type !== "call") return false;
1119
+ const method = node.childForFieldName("method");
1120
+ if (!method) return false;
1121
+ return method.text === "require" || method.text === "require_relative";
1122
+ }
1123
+ function extractRubyDeclarationName(node) {
1124
+ if (node.type !== "class" && node.type !== "module") return null;
1125
+ return node.childForFieldName("name")?.text ?? null;
1126
+ }
1127
+ function collectPrecedingRubyComments(children, index, source) {
1128
+ let lastIdx = -1;
1129
+ let firstIdx = -1;
1130
+ for (let k = index - 1; k >= 0; k--) {
1131
+ const prev = children[k];
1132
+ if ((prev.grammarType ?? prev.type) === "comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("#")) {
1133
+ if (lastIdx === -1) lastIdx = k;
1134
+ firstIdx = k;
1135
+ } else break;
1136
+ else break;
1137
+ }
1138
+ if (firstIdx === -1) return null;
1139
+ const first = children[firstIdx];
1140
+ const last = children[lastIdx];
1141
+ return {
1142
+ text: source.slice(first.startIndex, last.endIndex),
1143
+ startIndex: first.startIndex,
1144
+ endIndex: last.endIndex
1145
+ };
1146
+ }
1147
+ const rubyMergeAdapter = {
1148
+ language: "ruby",
1149
+ grammarModule: "tree-sitter-ruby",
1150
+ testFilePatterns: [/_test\.rb$/, /_spec\.rb$/],
1151
+ urlFingerprintConfig: RUBY_URL_FINGERPRINT_CONFIG,
1152
+ parseStatements(tree, source) {
1153
+ const imports = [];
1154
+ const importAnchors = [];
1155
+ const statements = [];
1156
+ for (const child of tree.rootNode.children) {
1157
+ if (child.type === "comment") continue;
1158
+ const declarationName = extractRubyDeclarationName(child);
1159
+ if (isRequireCall(child)) {
1160
+ const text = source.slice(child.startIndex, child.endIndex);
1161
+ imports.push({
1162
+ key: text.trim(),
1163
+ text
1164
+ });
1165
+ importAnchors.push(text);
1166
+ continue;
1167
+ }
1168
+ const kind = declarationName ? "declaration" : "other";
1169
+ statements.push({
1170
+ key: declarationName,
1171
+ kind,
1172
+ text: source.slice(child.startIndex, child.endIndex)
1173
+ });
1174
+ }
1175
+ return {
1176
+ imports,
1177
+ importAnchors,
1178
+ statements
1179
+ };
1180
+ },
1181
+ renderImports(imports) {
1182
+ return imports.map((entry) => entry.text);
1183
+ },
1184
+ extractDocstrings(tree, source) {
1185
+ const result = /* @__PURE__ */ new Map();
1186
+ const rootChildren = tree.rootNode.children;
1187
+ for (let i = 0; i < rootChildren.length; i++) {
1188
+ const child = rootChildren[i];
1189
+ const name = extractRubyDeclarationName(child);
1190
+ if (!name) continue;
1191
+ const docstring = collectPrecedingRubyComments(rootChildren, i, source);
1192
+ const members = /* @__PURE__ */ new Map();
1193
+ if (child.type === "class" || child.type === "module") {
1194
+ const allClassChildren = child.children;
1195
+ const body = child.childForFieldName("body");
1196
+ let firstMethodDocFromClass = null;
1197
+ for (let j = 0; j < allClassChildren.length; j++) if (allClassChildren[j] === body || allClassChildren[j].type === "body_statement") {
1198
+ firstMethodDocFromClass = collectPrecedingRubyComments(allClassChildren, j, source);
1199
+ break;
1200
+ }
1201
+ if (body) {
1202
+ const bodyChildren = body.children;
1203
+ let firstMethodHandled = false;
1204
+ for (let j = 0; j < bodyChildren.length; j++) {
1205
+ const member = bodyChildren[j];
1206
+ if (member.type !== "method") continue;
1207
+ const memberName = member.childForFieldName("name")?.text;
1208
+ if (!memberName) continue;
1209
+ let memberDoc;
1210
+ if (!firstMethodHandled && firstMethodDocFromClass) {
1211
+ memberDoc = firstMethodDocFromClass;
1212
+ firstMethodHandled = true;
1213
+ } else {
1214
+ memberDoc = collectPrecedingRubyComments(bodyChildren, j, source);
1215
+ firstMethodHandled = true;
1216
+ }
1217
+ members.set(memberName, {
1218
+ docstring: memberDoc,
1219
+ declStartIndex: member.startIndex,
1220
+ declColumn: member.startPosition.column,
1221
+ urlFingerprint: extractUrlFingerprint(member, RUBY_URL_FINGERPRINT_CONFIG)
1222
+ });
1223
+ }
1224
+ }
1225
+ }
1226
+ result.set(name, {
1227
+ docstring,
1228
+ declStartIndex: child.startIndex,
1229
+ declColumn: child.startPosition.column,
1230
+ members
1231
+ });
1232
+ }
1233
+ return result;
1234
+ }
1235
+ };
1236
+ //#endregion
1237
+ //#region src/engine/merge-adapters/rust.ts
1238
+ const RUST_URL_FINGERPRINT_CONFIG = {
1239
+ stringNodeTypes: ["string_literal"],
1240
+ contentNodeTypes: ["string_content"],
1241
+ interpolationNodeTypes: []
1242
+ };
1243
+ function declarationKey(node) {
1244
+ switch (node.type) {
1245
+ case "struct_item":
1246
+ case "enum_item":
1247
+ case "function_item":
1248
+ case "trait_item":
1249
+ case "type_item":
1250
+ case "const_item": return node.childForFieldName("name")?.text ?? null;
1251
+ case "impl_item": {
1252
+ const typeName = node.childForFieldName("type")?.text ?? "unknown";
1253
+ const traitName = node.childForFieldName("trait")?.text;
1254
+ return traitName ? `impl:${traitName}->${typeName}` : `impl:${typeName}`;
1255
+ }
1256
+ default: return null;
1257
+ }
1258
+ }
1259
+ function collectPrecedingRustDocComments(children, index, source) {
1260
+ let lastIdx = -1;
1261
+ let firstIdx = -1;
1262
+ for (let k = index - 1; k >= 0; k--) {
1263
+ const prev = children[k];
1264
+ if (prev.type === "line_comment") if (source.slice(prev.startIndex, prev.endIndex).startsWith("///")) {
1265
+ if (lastIdx === -1) lastIdx = k;
1266
+ firstIdx = k;
1267
+ } else break;
1268
+ else break;
1269
+ }
1270
+ if (firstIdx === -1) return null;
1271
+ const first = children[firstIdx];
1272
+ const last = children[lastIdx];
1273
+ return {
1274
+ text: source.slice(first.startIndex, last.endIndex),
1275
+ startIndex: first.startIndex,
1276
+ endIndex: last.endIndex
1277
+ };
1278
+ }
1279
+ //#endregion
1280
+ //#region src/engine/merge-adapters/index.ts
1281
+ const adapters = new Map([
1282
+ ["dotnet", dotnetMergeAdapter],
1283
+ ["elixir", elixirMergeAdapter],
1284
+ ["go", goMergeAdapter],
1285
+ ["kotlin", kotlinMergeAdapter],
1286
+ ["node", nodeMergeAdapter],
1287
+ ["php", phpMergeAdapter],
1288
+ ["python", pythonMergeAdapter],
1289
+ ["ruby", rubyMergeAdapter],
1290
+ ["rust", {
1291
+ language: "rust",
1292
+ grammarModule: "tree-sitter-rust",
1293
+ testFilePatterns: [/_test\.rs$/, /(?:^|\/)tests\/.*\.rs$/],
1294
+ urlFingerprintConfig: RUST_URL_FINGERPRINT_CONFIG,
1295
+ parseStatements(tree, source) {
1296
+ const imports = [];
1297
+ const importAnchors = [];
1298
+ const statements = [];
1299
+ for (const child of tree.rootNode.children) {
1300
+ if (child.type === "comment") continue;
1301
+ if (child.type === "use_declaration") {
1302
+ const text = source.slice(child.startIndex, child.endIndex);
1303
+ imports.push({
1304
+ key: text.trim(),
1305
+ text
1306
+ });
1307
+ importAnchors.push(text);
1308
+ continue;
1309
+ }
1310
+ const key = declarationKey(child);
1311
+ statements.push({
1312
+ key,
1313
+ kind: key ? "declaration" : "other",
1314
+ text: source.slice(child.startIndex, child.endIndex)
1315
+ });
1316
+ }
1317
+ return {
1318
+ imports,
1319
+ importAnchors,
1320
+ statements
1321
+ };
1322
+ },
1323
+ renderImports(imports) {
1324
+ return imports.map((entry) => entry.text);
1325
+ },
1326
+ extractDocstrings(tree, source) {
1327
+ const result = /* @__PURE__ */ new Map();
1328
+ const rootChildren = tree.rootNode.children;
1329
+ for (let i = 0; i < rootChildren.length; i++) {
1330
+ const child = rootChildren[i];
1331
+ const key = declarationKey(child);
1332
+ if (!key) continue;
1333
+ const docstring = collectPrecedingRustDocComments(rootChildren, i, source);
1334
+ result.set(key, {
1335
+ docstring,
1336
+ declStartIndex: child.startIndex,
1337
+ declColumn: child.startPosition.column,
1338
+ members: /* @__PURE__ */ new Map()
1339
+ });
1340
+ }
1341
+ return result;
1342
+ }
1343
+ }]
1344
+ ]);
1345
+ function getMergeAdapter(language) {
1346
+ return adapters.get(language);
1347
+ }
1348
+ //#endregion
1349
+ //#region src/engine/merger.ts
1350
+ /**
1351
+ * AST-level file merger using tree-sitter.
1352
+ *
1353
+ * When generating into a directory that already contains files, the merger
1354
+ * parses both the existing and generated content, detects top-level symbols,
1355
+ * and only appends symbols that don't already exist. Hand-written code is
1356
+ * never modified or removed.
1357
+ *
1358
+ * Supports any language with a tree-sitter grammar package.
1359
+ */
1360
+ const parserCache = /* @__PURE__ */ new Map();
1361
+ function findIgnoredRegions(source) {
1362
+ const regions = [];
1363
+ const startTag = "@oagen-ignore-start";
1364
+ const endTag = "@oagen-ignore-end";
1365
+ let searchFrom = 0;
1366
+ while (true) {
1367
+ const startIdx = source.indexOf(startTag, searchFrom);
1368
+ if (startIdx === -1) break;
1369
+ const endIdx = source.indexOf(endTag, startIdx + 19);
1370
+ if (endIdx === -1) break;
1371
+ regions.push({
1372
+ startIndex: startIdx,
1373
+ endIndex: endIdx + 17
1374
+ });
1375
+ searchFrom = endIdx + 17;
1376
+ }
1377
+ return regions;
1378
+ }
1379
+ function buildIgnoredSymbolNames(docstrings, regions) {
1380
+ if (regions.length === 0) return /* @__PURE__ */ new Set();
1381
+ const ignored = /* @__PURE__ */ new Set();
1382
+ for (const [name, info] of docstrings) if (regions.some((r) => info.declStartIndex >= r.startIndex && info.declStartIndex <= r.endIndex)) ignored.add(name);
1383
+ return ignored;
1384
+ }
1385
+ /**
1386
+ * Check if a tree-sitter grammar is configured for the given language.
1387
+ */
1388
+ function hasGrammar(language) {
1389
+ return getMergeAdapter(language) !== void 0;
1390
+ }
1391
+ async function getParser(language) {
1392
+ const cached = parserCache.get(language);
1393
+ if (cached) return cached;
1394
+ const adapter = getMergeAdapter(language);
1395
+ if (!adapter) throw new Error(`No tree-sitter grammar configured for language "${language}". Add a merge adapter and install the corresponding tree-sitter grammar package.`);
1396
+ const mod = await import(adapter.grammarModule);
1397
+ const exported = adapter.resolveGrammar ? adapter.resolveGrammar(mod) : mod;
1398
+ const grammar = exported.default ?? exported;
1399
+ const parser = new Parser();
1400
+ parser.setLanguage(grammar);
1401
+ parserCache.set(language, parser);
1402
+ return parser;
1403
+ }
1404
+ /**
1405
+ * Extract top-level statements from generated source with their names
1406
+ * and exact text span.
1407
+ */
1408
+ async function extractStatements(source, language) {
1409
+ const parser = await getParser(language);
1410
+ const adapter = getMergeAdapter(language);
1411
+ if (!adapter) throw new Error(`No merge adapter configured for language "${language}"`);
1412
+ const tree = safeParse(parser, source);
1413
+ return adapter.parseStatements(tree, source);
1414
+ }
1415
+ /**
1416
+ * Merge generated content into an existing file.
1417
+ *
1418
+ * - Parses both files with tree-sitter
1419
+ * - Detects which top-level symbols already exist
1420
+ * - Appends only new symbols
1421
+ * - Never modifies or removes existing code
1422
+ * - Adds auto-generated header if not present
1423
+ */
1424
+ async function mergeIntoExisting(existingContent, generatedContent, language, header) {
1425
+ const adapter = getMergeAdapter(language);
1426
+ if (!adapter) throw new Error(`No merge adapter configured for language "${language}"`);
1427
+ const existingStatements = await extractStatements(existingContent, language);
1428
+ const generatedStatements = await extractStatements(generatedContent, language);
1429
+ const existingKeys = /* @__PURE__ */ new Set();
1430
+ const existingUnnamedTexts = /* @__PURE__ */ new Set();
1431
+ const existingImportKeys = /* @__PURE__ */ new Set();
1432
+ const existingReexports = /* @__PURE__ */ new Set();
1433
+ let lastImportEndIndex = -1;
1434
+ const existingImportedNames = /* @__PURE__ */ new Set();
1435
+ for (const imp of existingStatements.imports) {
1436
+ existingImportKeys.add(imp.key);
1437
+ const braceMatch = imp.text.match(/\{([^}]+)\}/);
1438
+ if (braceMatch) for (const name of braceMatch[1].split(",")) {
1439
+ const trimmed = name.replace(/\btype\b/, "").trim();
1440
+ if (trimmed) existingImportedNames.add(trimmed);
1441
+ }
1442
+ }
1443
+ for (const anchor of existingStatements.importAnchors) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(anchor)).split("\n").length - 1 + anchor.split("\n").length - 1;
1444
+ if (lastImportEndIndex === -1 && existingStatements.importInsertionAnchor) lastImportEndIndex = existingContent.slice(0, existingContent.indexOf(existingStatements.importInsertionAnchor)).split("\n").length - 1 + existingStatements.importInsertionAnchor.split("\n").length - 1;
1445
+ for (const stmt of existingStatements.statements) {
1446
+ if (stmt.kind === "reexport") existingReexports.add(adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim());
1447
+ if (stmt.key) existingKeys.add(stmt.key);
1448
+ else existingUnnamedTexts.add(stmt.text.trim());
1449
+ }
1450
+ const headerLine = header.trim();
1451
+ const newImports = [];
1452
+ const toAppend = [];
1453
+ let preserved = 0;
1454
+ for (const imp of generatedStatements.imports) {
1455
+ if (imp.text.trim() === headerLine) continue;
1456
+ if (existingImportKeys.has(imp.key)) {
1457
+ const braceMatch = imp.text.match(/\{([^}]+)\}/);
1458
+ if (braceMatch) {
1459
+ const newNames = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean).filter((n) => !existingImportedNames.has(n) && !existingKeys.has(n));
1460
+ if (newNames.length > 0) {
1461
+ const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
1462
+ const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
1463
+ if (sourceMatch) newImports.push({
1464
+ key: imp.key + "#supplemental",
1465
+ text: `${prefix} { ${newNames.join(", ")} } from ${sourceMatch[1]};`
1466
+ });
1467
+ }
1468
+ }
1469
+ preserved++;
1470
+ continue;
1471
+ }
1472
+ const braceMatch = imp.text.match(/\{([^}]+)\}/);
1473
+ if (braceMatch) {
1474
+ const names = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean);
1475
+ const isAlreadyDefined = (n) => existingImportedNames.has(n) || existingKeys.has(n);
1476
+ if (names.length > 0 && names.every(isAlreadyDefined)) {
1477
+ preserved++;
1478
+ continue;
1479
+ }
1480
+ if (names.some(isAlreadyDefined)) {
1481
+ const newNames = names.filter((n) => !isAlreadyDefined(n));
1482
+ if (newNames.length === 0) {
1483
+ preserved++;
1484
+ continue;
1485
+ }
1486
+ const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
1487
+ const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
1488
+ if (sourceMatch) {
1489
+ const newText = `${prefix} { ${newNames.join(", ")} } from ${sourceMatch[1]};`;
1490
+ newImports.push({
1491
+ key: imp.key,
1492
+ text: newText
1493
+ });
1494
+ continue;
1495
+ }
1496
+ }
1497
+ }
1498
+ newImports.push(imp);
1499
+ }
1500
+ for (const stmt of generatedStatements.statements) {
1501
+ if (stmt.text.trim() === headerLine) continue;
1502
+ if (stmt.kind === "reexport") {
1503
+ const normalizedText = adapter.normalizeReexport ? adapter.normalizeReexport(stmt.text.trim()) : stmt.text.trim();
1504
+ if (existingReexports.has(normalizedText) || stmt.key !== null && existingKeys.has(stmt.key)) {
1505
+ preserved++;
1506
+ continue;
1507
+ }
1508
+ }
1509
+ if (stmt.key && (existingKeys.has(stmt.key) || existingImportedNames.has(stmt.key))) {
1510
+ preserved++;
1511
+ continue;
1512
+ }
1513
+ if (!stmt.key) {
1514
+ if (existingUnnamedTexts.has(stmt.text.trim())) {
1515
+ preserved++;
1516
+ continue;
1517
+ }
1518
+ }
1519
+ toAppend.push(stmt.text);
1520
+ }
1521
+ if (newImports.length === 0 && toAppend.length === 0) {
1522
+ if (!adapter.extractMembers) {}
1523
+ }
1524
+ let result = existingContent;
1525
+ if (toAppend.length > 0) result = result.trimEnd() + "\n\n" + toAppend.join("\n\n") + "\n";
1526
+ let deepAdded = 0;
1527
+ const insertions = [];
1528
+ if (adapter.extractMembers) {
1529
+ const parser = await getParser(language);
1530
+ const resultTree = safeParse(parser, result);
1531
+ const generatedTree = safeParse(parser, generatedContent);
1532
+ const resultSymbols = adapter.extractMembers(resultTree, result);
1533
+ const generatedSymbols = adapter.extractMembers(generatedTree, generatedContent);
1534
+ const existingTree = safeParse(parser, existingContent);
1535
+ const deepIgnoredSymbols = buildIgnoredSymbolNames(adapter.extractDocstrings(existingTree, existingContent), findIgnoredRegions(existingContent));
1536
+ for (const [symbolName, genSymbol] of generatedSymbols) {
1537
+ if (deepIgnoredSymbols.has(symbolName)) continue;
1538
+ const existSymbol = resultSymbols.get(symbolName);
1539
+ if (!existSymbol) continue;
1540
+ const existingMemberKeys = new Set(existSymbol.members.map((m) => m.key));
1541
+ const newMembers = genSymbol.members.filter((m) => !existingMemberKeys.has(m.key));
1542
+ if (newMembers.length > 0) {
1543
+ if (adapter.shouldSkipDeepMerge?.(symbolName, existingMemberKeys, newMembers)) continue;
1544
+ const indent = existSymbol.memberIndent ?? " ";
1545
+ const insertText = newMembers.map((m) => indent + m.text).join("\n");
1546
+ insertions.push({
1547
+ line: existSymbol.bodyEndLine,
1548
+ text: insertText
1549
+ });
1550
+ deepAdded += newMembers.length;
1551
+ }
1552
+ }
1553
+ if (insertions.length > 0) {
1554
+ insertions.sort((a, b) => b.line - a.line);
1555
+ const resultLines = result.split("\n");
1556
+ for (const ins of insertions) resultLines.splice(ins.line, 0, ins.text);
1557
+ result = resultLines.join("\n");
1558
+ }
1559
+ }
1560
+ if (newImports.length > 0 && (toAppend.length > 0 || deepAdded > 0)) {
1561
+ const addedParts = [...toAppend];
1562
+ if (insertions) for (const ins of insertions) addedParts.push(ins.text);
1563
+ const addedCodeText = addedParts.join("\n");
1564
+ const filteredImports = [];
1565
+ for (const imp of newImports) {
1566
+ const braceMatch = imp.text.match(/\{([^}]+)\}/);
1567
+ if (!braceMatch) {
1568
+ filteredImports.push(imp);
1569
+ continue;
1570
+ }
1571
+ const names = braceMatch[1].split(",").map((n) => n.replace(/\btype\b/, "").trim()).filter(Boolean);
1572
+ const usedNames = names.filter((name) => addedCodeText.includes(name));
1573
+ if (usedNames.length === 0) continue;
1574
+ if (usedNames.length === names.length) filteredImports.push(imp);
1575
+ else {
1576
+ const prefix = imp.text.trimStart().startsWith("import type") ? "import type" : "import";
1577
+ const sourceMatch = imp.text.match(/from\s+(['"][^'"]+['"]);?/);
1578
+ if (sourceMatch) filteredImports.push({
1579
+ key: imp.key,
1580
+ text: `${prefix} { ${usedNames.join(", ")} } from ${sourceMatch[1]};`
1581
+ });
1582
+ else filteredImports.push(imp);
1583
+ }
1584
+ }
1585
+ if (filteredImports.length > 0) {
1586
+ const renderedImports = adapter.renderImports ? adapter.renderImports(filteredImports) : filteredImports.map((entry) => entry.text);
1587
+ const lines = result.split("\n");
1588
+ const insertIdx = lastImportEndIndex + 1;
1589
+ lines.splice(insertIdx, 0, ...renderedImports);
1590
+ result = lines.join("\n");
1591
+ }
1592
+ }
1593
+ let docstringUpdates = 0;
1594
+ {
1595
+ const parser = await getParser(language);
1596
+ const resultTree = safeParse(parser, result);
1597
+ const generatedTree = safeParse(parser, generatedContent);
1598
+ const resultDocs = adapter.extractDocstrings(resultTree, result);
1599
+ const generatedDocs = adapter.extractDocstrings(generatedTree, generatedContent);
1600
+ const docIgnoredSymbols = buildIgnoredSymbolNames(resultDocs, findIgnoredRegions(result));
1601
+ const edits = [];
1602
+ for (const [symbolName, genInfo] of generatedDocs) {
1603
+ const existInfo = resultDocs.get(symbolName);
1604
+ if (!existInfo) continue;
1605
+ if (docIgnoredSymbols.has(symbolName)) continue;
1606
+ const genDoc = genInfo.docstring && genInfo.docstring.text.trim() !== headerLine ? genInfo.docstring : null;
1607
+ if (genDoc) if (existInfo.docstring) {
1608
+ if (!existInfo.docstring.text.includes("@oagen-ignore") && existInfo.docstring.text !== genDoc.text) {
1609
+ edits.push({
1610
+ start: existInfo.docstring.startIndex,
1611
+ end: existInfo.docstring.endIndex,
1612
+ newText: genDoc.text
1613
+ });
1614
+ docstringUpdates++;
1615
+ }
1616
+ } else {
1617
+ const lineStart = existInfo.declStartIndex - existInfo.declColumn;
1618
+ const indent = " ".repeat(existInfo.declColumn);
1619
+ edits.push({
1620
+ start: lineStart,
1621
+ end: lineStart,
1622
+ newText: indent + genDoc.text + "\n"
1623
+ });
1624
+ docstringUpdates++;
1625
+ }
1626
+ const matchedExistMembers = /* @__PURE__ */ new Set();
1627
+ for (const [memberName, genMember] of genInfo.members) {
1628
+ const existMember = existInfo.members.get(memberName);
1629
+ if (!existMember || !genMember.docstring) continue;
1630
+ matchedExistMembers.add(memberName);
1631
+ if (existMember.docstring) {
1632
+ if (!existMember.docstring.text.includes("@oagen-ignore") && existMember.docstring.text !== genMember.docstring.text) {
1633
+ edits.push({
1634
+ start: existMember.docstring.startIndex,
1635
+ end: existMember.docstring.endIndex,
1636
+ newText: genMember.docstring.text
1637
+ });
1638
+ docstringUpdates++;
1639
+ }
1640
+ } else {
1641
+ const lineStart = existMember.declStartIndex - existMember.declColumn;
1642
+ const indent = " ".repeat(existMember.declColumn);
1643
+ edits.push({
1644
+ start: lineStart,
1645
+ end: lineStart,
1646
+ newText: indent + genMember.docstring.text + "\n"
1647
+ });
1648
+ docstringUpdates++;
1649
+ }
1650
+ }
1651
+ for (const [_genName, genMember] of genInfo.members) {
1652
+ if (!genMember.docstring || !genMember.urlFingerprint) continue;
1653
+ for (const [existName, existMember] of existInfo.members) {
1654
+ if (matchedExistMembers.has(existName)) continue;
1655
+ if (!existMember.urlFingerprint || existMember.urlFingerprint !== genMember.urlFingerprint) continue;
1656
+ if (existMember.docstring) {
1657
+ if (existMember.docstring.text.includes("@oagen-ignore")) continue;
1658
+ if (existMember.docstring.text !== genMember.docstring.text) {
1659
+ edits.push({
1660
+ start: existMember.docstring.startIndex,
1661
+ end: existMember.docstring.endIndex,
1662
+ newText: genMember.docstring.text
1663
+ });
1664
+ docstringUpdates++;
1665
+ }
1666
+ } else {
1667
+ const lineStart = existMember.declStartIndex - existMember.declColumn;
1668
+ const indent = " ".repeat(existMember.declColumn);
1669
+ edits.push({
1670
+ start: lineStart,
1671
+ end: lineStart,
1672
+ newText: indent + genMember.docstring.text + "\n"
1673
+ });
1674
+ docstringUpdates++;
1675
+ }
1676
+ matchedExistMembers.add(existName);
1677
+ break;
1678
+ }
1679
+ }
1680
+ }
1681
+ if (edits.length > 0) {
1682
+ edits.sort((a, b) => b.start - a.start);
1683
+ for (const edit of edits) result = result.slice(0, edit.start) + edit.newText + result.slice(edit.end);
1684
+ }
1685
+ }
1686
+ const totalAdded = (toAppend.length > 0 || deepAdded > 0 ? adapter.renderImports ? adapter.renderImports(newImports).length : newImports.length : 0) + toAppend.length + deepAdded;
1687
+ if (totalAdded === 0 && docstringUpdates === 0) return {
1688
+ content: existingContent,
1689
+ added: 0,
1690
+ preserved,
1691
+ changed: false
1692
+ };
1693
+ return {
1694
+ content: result,
1695
+ added: totalAdded,
1696
+ preserved,
1697
+ changed: true
1698
+ };
1699
+ }
1700
+ //#endregion
1701
+ //#region src/engine/json-merge.ts
1702
+ /**
1703
+ * Deep merge two parsed JSON values.
1704
+ *
1705
+ * - Objects: recursively merge, preserving existing keys not in generated
1706
+ * - Arrays: generated replaces existing entirely (generated owns array contents)
1707
+ * - Primitives: generated wins
1708
+ */
1709
+ function deepMergeJson(existing, generated) {
1710
+ if (typeof existing === "object" && existing !== null && !Array.isArray(existing) && typeof generated === "object" && generated !== null && !Array.isArray(generated)) {
1711
+ const merged = { ...existing };
1712
+ for (const [key, value] of Object.entries(generated)) if (key in merged) merged[key] = deepMergeJson(merged[key], value);
1713
+ else merged[key] = value;
1714
+ return merged;
1715
+ }
1716
+ return generated;
1717
+ }
1718
+ //#endregion
1719
+ //#region src/engine/writer.ts
1720
+ /**
1721
+ * Write generated files to disk with AST-level merging.
1722
+ *
1723
+ * When a file already exists:
1724
+ * - skipIfExists → leave it alone entirely
1725
+ * - AST merge available → parse both, append only new symbols
1726
+ * - No grammar → skip (never clobber hand-written code)
1727
+ *
1728
+ * New files are always written in full.
1729
+ */
1730
+ async function writeFiles(files, outputDir, options) {
1731
+ const result = {
1732
+ written: [],
1733
+ merged: [],
1734
+ skipped: [],
1735
+ identical: [],
1736
+ ignored: []
1737
+ };
1738
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
1739
+ const language = options?.language;
1740
+ const header = options?.header ?? "";
1741
+ for (const file of sorted) {
1742
+ const fullPath = path$1.join(outputDir, file.path);
1743
+ let existingContent = null;
1744
+ try {
1745
+ existingContent = await fs.readFile(fullPath, "utf-8");
1746
+ } catch {}
1747
+ if (existingContent === null) {
1748
+ await fs.mkdir(path$1.dirname(fullPath), { recursive: true });
1749
+ await fs.writeFile(fullPath, file.content, "utf-8");
1750
+ result.written.push(file.path);
1751
+ continue;
1752
+ }
1753
+ if (file.skipIfExists) {
1754
+ if (header && !existingContent.startsWith(header)) await fs.writeFile(fullPath, header + "\n\n" + existingContent, "utf-8");
1755
+ result.skipped.push(file.path);
1756
+ continue;
1757
+ }
1758
+ if (existingContent.includes("@oagen-ignore-file")) {
1759
+ result.ignored.push(file.path);
1760
+ continue;
1761
+ }
1762
+ const adapter = language ? getMergeAdapter(language) : void 0;
1763
+ if (adapter?.testFilePatterns ? adapter.testFilePatterns.some((p) => p.test(file.path)) : isDefaultTestFile(file.path)) {
1764
+ result.skipped.push(file.path);
1765
+ continue;
1766
+ }
1767
+ if (existingContent === file.content) {
1768
+ result.identical.push(file.path);
1769
+ continue;
1770
+ }
1771
+ if (file.overwriteExisting) {
1772
+ await fs.writeFile(fullPath, file.content, "utf-8");
1773
+ result.written.push(file.path);
1774
+ continue;
1775
+ }
1776
+ if (file.path.endsWith(".json")) {
1777
+ try {
1778
+ const merged = deepMergeJson(JSON.parse(existingContent), JSON.parse(file.content));
1779
+ const mergedContent = JSON.stringify(merged, null, 2) + "\n";
1780
+ if (mergedContent === existingContent) result.identical.push(file.path);
1781
+ else {
1782
+ await fs.writeFile(fullPath, mergedContent, "utf-8");
1783
+ result.merged.push(file.path);
1784
+ }
1785
+ } catch {
1786
+ await fs.writeFile(fullPath, file.content, "utf-8");
1787
+ result.written.push(file.path);
1788
+ }
1789
+ continue;
1790
+ }
1791
+ if (language && hasGrammar(language)) try {
1792
+ const mergeResult = await mergeIntoExisting(existingContent, file.content, language, header);
1793
+ let finalContent = mergeResult.content;
1794
+ if (header && !finalContent.startsWith(header)) finalContent = header + "\n\n" + finalContent;
1795
+ if (!mergeResult.changed && finalContent === existingContent) {
1796
+ result.identical.push(file.path);
1797
+ continue;
1798
+ }
1799
+ await fs.writeFile(fullPath, finalContent, "utf-8");
1800
+ result.merged.push(file.path);
1801
+ continue;
1802
+ } catch (err) {
1803
+ console.warn(`[oagen] AST merge failed for ${file.path}, falling back to overwrite.${err instanceof Error ? ` ${err.message}` : ""}`);
1804
+ await fs.writeFile(fullPath, file.content, "utf-8");
1805
+ result.written.push(file.path);
1806
+ continue;
1807
+ }
1808
+ result.skipped.push(file.path);
1809
+ }
1810
+ return result;
1811
+ }
1812
+ /** Comprehensive fallback when no adapter is available. */
1813
+ function isDefaultTestFile(filePath) {
1814
+ return /\.(spec|test)\.[jt]sx?$/.test(filePath) || filePath.endsWith("_test.go") || filePath.endsWith("_test.rb") || filePath.endsWith("_spec.rb") || /(?:^|\/)test_.*\.py$/.test(filePath) || filePath.endsWith("_test.py") || filePath.endsWith("Test.php") || filePath.endsWith("_test.rs") || /(?:^|\/)tests\/.*\.rs$/.test(filePath);
1815
+ }
1816
+ //#endregion
1817
+ //#region src/engine/generate-files.ts
1818
+ function buildEmitterContext(spec, options) {
1819
+ return {
1820
+ namespace: toSnakeCase(options.namespace),
1821
+ namespacePascal: options.namespace,
1822
+ spec,
1823
+ outputDir: options.outputDir,
1824
+ apiSurface: options.apiSurface,
1825
+ overlayLookup: options.overlayLookup
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Collect model and enum names transitively referenced by service operations.
1830
+ * Walks operation parameters, request bodies, responses, and pagination item
1831
+ * types, then chases model field references until the set stabilizes.
1832
+ */
1833
+ function collectReferencedNames(services, models) {
1834
+ const referencedModels = /* @__PURE__ */ new Set();
1835
+ const referencedEnums = /* @__PURE__ */ new Set();
1836
+ const collectFromTypeRef = (ref) => {
1837
+ walkTypeRef(ref, {
1838
+ model: (r) => referencedModels.add(r.name),
1839
+ enum: (r) => referencedEnums.add(r.name)
1840
+ });
1841
+ };
1842
+ for (const service of services) for (const op of service.operations) {
1843
+ for (const p of [
1844
+ ...op.pathParams,
1845
+ ...op.queryParams,
1846
+ ...op.headerParams,
1847
+ ...op.cookieParams ?? []
1848
+ ]) collectFromTypeRef(p.type);
1849
+ if (op.requestBody) collectFromTypeRef(op.requestBody);
1850
+ collectFromTypeRef(op.response);
1851
+ if (op.pagination) collectFromTypeRef(op.pagination.itemType);
1852
+ for (const err of op.errors) if (err.type) collectFromTypeRef(err.type);
1853
+ if (op.successResponses) for (const sr of op.successResponses) collectFromTypeRef(sr.type);
1854
+ }
1855
+ const modelsByName = new Map(models.map((m) => [m.name, m]));
1856
+ const visited = /* @__PURE__ */ new Set();
1857
+ const queue = [...referencedModels];
1858
+ while (queue.length > 0) {
1859
+ const name = queue.pop();
1860
+ if (visited.has(name)) continue;
1861
+ visited.add(name);
1862
+ const model = modelsByName.get(name);
1863
+ if (!model) continue;
1864
+ for (const field of model.fields) {
1865
+ collectFromTypeRef(field.type);
1866
+ for (const m of referencedModels) if (!visited.has(m)) queue.push(m);
1867
+ }
1868
+ }
1869
+ return {
1870
+ models: referencedModels,
1871
+ enums: referencedEnums
1872
+ };
1873
+ }
1874
+ /** Collect all generated files from an emitter (no headers, no path prefixes). */
1875
+ function generateAllFiles(spec, emitter, ctx) {
1876
+ const referenced = collectReferencedNames(spec.services, spec.models);
1877
+ const reachableModels = spec.models.filter((m) => referenced.models.has(m.name));
1878
+ const reachableEnums = spec.enums.filter((e) => referenced.enums.has(e.name));
1879
+ const reachableSpec = {
1880
+ ...spec,
1881
+ models: reachableModels,
1882
+ enums: reachableEnums
1883
+ };
1884
+ return [
1885
+ ...emitter.generateModels(reachableModels, ctx),
1886
+ ...emitter.generateEnums(reachableEnums, ctx),
1887
+ ...emitter.generateResources(spec.services, ctx),
1888
+ ...emitter.generateClient(spec, ctx),
1889
+ ...emitter.generateErrors(ctx),
1890
+ ...emitter.generateConfig(ctx),
1891
+ ...emitter.generateTypeSignatures?.(reachableSpec, ctx) ?? [],
1892
+ ...emitter.generateTests(reachableSpec, ctx),
1893
+ ...emitter.generateManifest?.(spec, ctx) ?? []
1894
+ ];
1895
+ }
1896
+ /** Apply file header to generated files, respecting headerPlacement and JSON files. */
1897
+ function applyFileHeaders(files, header) {
1898
+ return files.map((f) => ({
1899
+ ...f,
1900
+ content: !header || f.path.endsWith(".json") || f.headerPlacement === "skip" ? f.content : header + "\n\n" + f.content,
1901
+ skipIfExists: f.skipIfExists ?? false
1902
+ }));
1903
+ }
1904
+ function generateFiles(spec, emitter, options) {
1905
+ const ctx = buildEmitterContext(spec, options);
1906
+ const files = generateAllFiles(spec, emitter, ctx);
1907
+ const header = emitter.fileHeader();
1908
+ return {
1909
+ files: applyFileHeaders(files, header),
1910
+ ctx,
1911
+ header
1912
+ };
1913
+ }
1914
+ //#endregion
1915
+ //#region src/engine/integrate.ts
1916
+ /**
1917
+ * Resolve a relative import path against a source file path to get the
1918
+ * target file path. Handles implicit .ts extensions and /index.ts.
1919
+ */
1920
+ function resolveImportPath(fromFilePath, importModulePath, knownPaths) {
1921
+ const cleaned = importModulePath.replace(/^['"]|['"]$/g, "");
1922
+ if (!cleaned.startsWith(".")) return void 0;
1923
+ const dir = path.dirname(fromFilePath);
1924
+ const resolved = path.normalize(path.join(dir, cleaned));
1925
+ for (const candidate of [
1926
+ resolved,
1927
+ `${resolved}.ts`,
1928
+ `${resolved}/index.ts`
1929
+ ]) if (knownPaths.has(candidate)) return candidate;
1930
+ }
1931
+ /**
1932
+ * Determine whether a generated file is a "root" — a file that should always
1933
+ * be integrated into the target. Roots are entry points from which we trace
1934
+ * the import graph. Everything else must be reachable from a root.
1935
+ *
1936
+ * Roots: resource classes, client, config, errors, enums, barrel index files.
1937
+ * Non-roots: serializers, fixtures, standalone interfaces, test files.
1938
+ */
1939
+ function isRootFile(filePath) {
1940
+ if (filePath.includes("/fixtures/")) return false;
1941
+ if (filePath.endsWith(".spec.ts") || filePath.endsWith(".test.ts")) return false;
1942
+ if (filePath.includes("/serializers/")) return false;
1943
+ if (filePath.includes("/interfaces/") && !filePath.endsWith("/index.ts")) return false;
1944
+ return true;
1945
+ }
1946
+ /**
1947
+ * Build a file-level dependency graph from generated files and return only
1948
+ * files reachable from root entry points. Uses tree-sitter to parse imports.
1949
+ */
1950
+ async function treeShakeFiles(files, language) {
1951
+ if (!hasGrammar(language)) return files;
1952
+ const knownPaths = new Set(files.map((f) => f.path));
1953
+ const deps = /* @__PURE__ */ new Map();
1954
+ for (const file of files) {
1955
+ if (!file.path.endsWith(".ts") && !file.path.endsWith(".js")) {
1956
+ deps.set(file.path, /* @__PURE__ */ new Set());
1957
+ continue;
1958
+ }
1959
+ const parsed = await extractStatements(file.content, language);
1960
+ const fileDeps = /* @__PURE__ */ new Set();
1961
+ for (const imp of parsed.imports) {
1962
+ const resolved = resolveImportPath(file.path, imp.key, knownPaths);
1963
+ if (resolved) fileDeps.add(resolved);
1964
+ }
1965
+ for (const stmt of parsed.statements) if (stmt.kind === "reexport") {
1966
+ const match = stmt.text.match(/from\s+['"]([^'"]+)['"]/);
1967
+ if (match) {
1968
+ const resolved = resolveImportPath(file.path, match[1], knownPaths);
1969
+ if (resolved) fileDeps.add(resolved);
1970
+ }
1971
+ }
1972
+ deps.set(file.path, fileDeps);
1973
+ }
1974
+ const reachable = /* @__PURE__ */ new Set();
1975
+ const queue = [];
1976
+ for (const file of files) if (isRootFile(file.path)) {
1977
+ reachable.add(file.path);
1978
+ queue.push(file.path);
1979
+ }
1980
+ while (queue.length > 0) {
1981
+ const current = queue.pop();
1982
+ const fileDeps = deps.get(current);
1983
+ if (!fileDeps) continue;
1984
+ for (const dep of fileDeps) if (!reachable.has(dep)) {
1985
+ reachable.add(dep);
1986
+ queue.push(dep);
1987
+ }
1988
+ }
1989
+ return files.filter((f) => reachable.has(f.path));
1990
+ }
1991
+ function mapFilesForTargetIntegration(files, language) {
1992
+ const langPrefix = `${language}/`;
1993
+ return files.filter((f) => f.integrateTarget !== false).map((f) => {
1994
+ const stripped = f.path.startsWith(langPrefix) ? f.path.replace(langPrefix, "") : f.path;
1995
+ return {
1996
+ ...f,
1997
+ skipIfExists: false,
1998
+ path: stripped
1999
+ };
2000
+ });
2001
+ }
2002
+ async function integrateGeneratedFiles(opts) {
2003
+ return writeFiles(await treeShakeFiles(mapFilesForTargetIntegration(opts.files, opts.language), opts.language), opts.targetDir, {
2004
+ language: opts.language,
2005
+ header: opts.header
2006
+ });
2007
+ }
2008
+ //#endregion
2009
+ //#region src/engine/formatter.ts
2010
+ /**
2011
+ * Post-integration formatter.
2012
+ *
2013
+ * After generating and merging files into a target directory, runs the
2014
+ * emitter-provided format command on all written/merged files so that
2015
+ * generated code matches the target project's style conventions.
2016
+ */
2017
+ const execFileAsync = promisify(execFile);
2018
+ /**
2019
+ * Run the emitter's format command on the given target files.
2020
+ * Silently skips if the emitter has no formatCommand or returns null.
2021
+ */
2022
+ async function formatTargetFiles(emitter, targetDir, filePaths) {
2023
+ if (filePaths.length === 0) return;
2024
+ if (!emitter.formatCommand) return;
2025
+ const formatCmd = emitter.formatCommand(targetDir);
2026
+ if (!formatCmd) return;
2027
+ const absolutePaths = filePaths.map((f) => path$1.resolve(targetDir, f));
2028
+ const batchSize = formatCmd.batchSize ?? 100;
2029
+ for (let i = 0; i < absolutePaths.length; i += batchSize) {
2030
+ const batch = absolutePaths.slice(i, i + batchSize);
2031
+ try {
2032
+ await execFileAsync(formatCmd.cmd, [...formatCmd.args, ...batch], {
2033
+ cwd: targetDir,
2034
+ maxBuffer: 10 * 1024 * 1024,
2035
+ timeout: 12e4
2036
+ });
2037
+ } catch {}
2038
+ }
2039
+ }
2040
+ //#endregion
2041
+ //#region src/engine/orchestrator.ts
2042
+ async function generate(spec, emitter, options) {
2043
+ const { files: withHeaders, header } = generateFiles(spec, emitter, options);
2044
+ if (options.dryRun) {
2045
+ if (options.target) {
2046
+ console.log(`\nTarget integration (${options.target}):`);
2047
+ for (const f of withHeaders) console.log(` ${f.path}`);
2048
+ }
2049
+ return withHeaders;
2050
+ }
2051
+ const writeResult = await writeFiles(withHeaders, options.outputDir, {
2052
+ language: emitter.language,
2053
+ header
2054
+ });
2055
+ if (writeResult.merged.length > 0) console.log(`Merged into ${writeResult.merged.length} existing files (additive only)`);
2056
+ if (writeResult.ignored.length > 0) console.log(`Ignored ${writeResult.ignored.length} files (@oagen-ignore-file)`);
2057
+ if (options.target) {
2058
+ const targetResult = await integrateGeneratedFiles({
2059
+ files: withHeaders,
2060
+ language: emitter.language,
2061
+ targetDir: options.target,
2062
+ header
2063
+ });
2064
+ if (targetResult.written.length > 0) console.log(`Target: created ${targetResult.written.length} new files`);
2065
+ if (targetResult.merged.length > 0) console.log(`Target: merged into ${targetResult.merged.length} existing files (additive only)`);
2066
+ if (targetResult.skipped.length > 0) console.log(`Target: skipped ${targetResult.skipped.length} files (excluded or no grammar)`);
2067
+ const allTargetFiles = [...targetResult.written, ...targetResult.merged];
2068
+ if (allTargetFiles.length > 0) await formatTargetFiles(emitter, options.target, allTargetFiles);
2069
+ }
2070
+ return withHeaders;
2071
+ }
2072
+ //#endregion
2073
+ //#region src/engine/registry.ts
2074
+ const emitters = /* @__PURE__ */ new Map();
2075
+ function registerEmitter(emitter) {
2076
+ emitters.set(emitter.language, emitter);
2077
+ }
2078
+ function getEmitter(language) {
2079
+ const emitter = emitters.get(language);
2080
+ if (!emitter) throw new RegistryError(`Unknown language: ${language}. Available: ${[...emitters.keys()].join(", ") || "(none)"}`, `Register an emitter for "${language}" via registerEmitter() or check your oagen.config.ts plugin configuration.`);
2081
+ return emitter;
2082
+ }
2083
+ //#endregion
2084
+ //#region src/differ/classify.ts
2085
+ function classifyFieldChange(kind, fieldName, isRequired) {
2086
+ switch (kind) {
2087
+ case "field-added": return {
2088
+ kind,
2089
+ fieldName,
2090
+ classification: isRequired ? "breaking" : "additive"
2091
+ };
2092
+ case "field-removed": return {
2093
+ kind,
2094
+ fieldName,
2095
+ classification: "breaking"
2096
+ };
2097
+ case "field-type-changed": return {
2098
+ kind,
2099
+ fieldName,
2100
+ classification: "breaking"
2101
+ };
2102
+ case "field-required-changed": return {
2103
+ kind,
2104
+ fieldName,
2105
+ classification: isRequired ? "breaking" : "additive",
2106
+ details: isRequired ? "optional → required" : "required → optional"
2107
+ };
2108
+ case "field-access-changed": return {
2109
+ kind,
2110
+ fieldName,
2111
+ classification: "breaking"
2112
+ };
2113
+ }
2114
+ }
2115
+ function classifyParamChange(kind, paramName, isRequired) {
2116
+ switch (kind) {
2117
+ case "param-added": return {
2118
+ kind,
2119
+ paramName,
2120
+ classification: isRequired ? "breaking" : "additive"
2121
+ };
2122
+ case "param-removed": return {
2123
+ kind,
2124
+ paramName,
2125
+ classification: "breaking"
2126
+ };
2127
+ case "param-type-changed": return {
2128
+ kind,
2129
+ paramName,
2130
+ classification: "breaking"
2131
+ };
2132
+ case "param-required-changed": return {
2133
+ kind,
2134
+ paramName,
2135
+ classification: isRequired ? "breaking" : "additive"
2136
+ };
2137
+ }
2138
+ }
2139
+ //#endregion
2140
+ //#region src/differ/models.ts
2141
+ function diffModels(oldModels, newModels) {
2142
+ const changes = [];
2143
+ const oldByName = new Map(oldModels.map((m) => [m.name, m]));
2144
+ const newByName = new Map(newModels.map((m) => [m.name, m]));
2145
+ for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
2146
+ kind: "model-added",
2147
+ name,
2148
+ classification: "additive"
2149
+ });
2150
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
2151
+ kind: "model-removed",
2152
+ name,
2153
+ classification: "breaking"
2154
+ });
2155
+ for (const [name, newModel] of newByName) {
2156
+ const oldModel = oldByName.get(name);
2157
+ if (!oldModel) continue;
2158
+ const fieldChanges = diffFields(oldModel, newModel);
2159
+ if (fieldChanges.length > 0) {
2160
+ const hasBreaking = fieldChanges.some((fc) => fc.classification === "breaking");
2161
+ changes.push({
2162
+ kind: "model-modified",
2163
+ name,
2164
+ fieldChanges,
2165
+ classification: hasBreaking ? "breaking" : "additive"
2166
+ });
2167
+ }
2168
+ }
2169
+ return changes;
2170
+ }
2171
+ function diffFields(oldModel, newModel) {
2172
+ const changes = [];
2173
+ const oldByName = new Map(oldModel.fields.map((f) => [f.name, f]));
2174
+ const newByName = new Map(newModel.fields.map((f) => [f.name, f]));
2175
+ for (const [name, field] of newByName) if (!oldByName.has(name)) changes.push(classifyFieldChange("field-added", name, field.required));
2176
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push(classifyFieldChange("field-removed", name));
2177
+ for (const [name, newField] of newByName) {
2178
+ const oldField = oldByName.get(name);
2179
+ if (!oldField) continue;
2180
+ if (!typeRefsEqual(oldField.type, newField.type)) changes.push({
2181
+ kind: "field-type-changed",
2182
+ fieldName: name,
2183
+ classification: "breaking",
2184
+ details: `type changed`
2185
+ });
2186
+ if (oldField.required !== newField.required) changes.push(classifyFieldChange("field-required-changed", name, newField.required));
2187
+ if (!!oldField.readOnly !== !!newField.readOnly || !!oldField.writeOnly !== !!newField.writeOnly) changes.push(classifyFieldChange("field-access-changed", name));
2188
+ }
2189
+ return changes;
2190
+ }
2191
+ function typeRefsEqual(a, b) {
2192
+ if (a.kind !== b.kind) return false;
2193
+ switch (a.kind) {
2194
+ case "primitive": return b.kind === "primitive" && a.type === b.type && (a.format ?? "") === (b.format ?? "");
2195
+ case "array": return b.kind === "array" && typeRefsEqual(a.items, b.items);
2196
+ case "model": return b.kind === "model" && a.name === b.name;
2197
+ case "enum": return b.kind === "enum" && a.name === b.name;
2198
+ case "literal": return b.kind === "literal" && a.value === b.value;
2199
+ case "nullable": return b.kind === "nullable" && typeRefsEqual(a.inner, b.inner);
2200
+ case "union": {
2201
+ if (b.kind !== "union") return false;
2202
+ if (a.variants.length !== b.variants.length) return false;
2203
+ if (!a.variants.every((v, i) => typeRefsEqual(v, b.variants[i]))) return false;
2204
+ if (!a.discriminator && !b.discriminator) return true;
2205
+ if (!a.discriminator || !b.discriminator) return false;
2206
+ if (a.discriminator.property !== b.discriminator.property) return false;
2207
+ const aKeys = Object.keys(a.discriminator.mapping).sort();
2208
+ const bKeys = Object.keys(b.discriminator.mapping).sort();
2209
+ if (aKeys.length !== bKeys.length) return false;
2210
+ return aKeys.every((k, i) => k === bKeys[i] && a.discriminator.mapping[k] === b.discriminator.mapping[k]);
2211
+ }
2212
+ case "map": return b.kind === "map" && typeRefsEqual(a.valueType, b.valueType);
2213
+ }
2214
+ return assertNever(a);
2215
+ }
2216
+ //#endregion
2217
+ //#region src/differ/operations.ts
2218
+ function diffOperations(serviceName, oldOps, newOps) {
2219
+ const changes = [];
2220
+ const oldByName = new Map(oldOps.map((o) => [o.name, o]));
2221
+ const newByName = new Map(newOps.map((o) => [o.name, o]));
2222
+ for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
2223
+ kind: "operation-added",
2224
+ serviceName,
2225
+ operationName: name,
2226
+ classification: "additive"
2227
+ });
2228
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
2229
+ kind: "operation-removed",
2230
+ serviceName,
2231
+ operationName: name,
2232
+ classification: "breaking"
2233
+ });
2234
+ for (const [name, newOp] of newByName) {
2235
+ const oldOp = oldByName.get(name);
2236
+ if (!oldOp) continue;
2237
+ const paramChanges = diffParams(oldOp, newOp);
2238
+ const responseChanged = !typeRefsEqual(oldOp.response, newOp.response);
2239
+ const requestBodyChanged = !requestBodiesEqual(oldOp.requestBody, newOp.requestBody);
2240
+ const httpMethodChanged = oldOp.httpMethod !== newOp.httpMethod;
2241
+ const pathChanged = oldOp.path !== newOp.path;
2242
+ const paginatedChanged = !!oldOp.pagination !== !!newOp.pagination;
2243
+ const injectIdempotencyKeyChanged = oldOp.injectIdempotencyKey !== newOp.injectIdempotencyKey;
2244
+ const errorsDiff = classifyErrorsChange(oldOp.errors, newOp.errors);
2245
+ const errorsChanged = errorsDiff !== "none";
2246
+ if (paramChanges.length > 0 || responseChanged || requestBodyChanged || httpMethodChanged || pathChanged || paginatedChanged || injectIdempotencyKeyChanged || errorsChanged) {
2247
+ const paginatedBreaking = paginatedChanged && !newOp.pagination;
2248
+ const injectIdempotencyKeyBreaking = injectIdempotencyKeyChanged && !newOp.injectIdempotencyKey;
2249
+ const hasBreaking = responseChanged || requestBodyChanged || httpMethodChanged || pathChanged || paginatedBreaking || injectIdempotencyKeyBreaking || errorsDiff === "breaking" || paramChanges.some((pc) => pc.classification === "breaking");
2250
+ changes.push({
2251
+ kind: "operation-modified",
2252
+ serviceName,
2253
+ operationName: name,
2254
+ paramChanges,
2255
+ responseChanged,
2256
+ requestBodyChanged,
2257
+ httpMethodChanged,
2258
+ pathChanged,
2259
+ paginatedChanged,
2260
+ injectIdempotencyKeyChanged,
2261
+ errorsChanged,
2262
+ classification: hasBreaking ? "breaking" : "additive"
2263
+ });
2264
+ }
2265
+ }
2266
+ return changes;
2267
+ }
2268
+ function requestBodiesEqual(a, b) {
2269
+ if (!a && !b) return true;
2270
+ if (!a || !b) return false;
2271
+ return typeRefsEqual(a, b);
2272
+ }
2273
+ function classifyErrorsChange(oldErrors, newErrors) {
2274
+ const oldByCode = new Map(oldErrors.map((e) => [e.statusCode, e]));
2275
+ const newByCode = new Map(newErrors.map((e) => [e.statusCode, e]));
2276
+ let hasAdditive = false;
2277
+ let hasBreaking = false;
2278
+ for (const code of newByCode.keys()) if (!oldByCode.has(code)) hasAdditive = true;
2279
+ for (const code of oldByCode.keys()) if (!newByCode.has(code)) hasBreaking = true;
2280
+ for (const [code, newErr] of newByCode) {
2281
+ const oldErr = oldByCode.get(code);
2282
+ if (!oldErr) continue;
2283
+ if (!oldErr.type && !newErr.type) continue;
2284
+ if (!oldErr.type || !newErr.type || !typeRefsEqual(oldErr.type, newErr.type)) hasBreaking = true;
2285
+ }
2286
+ if (hasBreaking) return "breaking";
2287
+ if (hasAdditive) return "additive";
2288
+ return "none";
2289
+ }
2290
+ function diffParams(oldOp, newOp) {
2291
+ const changes = [];
2292
+ const allOldParams = [
2293
+ ...oldOp.pathParams,
2294
+ ...oldOp.queryParams,
2295
+ ...oldOp.headerParams
2296
+ ];
2297
+ const allNewParams = [
2298
+ ...newOp.pathParams,
2299
+ ...newOp.queryParams,
2300
+ ...newOp.headerParams
2301
+ ];
2302
+ const oldByName = new Map(allOldParams.map((p) => [p.name, p]));
2303
+ const newByName = new Map(allNewParams.map((p) => [p.name, p]));
2304
+ for (const [name, param] of newByName) if (!oldByName.has(name)) changes.push(classifyParamChange("param-added", name, param.required));
2305
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push(classifyParamChange("param-removed", name));
2306
+ for (const [name, newParam] of newByName) {
2307
+ const oldParam = oldByName.get(name);
2308
+ if (!oldParam) continue;
2309
+ if (!typeRefsEqual(oldParam.type, newParam.type)) changes.push(classifyParamChange("param-type-changed", name));
2310
+ if (oldParam.required !== newParam.required) changes.push(classifyParamChange("param-required-changed", name, newParam.required));
2311
+ }
2312
+ return changes;
2313
+ }
2314
+ //#endregion
2315
+ //#region src/differ/services.ts
2316
+ function diffServices(oldServices, newServices) {
2317
+ const changes = [];
2318
+ const oldByName = new Map(oldServices.map((s) => [s.name, s]));
2319
+ const newByName = new Map(newServices.map((s) => [s.name, s]));
2320
+ for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
2321
+ kind: "service-added",
2322
+ name,
2323
+ classification: "additive"
2324
+ });
2325
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
2326
+ kind: "service-removed",
2327
+ name,
2328
+ classification: "breaking"
2329
+ });
2330
+ for (const [name, newService] of newByName) {
2331
+ const oldService = oldByName.get(name);
2332
+ if (!oldService) continue;
2333
+ changes.push(...diffOperations(name, oldService.operations, newService.operations));
2334
+ }
2335
+ return changes;
2336
+ }
2337
+ //#endregion
2338
+ //#region src/differ/diff.ts
2339
+ function diffSpecs(oldSpec, newSpec) {
2340
+ const changes = [
2341
+ ...diffModels(oldSpec.models, newSpec.models),
2342
+ ...diffEnums(oldSpec.enums, newSpec.enums),
2343
+ ...diffServices(oldSpec.services, newSpec.services)
2344
+ ];
2345
+ return {
2346
+ oldVersion: oldSpec.version,
2347
+ newVersion: newSpec.version,
2348
+ changes,
2349
+ summary: summarize(changes)
2350
+ };
2351
+ }
2352
+ function diffEnums(oldEnums, newEnums) {
2353
+ const changes = [];
2354
+ const oldByName = new Map(oldEnums.map((e) => [e.name, e]));
2355
+ const newByName = new Map(newEnums.map((e) => [e.name, e]));
2356
+ for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
2357
+ kind: "enum-added",
2358
+ name,
2359
+ classification: "additive"
2360
+ });
2361
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
2362
+ kind: "enum-removed",
2363
+ name,
2364
+ classification: "breaking"
2365
+ });
2366
+ for (const [name, newEnum] of newByName) {
2367
+ const oldEnum = oldByName.get(name);
2368
+ if (!oldEnum) continue;
2369
+ const valueChanges = diffEnumValues(oldEnum, newEnum);
2370
+ if (valueChanges.length > 0) {
2371
+ const hasBreaking = valueChanges.some((vc) => vc.classification === "breaking");
2372
+ changes.push({
2373
+ kind: "enum-modified",
2374
+ name,
2375
+ valueChanges,
2376
+ classification: hasBreaking ? "breaking" : "additive"
2377
+ });
2378
+ }
2379
+ }
2380
+ return changes;
2381
+ }
2382
+ function diffEnumValues(oldEnum, newEnum) {
2383
+ const changes = [];
2384
+ const oldByName = new Map(oldEnum.values.map((v) => [v.name, v]));
2385
+ const newByName = new Map(newEnum.values.map((v) => [v.name, v]));
2386
+ for (const [name] of newByName) if (!oldByName.has(name)) changes.push({
2387
+ kind: "value-added",
2388
+ valueName: name,
2389
+ classification: "additive"
2390
+ });
2391
+ for (const [name] of oldByName) if (!newByName.has(name)) changes.push({
2392
+ kind: "value-removed",
2393
+ valueName: name,
2394
+ classification: "breaking"
2395
+ });
2396
+ for (const [name, newVal] of newByName) {
2397
+ const oldVal = oldByName.get(name);
2398
+ if (!oldVal) continue;
2399
+ if (oldVal.value !== newVal.value) changes.push({
2400
+ kind: "value-changed",
2401
+ valueName: name,
2402
+ classification: "breaking",
2403
+ details: `value changed from '${oldVal.value}' to '${newVal.value}'`
2404
+ });
2405
+ }
2406
+ return changes;
2407
+ }
2408
+ function summarize(changes) {
2409
+ let added = 0;
2410
+ let removed = 0;
2411
+ let modified = 0;
2412
+ let breaking = 0;
2413
+ let additive = 0;
2414
+ for (const c of changes) {
2415
+ if (c.kind.endsWith("-added")) added++;
2416
+ else if (c.kind.endsWith("-removed")) removed++;
2417
+ else if (c.kind.endsWith("-modified")) modified++;
2418
+ if (c.classification === "breaking") breaking++;
2419
+ else additive++;
2420
+ }
2421
+ return {
2422
+ added,
2423
+ removed,
2424
+ modified,
2425
+ breaking,
2426
+ additive
2427
+ };
2428
+ }
2429
+ //#endregion
2430
+ export { integrateGeneratedFiles as a, applyFileHeaders as c, generateAllFiles as d, generateFiles as f, generate as i, buildEmitterContext as l, getEmitter as n, mapFilesForTargetIntegration as o, registerEmitter as r, treeShakeFiles as s, diffSpecs as t, collectReferencedNames as u };
2431
+
2432
+ //# sourceMappingURL=diff-Cn6S74vV.mjs.map