@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,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
|
+
}
|