@qa-gentic/stlc-agents 1.0.27 → 1.0.29

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/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/bin/postinstall.js +14 -4
  7. package/package.json +19 -7
  8. package/skills/migrate-framework/SKILL.md +207 -0
  9. package/src/stlc_agents/agent_migration/__init__.py +0 -0
  10. package/src/stlc_agents/agent_migration/_migrate.py +1398 -0
  11. package/src/stlc_agents/agent_migration/cli.py +217 -0
  12. package/src/stlc_agents/agent_migration/detector.py +81 -0
  13. package/src/stlc_agents/agent_migration/mapper.py +439 -0
  14. package/src/stlc_agents/agent_migration/reporter.py +86 -0
  15. package/src/stlc_agents/agent_migration/server.py +267 -0
  16. package/src/stlc_agents/agent_migration/transformer/__init__.py +0 -0
  17. package/src/stlc_agents/agent_migration/transformer/config_merger.py +513 -0
  18. package/src/stlc_agents/agent_migration/transformer/healer_injector.py +1143 -0
  19. package/src/stlc_agents/agent_migration/transformer/import_fixer.py +419 -0
  20. package/src/stlc_agents/agent_migration/transformer/js_to_ts.py +413 -0
  21. package/src/stlc_agents/agent_migration/transformer/local_var_hoister.py +378 -0
  22. package/src/stlc_agents/agent_migration/transformer/locator_moderniser.py +132 -0
  23. package/src/stlc_agents/agent_migration/transformer/locator_registrar.py +328 -0
  24. package/src/stlc_agents/agent_migration/transformer/spec_to_bdd.py +820 -0
  25. package/src/stlc_agents/agent_playwright_generator/server.py +926 -91
  26. package/src/stlc_agents/__pycache__/__init__.cpython-314.pyc +0 -0
  27. package/src/stlc_agents/agent_gherkin_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/src/stlc_agents/agent_gherkin_generator/__pycache__/server.cpython-314.pyc +0 -0
  29. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  30. package/src/stlc_agents/agent_gherkin_generator/tools/__pycache__/ado_gherkin.cpython-314.pyc +0 -0
  31. package/src/stlc_agents/agent_helix_writer/__pycache__/__init__.cpython-314.pyc +0 -0
  32. package/src/stlc_agents/agent_helix_writer/__pycache__/server.cpython-314.pyc +0 -0
  33. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  34. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/boilerplate.cpython-314.pyc +0 -0
  35. package/src/stlc_agents/agent_helix_writer/tools/__pycache__/helix_write.cpython-314.pyc +0 -0
  36. package/src/stlc_agents/agent_playwright_generator/__pycache__/__init__.cpython-314.pyc +0 -0
  37. package/src/stlc_agents/agent_playwright_generator/__pycache__/server.cpython-314.pyc +0 -0
  38. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  39. package/src/stlc_agents/agent_playwright_generator/tools/__pycache__/ado_attach.cpython-314.pyc +0 -0
  40. package/src/stlc_agents/agent_test_case_manager/__pycache__/__init__.cpython-314.pyc +0 -0
  41. package/src/stlc_agents/agent_test_case_manager/__pycache__/server.cpython-314.pyc +0 -0
  42. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  43. package/src/stlc_agents/agent_test_case_manager/tools/__pycache__/ado_workitem.cpython-314.pyc +0 -0
  44. package/src/stlc_agents/shared/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/src/stlc_agents/shared/__pycache__/auth.cpython-314.pyc +0 -0
  46. package/src/stlc_agents/shared/__pycache__/cost_tracker.cpython-314.pyc +0 -0
  47. package/src/stlc_agents/shared/__pycache__/pricing.cpython-314.pyc +0 -0
