@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.
- package/ARCHITECTURE-ADO.md +350 -0
- package/ARCHITECTURE-JIRA.md +203 -0
- package/QUICKSTART-ADO.md +400 -0
- package/QUICKSTART-JIRA.md +334 -0
- package/README.md +49 -0
- package/package.json +18 -6
- package/skills/migrate-framework/SKILL.md +207 -0
- package/src/stlc_agents/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_jira_manager/tools/__pycache__/jira_workitem.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/_migrate.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/cli.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/detector.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/mapper.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/reporter.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
- package/src/stlc_agents/agent_migration/cli.py +217 -0
- package/src/stlc_agents/agent_migration/detector.py +81 -0
- package/src/stlc_agents/agent_migration/mapper.py +439 -0
- package/src/stlc_agents/agent_migration/reporter.py +86 -0
- package/src/stlc_agents/agent_migration/server.py +267 -0
- package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/config_merger.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/healer_injector.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/import_fixer.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/js_to_ts.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/local_var_hoister.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_moderniser.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/locator_registrar.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/__pycache__/spec_to_bdd.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
- package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
- package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
- package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
- package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
- package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
- package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
- package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/shared_jira/__pycache__/auth.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/agent_runner.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/models.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/src/stlc_agents/webhook_orchestrator/__pycache__/webhook_bridge.cpython-313.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
- package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Update relative import paths to match Helix-QA directory structure.
|
|
2
|
+
|
|
3
|
+
The authoritative source for "where does source file X end up in Helix?" is
|
|
4
|
+
the mapper. When `_migrate.py` orchestrates the run it builds a lookup of
|
|
5
|
+
`{absolute_source_path → helix_target}` and passes it in via `source_to_target`.
|
|
6
|
+
The import fixer uses that lookup first — only falling back to heuristic
|
|
7
|
+
prediction (`_infer_helix_path`) when the imported file isn't in the mapping
|
|
8
|
+
(e.g. the import points to something outside the source tree).
|
|
9
|
+
|
|
10
|
+
This keeps imports byte-correct after disambiguation, custom mapper rules,
|
|
11
|
+
or any other mapper logic the fixer would otherwise have to mirror.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path, PurePosixPath
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Known path aliases used in Helix projects
|
|
19
|
+
_HELIX_ALIASES = {
|
|
20
|
+
re.compile(r"[Ll]ocator[Hh]ealer"): "@utils/locators/LocatorHealer",
|
|
21
|
+
re.compile(r"[Ll]ocator[Rr]epository"): "@utils/locators/LocatorRepository",
|
|
22
|
+
re.compile(r"[Tt]iming[Hh]ealer"): "@utils/locators/TimingHealer",
|
|
23
|
+
re.compile(r"[Vv]isual[Ii]ntent"): "@utils/locators/VisualIntentChecker",
|
|
24
|
+
re.compile(r"[Hh]ealing[Dd]ashboard"): "@utils/locators/HealingDashboard",
|
|
25
|
+
re.compile(r"[Bb]ase[Pp]age"): "./BasePage",
|
|
26
|
+
re.compile(r"pageFixture|PageFixture"): "@hooks/pageFixture",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Rewrite source-project @alias/... paths to Helix equivalents.
|
|
30
|
+
# Locators and page objects that were migrated get their alias updated.
|
|
31
|
+
_SOURCE_ALIAS_RE = re.compile(
|
|
32
|
+
r"^(@pages/|@steps/|@features/)(.+?)(?:\.(ts|js))?$"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _rewrite_source_alias(path: str) -> str | None:
|
|
37
|
+
"""
|
|
38
|
+
Rewrite a source-project path alias to its Helix equivalent.
|
|
39
|
+
e.g. '@pages/amplify/ui/locators' → '@locators/ui-locators.locators'
|
|
40
|
+
'@pages/amplify/ui/login.page' → '@pages/login.page'
|
|
41
|
+
'@pages/nova/locators' → '@locators/nova-locators.locators'
|
|
42
|
+
Returns None if no rewrite is needed or possible.
|
|
43
|
+
"""
|
|
44
|
+
m = _SOURCE_ALIAS_RE.match(path)
|
|
45
|
+
if not m:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
_, tail, _ext = m.groups()
|
|
49
|
+
# `name` is the last path segment, possibly including role suffix like "editModule.page"
|
|
50
|
+
name = Path(tail).name
|
|
51
|
+
|
|
52
|
+
from stlc_agents.agent_migration.mapper import _to_kebab, _feature_kebab
|
|
53
|
+
|
|
54
|
+
# Locator files (bare "locators" or "novaLocators")
|
|
55
|
+
if re.search(r"[Ll]ocators?$", name):
|
|
56
|
+
base = re.sub(r"[Ll]ocators?$", "", name)
|
|
57
|
+
if not base:
|
|
58
|
+
# Bare "locators" file — use parent directory name as prefix
|
|
59
|
+
parent = Path(tail).parent.name.lower()
|
|
60
|
+
return f"@locators/{parent}-locators.locators"
|
|
61
|
+
return f"@locators/{_to_kebab(base)}-locators.locators"
|
|
62
|
+
|
|
63
|
+
# Page objects — name ends with ".page" (no extension) or stem ends with "Page"
|
|
64
|
+
if re.search(r"\.page$", name):
|
|
65
|
+
base = re.sub(r"\.page$", "", name)
|
|
66
|
+
return f"@pages/{_to_kebab(base)}.page"
|
|
67
|
+
if re.search(r"[Pp]age$", Path(name).stem):
|
|
68
|
+
base = re.sub(r"[Pp]age$", "", Path(name).stem)
|
|
69
|
+
return f"@pages/{_to_kebab(base)}.page"
|
|
70
|
+
|
|
71
|
+
# Step definitions
|
|
72
|
+
if re.search(r"\.steps?$", name):
|
|
73
|
+
base = re.sub(r"\.steps?$", "", name)
|
|
74
|
+
return f"@steps/{_to_kebab(base)}.steps"
|
|
75
|
+
if re.search(r"[Ss]teps?$", Path(name).stem):
|
|
76
|
+
base = re.sub(r"[Ss]teps?$", "", Path(name).stem)
|
|
77
|
+
return f"@steps/{_to_kebab(base)}.steps"
|
|
78
|
+
|
|
79
|
+
# API clients — anything ending in `.api` lives under @helper/api/ in helix-qa
|
|
80
|
+
if re.search(r"\.api$", name):
|
|
81
|
+
return f"@helper/api/{name}"
|
|
82
|
+
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def fix_imports(
|
|
87
|
+
content: str,
|
|
88
|
+
source_path: str,
|
|
89
|
+
target_path: str,
|
|
90
|
+
source_root: str,
|
|
91
|
+
source_to_target: dict[str, str] | None = None,
|
|
92
|
+
) -> tuple[str, list[str]]:
|
|
93
|
+
"""
|
|
94
|
+
Update import paths in content for the new Helix target location.
|
|
95
|
+
|
|
96
|
+
source_path — absolute path of the original file
|
|
97
|
+
target_path — Helix-relative target path (e.g. 'src/pages/login.page.ts')
|
|
98
|
+
source_root — absolute path of the source project root
|
|
99
|
+
source_to_target — authoritative {absolute_source_path → helix_target_rel}
|
|
100
|
+
lookup from the mapper. When the imported file is
|
|
101
|
+
present here, its target is used verbatim instead of
|
|
102
|
+
being predicted by the heuristic. Pass `None` (or omit)
|
|
103
|
+
to fall back to the legacy guess-only behaviour.
|
|
104
|
+
Returns (updated_content, changes).
|
|
105
|
+
"""
|
|
106
|
+
changes: list[str] = []
|
|
107
|
+
lines = content.split("\n")
|
|
108
|
+
result = []
|
|
109
|
+
s2t = source_to_target or {}
|
|
110
|
+
|
|
111
|
+
for line in lines:
|
|
112
|
+
new_line = _fix_line(line, source_path, target_path, source_root, s2t, changes)
|
|
113
|
+
result.append(new_line)
|
|
114
|
+
|
|
115
|
+
return "\n".join(result), changes
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Per-line processing
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
_IMPORT_FROM_RE = re.compile(
|
|
123
|
+
r"^(\s*(?:import|export)\s+.*?\s+from\s+)(['\"])(\.[^'\"]+)\2(.*)"
|
|
124
|
+
)
|
|
125
|
+
# Match absolute alias imports on a single line: import Foo from '@alias/...'
|
|
126
|
+
_IMPORT_ALIAS_RE = re.compile(
|
|
127
|
+
r"^(\s*(?:import|export)\s+.*?\s+from\s+)(['\"])(@[^'\"]+)\2(.*)"
|
|
128
|
+
)
|
|
129
|
+
# Match closing line of a multi-line import: } from '@alias/...'
|
|
130
|
+
_IMPORT_ALIAS_CLOSE_RE = re.compile(
|
|
131
|
+
r"^(\s*\}[ \t]+from\s+)(['\"])(@[^'\"]+)\2(.*)"
|
|
132
|
+
)
|
|
133
|
+
_REQUIRE_RE = re.compile(r"(require\s*\(\s*)(['\"])(\.[^'\"]+)\2(\s*\))")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _fix_line(
|
|
137
|
+
line: str,
|
|
138
|
+
source_path: str,
|
|
139
|
+
target_path: str,
|
|
140
|
+
source_root: str,
|
|
141
|
+
source_to_target: dict[str, str],
|
|
142
|
+
changes: list[str],
|
|
143
|
+
) -> str:
|
|
144
|
+
# ES import/export from '...' (relative paths)
|
|
145
|
+
m = _IMPORT_FROM_RE.match(line)
|
|
146
|
+
if m:
|
|
147
|
+
prefix, quote, rel_path, suffix = m.groups()
|
|
148
|
+
new_path = _remap(rel_path, source_path, target_path, source_root, source_to_target, changes)
|
|
149
|
+
if new_path != rel_path:
|
|
150
|
+
return f"{prefix}{quote}{new_path}{quote}{suffix}"
|
|
151
|
+
return line
|
|
152
|
+
|
|
153
|
+
# ES import/export from '@alias/...' (absolute alias paths from source project)
|
|
154
|
+
m = _IMPORT_ALIAS_RE.match(line) or _IMPORT_ALIAS_CLOSE_RE.match(line)
|
|
155
|
+
if m:
|
|
156
|
+
prefix, quote, alias_path, suffix = m.groups()
|
|
157
|
+
new_path = _rewrite_source_alias_via_mapping(
|
|
158
|
+
alias_path, source_root, source_to_target,
|
|
159
|
+
)
|
|
160
|
+
if new_path is None:
|
|
161
|
+
new_path = _rewrite_source_alias(alias_path)
|
|
162
|
+
if new_path and new_path != alias_path:
|
|
163
|
+
changes.append(f"Remapped alias {alias_path!r} → {new_path!r}")
|
|
164
|
+
return f"{prefix}{quote}{new_path}{quote}{suffix}"
|
|
165
|
+
return line
|
|
166
|
+
|
|
167
|
+
# CommonJS require('...')
|
|
168
|
+
m = _REQUIRE_RE.search(line)
|
|
169
|
+
if m:
|
|
170
|
+
req_prefix, quote, rel_path, req_suffix = m.groups()
|
|
171
|
+
new_path = _remap(rel_path, source_path, target_path, source_root, source_to_target, changes)
|
|
172
|
+
if new_path != rel_path:
|
|
173
|
+
return line.replace(f"{quote}{rel_path}{quote}", f"{quote}{new_path}{quote}", 1)
|
|
174
|
+
|
|
175
|
+
return line
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Path remapping
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
def _remap(
|
|
183
|
+
rel_path: str,
|
|
184
|
+
source_path: str,
|
|
185
|
+
target_path: str,
|
|
186
|
+
source_root: str,
|
|
187
|
+
source_to_target: dict[str, str],
|
|
188
|
+
changes: list[str],
|
|
189
|
+
) -> str:
|
|
190
|
+
# Check against known infrastructure aliases first
|
|
191
|
+
for pattern, alias in _HELIX_ALIASES.items():
|
|
192
|
+
if pattern.search(rel_path):
|
|
193
|
+
changes.append(f"Remapped {rel_path!r} → {alias!r} (alias)")
|
|
194
|
+
return alias
|
|
195
|
+
|
|
196
|
+
# Resolve the import target relative to the source file
|
|
197
|
+
src_dir = Path(source_path).parent
|
|
198
|
+
raw_target = (src_dir / rel_path).resolve()
|
|
199
|
+
root = Path(source_root).resolve()
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
rel_to_root = raw_target.relative_to(root).as_posix()
|
|
203
|
+
except ValueError:
|
|
204
|
+
return rel_path # escapes source root — leave as-is
|
|
205
|
+
|
|
206
|
+
# Authoritative path: ask the mapper. We need to match against a file with
|
|
207
|
+
# an extension (the import didn't include one); try `.ts`, `.tsx`, `.js`,
|
|
208
|
+
# `.jsx` and bare-path index.* variants.
|
|
209
|
+
helix_target = _lookup_mapped_target(raw_target, source_to_target)
|
|
210
|
+
|
|
211
|
+
# Fallback to heuristic prediction when the imported file isn't a mapped
|
|
212
|
+
# source file (e.g. import of a generated file, or an external module
|
|
213
|
+
# masquerading as a relative path).
|
|
214
|
+
if helix_target is None:
|
|
215
|
+
helix_target = _infer_helix_path(rel_to_root)
|
|
216
|
+
if helix_target is None:
|
|
217
|
+
return rel_path
|
|
218
|
+
|
|
219
|
+
new_rel = _relative_helix_import(target_path, helix_target)
|
|
220
|
+
if new_rel != rel_path:
|
|
221
|
+
changes.append(f"Remapped import {rel_path!r} → {new_rel!r}")
|
|
222
|
+
return new_rel
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Mapper lookup — converts the resolved source path of an imported file into
|
|
227
|
+
# the helix target the mapper actually wrote (or will write). The import in
|
|
228
|
+
# the source code typically omits the extension, so we probe the common ones.
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
_IMPORT_PROBE_SUFFIXES = (".ts", ".tsx", ".js", ".jsx", "")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _lookup_mapped_target(raw_target: Path, s2t: dict[str, str]) -> str | None:
|
|
235
|
+
"""Return the helix target for `raw_target` if the mapper covers it."""
|
|
236
|
+
if not s2t:
|
|
237
|
+
return None
|
|
238
|
+
base = str(raw_target)
|
|
239
|
+
for suffix in _IMPORT_PROBE_SUFFIXES:
|
|
240
|
+
candidate = base + suffix
|
|
241
|
+
if candidate in s2t:
|
|
242
|
+
return _strip_ext(s2t[candidate])
|
|
243
|
+
# The import might point at a directory with an index file inside.
|
|
244
|
+
for index_name in ("index.ts", "index.tsx", "index.js", "index.jsx"):
|
|
245
|
+
candidate = str(raw_target / index_name)
|
|
246
|
+
if candidate in s2t:
|
|
247
|
+
return _strip_ext(s2t[candidate])
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _strip_ext(target_rel: str) -> str:
|
|
252
|
+
"""Drop the file extension from a helix target path for import use."""
|
|
253
|
+
for suffix in (".ts", ".tsx", ".js", ".jsx"):
|
|
254
|
+
if target_rel.endswith(suffix):
|
|
255
|
+
return target_rel[: -len(suffix)]
|
|
256
|
+
return target_rel
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Alias-via-mapping: resolve a source-project alias (e.g. `@pages/scweb/locators`)
|
|
261
|
+
# by reconstructing the absolute source path and looking it up in the mapping.
|
|
262
|
+
# Falls back to the legacy heuristic when the alias prefix isn't recognised.
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
# Map common source-project alias prefixes to their physical sub-paths inside
|
|
266
|
+
# the source tree. Most projects use `@pages/X` for `src/pages/X.ts`, etc.
|
|
267
|
+
# Unknown aliases fall through to the heuristic rewriter.
|
|
268
|
+
_ALIAS_SUBPATHS = {
|
|
269
|
+
"@pages": ("src/pages", "pages"),
|
|
270
|
+
"@steps": ("src/test/steps", "src/steps", "steps"),
|
|
271
|
+
"@features": ("src/test/features", "src/features", "features"),
|
|
272
|
+
"@locators": ("src/locators", "locators"),
|
|
273
|
+
"@helper": ("src/helper", "src/helpers", "helper", "helpers"),
|
|
274
|
+
"@hooks": ("src/hooks", "hooks"),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _rewrite_source_alias_via_mapping(
|
|
279
|
+
alias_path: str,
|
|
280
|
+
source_root: str,
|
|
281
|
+
s2t: dict[str, str],
|
|
282
|
+
) -> str | None:
|
|
283
|
+
"""
|
|
284
|
+
Try to resolve `@alias/sub/path` by probing physical sub-paths in the
|
|
285
|
+
source tree and looking the result up in the mapper's table.
|
|
286
|
+
"""
|
|
287
|
+
if not s2t or not alias_path.startswith("@"):
|
|
288
|
+
return None
|
|
289
|
+
head, _, tail = alias_path.partition("/")
|
|
290
|
+
if not tail:
|
|
291
|
+
return None
|
|
292
|
+
subpaths = _ALIAS_SUBPATHS.get(head)
|
|
293
|
+
if not subpaths:
|
|
294
|
+
return None
|
|
295
|
+
root = Path(source_root).resolve()
|
|
296
|
+
for sub in subpaths:
|
|
297
|
+
for suffix in _IMPORT_PROBE_SUFFIXES:
|
|
298
|
+
candidate = root / sub / (tail + suffix)
|
|
299
|
+
mapped = s2t.get(str(candidate.resolve()))
|
|
300
|
+
if mapped:
|
|
301
|
+
return _helix_target_to_alias(_strip_ext(mapped))
|
|
302
|
+
# Also try directory index lookup
|
|
303
|
+
for index_name in ("index.ts", "index.tsx", "index.js", "index.jsx"):
|
|
304
|
+
candidate = root / sub / tail / index_name
|
|
305
|
+
mapped = s2t.get(str(candidate.resolve()))
|
|
306
|
+
if mapped:
|
|
307
|
+
return _helix_target_to_alias(_strip_ext(mapped))
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _helix_target_to_alias(helix_rel: str) -> str:
|
|
312
|
+
"""
|
|
313
|
+
Convert a helix-relative file path (no extension) into the most natural
|
|
314
|
+
`@alias/...` form so callers can import via the configured tsconfig path
|
|
315
|
+
aliases.
|
|
316
|
+
"""
|
|
317
|
+
mapping = (
|
|
318
|
+
("src/locators/", "@locators/"),
|
|
319
|
+
("src/pages/", "@pages/"),
|
|
320
|
+
("src/test/steps/", "@steps/"),
|
|
321
|
+
("src/test/features/","@features/"),
|
|
322
|
+
("src/hooks/", "@hooks/"),
|
|
323
|
+
("src/helper/", "@helper/"),
|
|
324
|
+
("src/utils/", "@utils/"),
|
|
325
|
+
)
|
|
326
|
+
for prefix, alias in mapping:
|
|
327
|
+
if helix_rel.startswith(prefix):
|
|
328
|
+
return alias + helix_rel[len(prefix):]
|
|
329
|
+
return helix_rel
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _infer_helix_path(rel_to_root: str) -> str | None:
|
|
333
|
+
"""Guess the Helix file path from a source-root-relative path."""
|
|
334
|
+
name = Path(rel_to_root).stem
|
|
335
|
+
|
|
336
|
+
if re.search(r"[Ll]ocators?$", name):
|
|
337
|
+
from stlc_agents.agent_migration.mapper import _to_kebab # type: ignore[import]
|
|
338
|
+
stem = re.sub(r"[Ll]ocators?$", "", name)
|
|
339
|
+
if not stem:
|
|
340
|
+
# File is literally "locators.ts" — disambiguation would have added
|
|
341
|
+
# the parent directory name as prefix, e.g. ui/ → ui-locators.locators
|
|
342
|
+
parent = Path(rel_to_root).parent.name.lower()
|
|
343
|
+
return f"src/locators/{parent}-locators.locators"
|
|
344
|
+
return f"src/locators/{_to_kebab(stem)}.locators"
|
|
345
|
+
|
|
346
|
+
# Page-object import patterns:
|
|
347
|
+
# `Xxx.page` (stem = "Xxx.page", or "Xxx" after one strip)
|
|
348
|
+
# `XxxPage` (PascalCase suffix)
|
|
349
|
+
rel_path = rel_to_root
|
|
350
|
+
if rel_path.endswith(".page") or re.search(r"\.page(\.[jt]s)?$", rel_path):
|
|
351
|
+
from stlc_agents.agent_migration.mapper import _to_kebab # type: ignore[import]
|
|
352
|
+
# Get the bare class name: ".../Foo.page" → "Foo"
|
|
353
|
+
base = Path(rel_path).name
|
|
354
|
+
base = re.sub(r"\.[jt]s$", "", base)
|
|
355
|
+
base = re.sub(r"\.page$", "", base, flags=re.IGNORECASE)
|
|
356
|
+
return f"src/pages/{_to_kebab(base)}.page"
|
|
357
|
+
if re.search(r"[Pp]age$", name):
|
|
358
|
+
from stlc_agents.agent_migration.mapper import _to_kebab # type: ignore[import]
|
|
359
|
+
stem = re.sub(r"[Pp]age$", "", name)
|
|
360
|
+
return f"src/pages/{_to_kebab(stem)}.page"
|
|
361
|
+
|
|
362
|
+
if rel_path.endswith(".steps") or re.search(r"\.steps?(\.[jt]s)?$", rel_path):
|
|
363
|
+
from stlc_agents.agent_migration.mapper import _to_kebab # type: ignore[import]
|
|
364
|
+
base = Path(rel_path).name
|
|
365
|
+
base = re.sub(r"\.[jt]s$", "", base)
|
|
366
|
+
base = re.sub(r"\.steps?$", "", base, flags=re.IGNORECASE)
|
|
367
|
+
return f"src/test/steps/{_to_kebab(base)}.steps"
|
|
368
|
+
if re.search(r"[Ss]teps?$", name):
|
|
369
|
+
from stlc_agents.agent_migration.mapper import _to_kebab # type: ignore[import]
|
|
370
|
+
stem = re.sub(r"[Ss]teps?$", "", name)
|
|
371
|
+
return f"src/test/steps/{_to_kebab(stem)}.steps"
|
|
372
|
+
|
|
373
|
+
# Helper / util files (anywhere under `helpers/` or `utils/`). The mapper
|
|
374
|
+
# routes these to `src/helper/<preserved-subpath>` — match its behavior so
|
|
375
|
+
# imports get rewritten to the new flat location instead of the legacy
|
|
376
|
+
# source-relative path.
|
|
377
|
+
m = re.match(r"^(?:helpers?|utils?)/(.+)$", rel_to_root)
|
|
378
|
+
if m:
|
|
379
|
+
sub = m.group(1).rsplit(".", 1)[0] # drop extension
|
|
380
|
+
return f"src/helper/{sub}"
|
|
381
|
+
|
|
382
|
+
# Fallback for files at the source root (no directory prefix). Things like
|
|
383
|
+
# `global-cleanup.ts`, `global-variableSetup.ts` are raw-copied to the
|
|
384
|
+
# helix root and keep their original name — recomputing the relative path
|
|
385
|
+
# against the new file location is still needed.
|
|
386
|
+
if "/" not in rel_to_root:
|
|
387
|
+
return rel_to_root.rsplit(".", 1)[0] # drop extension; helix-root-relative
|
|
388
|
+
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _relative_helix_import(from_helix: str, to_helix: str) -> str:
|
|
393
|
+
"""Compute a relative import path between two Helix-relative paths."""
|
|
394
|
+
if to_helix.startswith("@"):
|
|
395
|
+
return to_helix
|
|
396
|
+
|
|
397
|
+
from_dir = PurePosixPath(from_helix).parent
|
|
398
|
+
to_path = PurePosixPath(to_helix)
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
rel = to_path.relative_to(from_dir)
|
|
402
|
+
return "./" + str(rel)
|
|
403
|
+
except ValueError:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
# Walk up from from_dir until we find the common ancestor
|
|
407
|
+
from_parts = from_dir.parts
|
|
408
|
+
to_parts = to_path.parts
|
|
409
|
+
|
|
410
|
+
common = 0
|
|
411
|
+
for f, t in zip(from_parts, to_parts):
|
|
412
|
+
if f == t:
|
|
413
|
+
common += 1
|
|
414
|
+
else:
|
|
415
|
+
break
|
|
416
|
+
|
|
417
|
+
up = len(from_parts) - common
|
|
418
|
+
down = to_parts[common:]
|
|
419
|
+
return "../" * up + "/".join(down)
|