@qa-gentic/stlc-agents 1.0.27 → 1.0.28

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 (90) hide show
  1. package/ARCHITECTURE-ADO.md +350 -0
  2. package/ARCHITECTURE-JIRA.md +203 -0
  3. package/QUICKSTART-ADO.md +400 -0
  4. package/QUICKSTART-JIRA.md +334 -0
  5. package/README.md +49 -0
  6. package/package.json +18 -6
  7. package/skills/migrate-framework/SKILL.md +207 -0
  8. package/src/stlc_agents/__pycache__/__init__.cpython-313.pyc +0 -0
  9. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-313.pyc +0 -0
  10. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-313.pyc +0 -0
  11. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  12. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-313.pyc +0 -0
  13. package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-313.pyc +0 -0
  14. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-313.pyc +0 -0
  15. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  16. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-313.pyc +0 -0
  17. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-313.pyc +0 -0
  18. package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-313.pyc +0 -0
  20. package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  21. package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-313.pyc +0 -0
  22. package/src/stlc_agents/agent_migration/__init__.py +0 -0
  23. package/src/stlc_agents/agent_migration/__pycache__/__init__.cpython-313.pyc +0 -0
  24. package/src/stlc_agents/agent_migration/__pycache__/_migrate.cpython-313.pyc +0 -0
  25. package/src/stlc_agents/agent_migration/__pycache__/cli.cpython-313.pyc +0 -0
  26. package/src/stlc_agents/agent_migration/__pycache__/detector.cpython-313.pyc +0 -0
  27. package/src/stlc_agents/agent_migration/__pycache__/mapper.cpython-313.pyc +0 -0
  28. package/src/stlc_agents/agent_migration/__pycache__/reporter.cpython-313.pyc +0 -0
  29. package/src/stlc_agents/agent_migration/__pycache__/server.cpython-313.pyc +0 -0
  30. package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
  31. package/src/stlc_agents/agent_migration/cli.py +217 -0
  32. package/src/stlc_agents/agent_migration/detector.py +81 -0
  33. package/src/stlc_agents/agent_migration/mapper.py +439 -0
  34. package/src/stlc_agents/agent_migration/reporter.py +86 -0
  35. package/src/stlc_agents/agent_migration/server.py +267 -0
  36. package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
  37. package/src/stlc_agents/agent_migration/transformer/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/src/stlc_agents/agent_migration/transformer/__pycache__/config_merger.cpython-313.pyc +0 -0
  39. package/src/stlc_agents/agent_migration/transformer/__pycache__/healer_injector.cpython-313.pyc +0 -0
  40. package/src/stlc_agents/agent_migration/transformer/__pycache__/import_fixer.cpython-313.pyc +0 -0
  41. package/src/stlc_agents/agent_migration/transformer/__pycache__/js_to_ts.cpython-313.pyc +0 -0
  42. package/src/stlc_agents/agent_migration/transformer/__pycache__/local_var_hoister.cpython-313.pyc +0 -0
  43. package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_moderniser.cpython-313.pyc +0 -0
  44. package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_registrar.cpython-313.pyc +0 -0
  45. package/src/stlc_agents/agent_migration/transformer/__pycache__/spec_to_bdd.cpython-313.pyc +0 -0
  46. package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
  47. package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
  48. package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
  49. package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
  50. package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
  51. package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
  52. package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
  53. package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
  54. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-313.pyc +0 -0
  56. package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
  57. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-313.pyc +0 -0
  59. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-313.pyc +0 -0
  60. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-313.pyc +0 -0
  61. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  62. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-313.pyc +0 -0
  63. package/src/stlc_agents/shared/__pycache__/__init__.cpython-313.pyc +0 -0
  64. package/src/stlc_agents/shared/__pycache__/auth.cpython-313.pyc +0 -0
  65. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-313.pyc +0 -0
  66. package/src/stlc_agents/shared/__pycache__/pricing.cpython-313.pyc +0 -0
  67. package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-313.pyc +0 -0
  68. package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-313.pyc +0 -0
  69. package/src/stlc_agents/webhook_orchestrator/__pycache__/__init__.cpython-313.pyc +0 -0
  70. package/src/stlc_agents/webhook_orchestrator/__pycache__/agent_runner.cpython-313.pyc +0 -0
  71. package/src/stlc_agents/webhook_orchestrator/__pycache__/models.cpython-313.pyc +0 -0
  72. package/src/stlc_agents/webhook_orchestrator/__pycache__/orchestrator.cpython-313.pyc +0 -0
  73. package/src/stlc_agents/webhook_orchestrator/__pycache__/webhook_bridge.cpython-313.pyc +0 -0
  74. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  75. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
  76. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  77. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
  78. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
  79. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  80. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
  81. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  82. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
  83. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
  84. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
  85. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  86. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
  87. package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
  88. package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
  89. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
  90. package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