@@ -0,0 +1,217 @@
1
+ """
2
+ stlc-migrate CLI — Migrate a Playwright test suite to Helix-QA structure.
3
+
4
+ Usage:
5
+ stlc-migrate --source <dir> --helix <dir> [options]
6
+
7
+ Options:
8
+ --conflict interactive | overwrite | skip (default: interactive)
9
+ --dry-run Preview migration without writing any files
10
+ --json Output results as JSON
11
+ """
12
+ from __future__ import annotations
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from .detector import detect_framework
19
+ from .mapper import map_source_files
20
+ from ._migrate import run_migration
21
+
22
+
23
+ def main() -> None:
24
+ parser = argparse.ArgumentParser(
25
+ prog="stlc-migrate",
26
+ description="Migrate a Playwright test suite to the Helix-QA agent-compatible structure.",
27
+ formatter_class=argparse.RawDescriptionHelpFormatter,
28
+ epilog=__doc__,
29
+ )
30
+ parser.add_argument(
31
+ "--source",
32
+ required=True,
33
+ metavar="DIR",
34
+ help="Source test project directory (plain Playwright JS/TS or Playwright+Cucumber JS/TS).",
35
+ )
36
+ parser.add_argument(
37
+ "--helix",
38
+ required=True,
39
+ metavar="DIR",
40
+ help="Helix-QA root directory (output). Must already exist unless --dry-run.",
41
+ )
42
+ parser.add_argument(
43
+ "--conflict",
44
+ choices=["interactive", "overwrite", "skip"],
45
+ default="interactive",
46
+ metavar="STRATEGY",
47
+ help=(
48
+ "Conflict resolution when a target file already exists: "
49
+ "'interactive' shows a report (default), "
50
+ "'overwrite' replaces existing files, "
51
+ "'skip' keeps existing files."
52
+ ),
53
+ )
54
+ parser.add_argument(
55
+ "--integration",
56
+ choices=["ado", "jira", "both"],
57
+ default="both",
58
+ metavar="WHICH",
59
+ help=(
60
+ "Which QA-STLC agent integration to install skills + .mcp.json for: "
61
+ "'ado' (qa-test-case-manager workflow), 'jira' (qa-jira-manager workflow), "
62
+ "or 'both' (default — every agent reachable from the migrated tree)."
63
+ ),
64
+ )
65
+ parser.add_argument(
66
+ "--dry-run",
67
+ action="store_true",
68
+ dest="dry_run",
69
+ help="Preview migration — show what would be written without touching any files.",
70
+ )
71
+ parser.add_argument(
72
+ "--json",
73
+ action="store_true",
74
+ dest="json_output",
75
+ help="Write results as JSON to stdout.",
76
+ )
77
+
78
+ args = parser.parse_args()
79
+
80
+ source_path = Path(args.source).resolve()
81
+ helix_path = Path(args.helix).resolve()
82
+
83
+ # ── Validation ──────────────────────────────────────────────────────────
84
+ if not source_path.exists():
85
+ _err(f"Source directory does not exist: {source_path}")
86
+
87
+ if not helix_path.exists() and not args.dry_run:
88
+ _err(
89
+ f"Helix directory does not exist: {helix_path}\n"
90
+ " Create it first or pass --dry-run to preview without writing."
91
+ )
92
+
93
+ # ── Detection ───────────────────────────────────────────────────────────
94
+ _info(f"Scanning source: {source_path}")
95
+ detection = detect_framework(str(source_path))
96
+
97
+ if "error" in detection:
98
+ _err(detection["error"])
99
+
100
+ fw = detection["framework"]
101
+ fc = detection["file_counts"]
102
+ _info(
103
+ f" Framework : {fw}\n"
104
+ f" Language : {detection['language']} | BDD: {detection['bdd']}\n"
105
+ f" Files : {fc['ts']} .ts {fc['js']} .js {fc['feature']} .feature"
106
+ )
107
+
108
+ # ── File mapping ────────────────────────────────────────────────────────
109
+ files = map_source_files(str(source_path))
110
+ mappable = [f for f in files if f["target"] is not None]
111
+ # role="unknown" files are raw-copied at their original path. We still list
112
+ # them so the user can see what got copied without classification.
113
+ unknown = [f for f in files if f["role"] == "unknown"]
114
+
115
+ _info(f" Mappable : {len(mappable)} files Raw-copied (unclassified): {len(unknown)}")
116
+
117
+ # ── Dry-run ─────────────────────────────────────────────────────────────
118
+ if args.dry_run:
119
+ _print_dry_run(mappable, unknown, helix_path, args.json_output)
120
+ return
121
+
122
+ # ── Conflict pre-check ──────────────────────────────────────────────────
123
+ pre_conflicts = [
124
+ f for f in mappable if f["target"] and (helix_path / f["target"]).exists()
125
+ ]
126
+ if pre_conflicts and args.conflict == "interactive":
127
+ _info(
128
+ f"\n ⚠ {len(pre_conflicts)} file(s) already exist in Helix. "
129
+ "They will be overwritten and listed in MIGRATION-REPORT.md.\n"
130
+ " Use --conflict skip to keep existing files, or --conflict overwrite to suppress this notice."
131
+ )
132
+
133
+ # ── Migration ───────────────────────────────────────────────────────────
134
+ _info("\nRunning migration ...")
135
+ result = run_migration(
136
+ source_dir=str(source_path),
137
+ helix_root=str(helix_path),
138
+ file_mappings=mappable,
139
+ conflict_strategy=args.conflict,
140
+ framework=fw,
141
+ integration=args.integration,
142
+ )
143
+
144
+ if args.json_output:
145
+ print(json.dumps(result, indent=2))
146
+ return
147
+
148
+ # ── Human-readable summary ──────────────────────────────────────────────
149
+ print()
150
+ print("Migration complete")
151
+ print(f" Written : {len(result['written'])}")
152
+ print(f" Skipped : {len(result['skipped'])}")
153
+ print(f" Conflicts : {len(result['conflicts'])}")
154
+ print(f" TODOs : {result['todo_count']}")
155
+
156
+ if result.get("todos"):
157
+ print("\nTODOs:")
158
+ for t in result["todos"][:10]:
159
+ print(f" • {t}")
160
+ if len(result["todos"]) > 10:
161
+ print(f" … and {len(result['todos']) - 10} more (see MIGRATION-REPORT.md)")
162
+
163
+ if result.get("report_path"):
164
+ print(f"\nReport : {result['report_path']}")
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Helpers
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def _print_dry_run(
172
+ mappable: list[dict],
173
+ unknown: list[dict],
174
+ helix_path: Path,
175
+ as_json: bool,
176
+ ) -> None:
177
+ plan = [
178
+ {
179
+ "source": f["relative"],
180
+ "target": f["target"],
181
+ "role": f["role"],
182
+ "js→ts": f.get("needs_js_to_ts", False),
183
+ "conflict": (helix_path / f["target"]).exists() if f["target"] else False,
184
+ }
185
+ for f in mappable
186
+ ]
187
+ if as_json:
188
+ print(json.dumps({"dry_run": True, "plan": plan, "unknown": [f["relative"] for f in unknown]}, indent=2))
189
+ return
190
+
191
+ print("\n[DRY RUN] Files that would be migrated:")
192
+ print(f" {'Source':<45} {'Target':<45} Role")
193
+ print(f" {'-'*45} {'-'*45} ----")
194
+ for f in plan:
195
+ conflict_flag = " ⚠ CONFLICT" if f["conflict"] else ""
196
+ js_ts_flag = " (JS→TS)" if f["js→ts"] else ""
197
+ print(f" {f['source']:<45} {f['target']:<45} {f['role']}{js_ts_flag}{conflict_flag}")
198
+
199
+ if unknown:
200
+ print(f"\n[DRY RUN] {len(unknown)} file(s) raw-copied with unclassified role:")
201
+ for f in unknown[:5]:
202
+ print(f" {f['relative']}")
203
+ if len(unknown) > 5:
204
+ print(f" … and {len(unknown) - 5} more")
205
+
206
+
207
+ def _info(msg: str) -> None:
208
+ print(msg, file=sys.stdout)
209
+
210
+
211
+ def _err(msg: str) -> None:
212
+ print(f"Error: {msg}", file=sys.stderr)
213
+ sys.exit(1)
214
+
215
+
216
+ if __name__ == "__main__":
217
+ main()
@@ -0,0 +1,81 @@
1
+ """Framework auto-detection for migrate_framework."""
2
+ from __future__ import annotations
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def detect_framework(source_dir: str) -> dict:
8
+ """
9
+ Detect the Playwright framework type from a source directory.
10
+
11
+ Returns:
12
+ {
13
+ framework: "playwright-bdd-ts" | "playwright-bdd-js" |
14
+ "playwright-ts" | "playwright-js",
15
+ language: "typescript" | "javascript",
16
+ bdd: bool,
17
+ config_files: list[str],
18
+ file_counts: { ts: int, js: int, feature: int },
19
+ }
20
+ """
21
+ root = Path(source_dir)
22
+ if not root.exists():
23
+ return {"error": f"Source directory does not exist: {source_dir}"}
24
+
25
+ _skip = {"node_modules", "dist", ".venv", "__pycache__", ".git"}
26
+
27
+ def _walk(suffix: str) -> list[Path]:
28
+ return [
29
+ p for p in root.rglob(f"*{suffix}")
30
+ if not any(s in p.parts for s in _skip)
31
+ ]
32
+
33
+ ts_files = _walk(".ts")
34
+ js_files = _walk(".js")
35
+ feature_files = _walk(".feature")
36
+
37
+ file_counts = {
38
+ "ts": len(ts_files),
39
+ "js": len(js_files),
40
+ "feature": len(feature_files),
41
+ }
42
+
43
+ language = "typescript" if len(ts_files) >= len(js_files) else "javascript"
44
+ bdd = len(feature_files) > 0
45
+
46
+ # Confirm BDD via package.json deps
47
+ pkg_path = root / "package.json"
48
+ if pkg_path.exists():
49
+ try:
50
+ pkg = json.loads(pkg_path.read_text(encoding="utf-8"))
51
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
52
+ if "@cucumber/cucumber" in deps or "cucumber" in deps:
53
+ bdd = True
54
+ except Exception:
55
+ pass
56
+
57
+ config_files = []
58
+ for name in (
59
+ "playwright.config.ts", "playwright.config.js",
60
+ "cucumber.js", "cucumber.cjs", "cucumber.mjs",
61
+ "tsconfig.json", "package.json",
62
+ ):
63
+ if (root / name).exists():
64
+ config_files.append(name)
65
+
66
+ if bdd and language == "typescript":
67
+ framework = "playwright-bdd-ts"
68
+ elif bdd:
69
+ framework = "playwright-bdd-js"
70
+ elif language == "typescript":
71
+ framework = "playwright-ts"
72
+ else:
73
+ framework = "playwright-js"
74
+
75
+ return {
76
+ "framework": framework,
77
+ "language": language,
78
+ "bdd": bdd,
79
+ "config_files": config_files,
80
+ "file_counts": file_counts,
81
+ }