@@ -0,0 +1,413 @@
1
+ """Convert CommonJS JavaScript to TypeScript ES-module syntax."""
2
+ from __future__ import annotations
3
+ import re
4
+
5
+
6
+ def convert_js_to_ts(content: str) -> tuple[str, list[str]]:
7
+ """
8
+ Convert a JS file to TS.
9
+ Returns (transformed_content, list_of_changes).
10
+ """
11
+ lines = content.split("\n")
12
+ result = []
13
+ changes: list[str] = []
14
+
15
+ for line in lines:
16
+ line = _transform_line(line, changes)
17
+ result.append(line)
18
+
19
+ joined = "\n".join(result)
20
+ joined, rt_changes = _add_return_types(joined)
21
+ changes.extend(rt_changes)
22
+ joined, cls_changes = _add_class_field_decls(joined)
23
+ changes.extend(cls_changes)
24
+ joined, ctor_changes = _annotate_constructor_params(joined)
25
+ changes.extend(ctor_changes)
26
+ joined, dup_changes = _rename_duplicate_methods(joined)
27
+ changes.extend(dup_changes)
28
+ joined, intl_changes = _cast_intl_format_options(joined)
29
+ changes.extend(intl_changes)
30
+ joined, tobe_changes = _fix_tobe_typo(joined)
31
+ changes.extend(tobe_changes)
32
+ return joined, changes
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # `.tobe.<value>(<msg>)` → `.toBe(<value>)`
37
+ #
38
+ # JS doesn't catch this — `expect(x).tobe` evaluates to undefined and only
39
+ # throws at runtime when `.<value>(...)` is called. TS rejects it at compile
40
+ # time. The clear intent is `.toBe(<value>)`; any trailing args were going
41
+ # to fail anyway so we drop them with a MIGRATION TODO.
42
+ # ---------------------------------------------------------------------------
43
+
44
+ def _fix_tobe_typo(content: str) -> tuple[str, list[str]]:
45
+ changes: list[str] = []
46
+
47
+ def _repl(m: re.Match) -> str:
48
+ value = m.group(1)
49
+ args = m.group(2).strip()
50
+ changes.append(f".tobe.{value}({args}) → .toBe({value}) (dropped invalid args)")
51
+ if args:
52
+ return f".toBe({value}) /* MIGRATION TODO: original second arg was {args!r} */"
53
+ return f".toBe({value})"
54
+
55
+ content = re.sub(
56
+ r"\.tobe\.(\w+)\(([^)]*)\)",
57
+ _repl,
58
+ content,
59
+ )
60
+ return content, changes
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Duplicate-method renaming
65
+ #
66
+ # JS allows two methods with the same name in a class — the second silently
67
+ # overrides the first. TypeScript flags this with TS2393 "Duplicate function
68
+ # implementation". Rename the second (and later) occurrences with a numeric
69
+ # suffix so the user can see both and decide which to keep.
70
+ # ---------------------------------------------------------------------------
71
+
72
+ def _rename_duplicate_methods(content: str) -> tuple[str, list[str]]:
73
+ changes: list[str] = []
74
+ out: list[str] = []
75
+ cursor = 0
76
+ class_re = re.compile(
77
+ r"\bclass\s+\w+(?:\s+extends\s+[\w.<>\[\]\s,]+?)?\s*\{",
78
+ )
79
+ for m in class_re.finditer(content):
80
+ body_open = m.end() - 1
81
+ body_close = _find_matching_brace(content, body_open)
82
+ if body_close < 0:
83
+ continue
84
+ body = content[body_open + 1 : body_close]
85
+
86
+ # Find every method definition: `[async] name(args) {`
87
+ method_re = re.compile(
88
+ r"(^\s+)(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*[\w.<>\[\]\s|&,]+)?\s*\{",
89
+ re.MULTILINE,
90
+ )
91
+ # Track name → count of occurrences so far
92
+ seen: dict[str, int] = {}
93
+ # Build replacements as a list of (span_in_body, new_text)
94
+ replacements: list[tuple[int, int, str]] = []
95
+ for mm in method_re.finditer(body):
96
+ name = mm.group(2)
97
+ if name in {"constructor", "if", "for", "while", "switch", "return", "function", "async"}:
98
+ continue
99
+ seen[name] = seen.get(name, 0) + 1
100
+ if seen[name] == 1:
101
+ continue
102
+ new_name = f"{name}_{seen[name]}"
103
+ # Find the position of the method name within the match.
104
+ name_start = mm.start() + mm.group(0).find(name)
105
+ name_end = name_start + len(name)
106
+ replacements.append((name_start, name_end, new_name))
107
+ changes.append(
108
+ f"Renamed duplicate method {name} → {new_name} "
109
+ f"(JS allowed; TS does not)"
110
+ )
111
+
112
+ if not replacements:
113
+ continue
114
+ # Apply replacements right-to-left so indices stay valid.
115
+ new_body = body
116
+ for start, end, repl in sorted(replacements, key=lambda t: t[0], reverse=True):
117
+ new_body = new_body[:start] + repl + new_body[end:]
118
+
119
+ out.append(content[cursor : body_open + 1])
120
+ out.append(new_body)
121
+ cursor = body_close
122
+ out.append(content[cursor:])
123
+ return "".join(out), changes
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Intl.DateTimeFormatOptions cast
128
+ #
129
+ # `date.toLocaleDateString("en-US", options)` — JS accepts any object; TS
130
+ # requires `Intl.DateTimeFormatOptions` whose string fields are literal types.
131
+ # Casting the second arg with `as Intl.DateTimeFormatOptions` keeps the
132
+ # runtime identical while satisfying tsc.
133
+ # ---------------------------------------------------------------------------
134
+
135
+ def _cast_intl_format_options(content: str) -> tuple[str, list[str]]:
136
+ changes: list[str] = []
137
+
138
+ def _repl(m: re.Match) -> str:
139
+ full = m.group(0)
140
+ # Skip if already cast.
141
+ if "as Intl.DateTimeFormatOptions" in full:
142
+ return full
143
+ receiver, locale, opts = m.group(1), m.group(2), m.group(3).strip()
144
+ if not opts:
145
+ return full
146
+ changes.append(f"Cast toLocaleDateString options as Intl.DateTimeFormatOptions")
147
+ return f"{receiver}.toLocaleDateString({locale}, {opts} as Intl.DateTimeFormatOptions)"
148
+
149
+ content = re.sub(
150
+ r"([\w.\[\]'\"]+)\s*\.\s*toLocaleDateString\(\s*([^,)]+)\s*,\s*([^)]+)\)",
151
+ _repl,
152
+ content,
153
+ )
154
+ return content, changes
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Class field declarations — surface `this.X = ...` constructor assignments
159
+ # as `X: any;` declarations so TypeScript doesn't complain that "Property X
160
+ # does not exist on type Foo". JS classes never need declarations; TS does.
161
+ # ---------------------------------------------------------------------------
162
+
163
+ def _add_class_field_decls(content: str) -> tuple[str, list[str]]:
164
+ changes: list[str] = []
165
+ out: list[str] = []
166
+ cursor = 0
167
+ # Find each `class Foo { ... }` opening. Handles `class Foo extends Bar` too.
168
+ class_re = re.compile(
169
+ r"\bclass\s+\w+(?:\s+extends\s+[\w.<>\[\]\s,]+?)?\s*\{",
170
+ )
171
+ for m in class_re.finditer(content):
172
+ body_open = m.end() - 1 # index of `{`
173
+ body_close = _find_matching_brace(content, body_open)
174
+ if body_close < 0:
175
+ continue
176
+ body = content[body_open + 1 : body_close]
177
+ # Find constructor and collect `this.X = ...` names.
178
+ names = _extract_constructor_this_assignments(body)
179
+ if not names:
180
+ continue
181
+ # Avoid re-declaring fields the class already has.
182
+ existing = set(re.findall(r"^\s*(?:private|public|protected|readonly)?\s*(\w+)[!:?=\s]", body, re.MULTILINE))
183
+ new_decls = [n for n in names if n not in existing]
184
+ if not new_decls:
185
+ continue
186
+ # Indent: match the existing body indentation if any (default 2 spaces).
187
+ indent_match = re.search(r"\n(\s+)", body)
188
+ indent = indent_match.group(1) if indent_match else " "
189
+ decls = "".join(f"\n{indent}{n}: any;" for n in new_decls)
190
+ # Splice the declarations just after the class opening `{`.
191
+ out.append(content[cursor : body_open + 1])
192
+ out.append(decls)
193
+ cursor = body_open + 1
194
+ changes.extend(f"Added class field declaration: {n}: any" for n in new_decls)
195
+ out.append(content[cursor:])
196
+ return "".join(out), changes
197
+
198
+
199
+ def _extract_constructor_this_assignments(class_body: str) -> list[str]:
200
+ """Return ordered, deduplicated list of `X` names from `this.X = ...`."""
201
+ m = re.search(r"\bconstructor\s*\(", class_body)
202
+ if not m:
203
+ return []
204
+ paren_open = m.end() - 1
205
+ paren_close = _find_matching_paren(class_body, paren_open)
206
+ if paren_close < 0:
207
+ return []
208
+ # Find the `{` that opens the constructor body.
209
+ brace_open = class_body.find("{", paren_close)
210
+ if brace_open < 0:
211
+ return []
212
+ brace_close = _find_matching_brace(class_body, brace_open)
213
+ if brace_close < 0:
214
+ return []
215
+ ctor_body = class_body[brace_open + 1 : brace_close]
216
+ names: list[str] = []
217
+ seen: set[str] = set()
218
+ for am in re.finditer(r"\bthis\.(\w+)\s*=", ctor_body):
219
+ n = am.group(1)
220
+ if n in seen:
221
+ continue
222
+ # Skip already-declared private healer fields injected later — they
223
+ # follow a `_` prefix convention.
224
+ if n.startswith("_"):
225
+ continue
226
+ names.append(n)
227
+ seen.add(n)
228
+ return names
229
+
230
+
231
+ def _annotate_constructor_params(content: str) -> tuple[str, list[str]]:
232
+ """Add `: any` to untyped constructor parameters."""
233
+ changes: list[str] = []
234
+
235
+ def _do(m: re.Match) -> str:
236
+ params = m.group(1)
237
+ if not params.strip():
238
+ return m.group(0)
239
+ annotated = []
240
+ for p in [p.strip() for p in params.split(",")]:
241
+ if not p:
242
+ continue
243
+ if ":" in p: # already typed
244
+ annotated.append(p)
245
+ else:
246
+ annotated.append(f"{p}: any")
247
+ if annotated == [p.strip() for p in params.split(",") if p.strip()]:
248
+ return m.group(0)
249
+ changes.append("Annotated constructor params as any")
250
+ return f"constructor({', '.join(annotated)})"
251
+
252
+ content = re.sub(r"\bconstructor\(([^)]*)\)", _do, content)
253
+ return content, changes
254
+
255
+
256
+ def _find_matching_brace(text: str, open_idx: int) -> int:
257
+ depth = 1
258
+ i = open_idx + 1
259
+ in_str: str | None = None
260
+ while i < len(text):
261
+ c = text[i]
262
+ if in_str:
263
+ if c == "\\":
264
+ i += 2
265
+ continue
266
+ if c == in_str:
267
+ in_str = None
268
+ i += 1
269
+ continue
270
+ if c in ("'", '"', "`"):
271
+ in_str = c
272
+ i += 1
273
+ continue
274
+ if c == "/" and i + 1 < len(text):
275
+ if text[i + 1] == "/":
276
+ nl = text.find("\n", i)
277
+ i = nl + 1 if nl >= 0 else len(text)
278
+ continue
279
+ if text[i + 1] == "*":
280
+ end = text.find("*/", i + 2)
281
+ i = end + 2 if end >= 0 else len(text)
282
+ continue
283
+ if c == "{":
284
+ depth += 1
285
+ elif c == "}":
286
+ depth -= 1
287
+ if depth == 0:
288
+ return i
289
+ i += 1
290
+ return -1
291
+
292
+
293
+ def _find_matching_paren(text: str, open_idx: int) -> int:
294
+ depth = 1
295
+ i = open_idx + 1
296
+ while i < len(text):
297
+ c = text[i]
298
+ if c == "(":
299
+ depth += 1
300
+ elif c == ")":
301
+ depth -= 1
302
+ if depth == 0:
303
+ return i
304
+ i += 1
305
+ return -1
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Line-level transformations
310
+ # ---------------------------------------------------------------------------
311
+
312
+ def _transform_line(line: str, changes: list[str]) -> str:
313
+ # const { X, Y } = require('Z') → import { X, Y } from 'Z'
314
+ m = re.match(
315
+ r"^(\s*)const\s*\{([^}]+)\}\s*=\s*require\(['\"]([^'\"]+)['\"]\)\s*;?\s*$",
316
+ line,
317
+ )
318
+ if m:
319
+ indent, names, module = m.groups()
320
+ module = _drop_js_ext(module)
321
+ changes.append(f"require('{module}') → import {{ ... }}")
322
+ return f"{indent}import {{ {names.strip()} }} from '{module}';"
323
+
324
+ # const X = require('Z') → import X from 'Z'
325
+ m = re.match(
326
+ r"^(\s*)const\s+(\w+)\s*=\s*require\(['\"]([^'\"]+)['\"]\)\s*;?\s*$",
327
+ line,
328
+ )
329
+ if m:
330
+ indent, name, module = m.groups()
331
+ module = _drop_js_ext(module)
332
+ changes.append(f"require('{module}') → import ... from ...")
333
+ return f"{indent}import {name} from '{module}';"
334
+
335
+ # module.exports.X = Y → export const X = Y
336
+ m = re.match(r"^(\s*)module\.exports\.(\w+)\s*=(.*)$", line)
337
+ if m:
338
+ indent, name, rest = m.groups()
339
+ changes.append(f"module.exports.{name} → export const {name}")
340
+ return f"{indent}export const {name} ={rest}"
341
+
342
+ # module.exports = X → export default X
343
+ m = re.match(r"^(\s*)module\.exports\s*=(.*)$", line)
344
+ if m:
345
+ indent, rest = m.groups()
346
+ changes.append("module.exports → export default")
347
+ return f"{indent}export default{rest}"
348
+
349
+ # exports.X = Y → export const X = Y
350
+ m = re.match(r"^(\s*)exports\.(\w+)\s*=(.*)$", line)
351
+ if m:
352
+ indent, name, rest = m.groups()
353
+ changes.append(f"exports.{name} → export const {name}")
354
+ return f"{indent}export const {name} ={rest}"
355
+
356
+ # Strip .js from import paths
357
+ if "from " in line:
358
+ new_line = re.sub(
359
+ r"(from\s+['\"])([^'\"]+)\.js(['\"])",
360
+ lambda m: f"{m.group(1)}{m.group(2)}{m.group(3)}",
361
+ line,
362
+ )
363
+ if new_line != line:
364
+ changes.append("Removed .js extension from import path")
365
+ return new_line
366
+
367
+ return line
368
+
369
+
370
+ def _drop_js_ext(module: str) -> str:
371
+ return module[:-3] if module.endswith(".js") else module
372
+
373
+
374
+ # ---------------------------------------------------------------------------
375
+ # Return-type annotations
376
+ # ---------------------------------------------------------------------------
377
+
378
+ def _add_return_types(content: str) -> tuple[str, list[str]]:
379
+ changes: list[str] = []
380
+
381
+ # async function foo(params) { → async function foo(params): Promise<any> {
382
+ def _annotate_fn(m: re.Match) -> str:
383
+ full = m.group(0)
384
+ if ": Promise" in full or "): void" in full:
385
+ return full
386
+ indent, name, params = m.group(1), m.group(2), m.group(3)
387
+ changes.append(f"Added Promise<any> to async function {name}")
388
+ return f"{indent}async function {name}({params}): Promise<any> {{"
389
+
390
+ content = re.sub(
391
+ r"^(\s*)async\s+function\s+(\w+)\s*\(([^)]*)\)\s*\{",
392
+ _annotate_fn,
393
+ content,
394
+ flags=re.MULTILINE,
395
+ )
396
+
397
+ # async methodName(params) { in class bodies
398
+ def _annotate_method(m: re.Match) -> str:
399
+ full = m.group(0)
400
+ if ": Promise" in full or "): void" in full:
401
+ return full
402
+ indent, name, params = m.group(1), m.group(2), m.group(3)
403
+ changes.append(f"Added Promise<any> to async method {name}")
404
+ return f"{indent}async {name}({params}): Promise<any> {{"
405
+
406
+ content = re.sub(
407
+ r"^(\s+)async\s+(\w+)\s*\(([^)]*)\)\s*\{",
408
+ _annotate_method,
409
+ content,
410
+ flags=re.MULTILINE,
411
+ )
412
+
413
+ return content, changes