qa-engineer 0.9.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 (232) hide show
  1. package/COMPATIBILITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.md +508 -0
  4. package/package.json +90 -0
  5. package/packages/installer/README.md +46 -0
  6. package/packages/installer/bin/qa.mjs +136 -0
  7. package/packages/installer/lib/agents/registry.mjs +209 -0
  8. package/packages/installer/lib/cli/commands.mjs +29 -0
  9. package/packages/installer/lib/cli/flags.mjs +64 -0
  10. package/packages/installer/lib/commands/doctor.mjs +190 -0
  11. package/packages/installer/lib/commands/install.mjs +291 -0
  12. package/packages/installer/lib/commands/onboard.mjs +163 -0
  13. package/packages/installer/lib/commands/repair.mjs +68 -0
  14. package/packages/installer/lib/commands/self-test.mjs +45 -0
  15. package/packages/installer/lib/commands/uninstall.mjs +167 -0
  16. package/packages/installer/lib/commands/update.mjs +69 -0
  17. package/packages/installer/lib/commands/verify.mjs +62 -0
  18. package/packages/installer/lib/constants.mjs +34 -0
  19. package/packages/installer/lib/core/bundle.mjs +124 -0
  20. package/packages/installer/lib/core/config.mjs +73 -0
  21. package/packages/installer/lib/core/conflict.mjs +29 -0
  22. package/packages/installer/lib/core/errors.mjs +29 -0
  23. package/packages/installer/lib/core/fs-safe.mjs +236 -0
  24. package/packages/installer/lib/core/hash.mjs +19 -0
  25. package/packages/installer/lib/core/lockfile.mjs +75 -0
  26. package/packages/installer/lib/core/logger.mjs +46 -0
  27. package/packages/installer/lib/core/manifest.mjs +89 -0
  28. package/packages/installer/lib/core/paths.mjs +74 -0
  29. package/packages/installer/lib/core/schema-validate.mjs +142 -0
  30. package/packages/installer/lib/core/skill-meta.mjs +75 -0
  31. package/packages/installer/lib/core/validate-install.mjs +152 -0
  32. package/packages/installer/lib/core/wrappers.mjs +91 -0
  33. package/packages/installer/lib/detect/environment.mjs +54 -0
  34. package/packages/installer/lib/detect/frameworks.mjs +94 -0
  35. package/packages/installer/lib/detect/project.mjs +126 -0
  36. package/packages/installer/lib/detect/recommend.mjs +85 -0
  37. package/packages/installer/lib/detect/scan.mjs +48 -0
  38. package/packages/installer/lib/ui/progress.mjs +32 -0
  39. package/packages/installer/lib/ui/theme.mjs +81 -0
  40. package/packages/installer/lib/version.mjs +47 -0
  41. package/packages/installer/package.json +28 -0
  42. package/packages/installer/schemas/qa-lock.schema.json +89 -0
  43. package/packages/installer/schemas/qa.config.schema.json +91 -0
  44. package/shared/analysis/lib/qa_analysis/__init__.py +11 -0
  45. package/shared/analysis/lib/qa_analysis/branding.json +9 -0
  46. package/shared/analysis/lib/qa_analysis/branding.py +175 -0
  47. package/shared/analysis/lib/qa_analysis/cli.py +129 -0
  48. package/shared/analysis/lib/qa_analysis/context.py +233 -0
  49. package/shared/analysis/lib/qa_analysis/contracts.py +158 -0
  50. package/shared/analysis/lib/qa_analysis/diff_guard.py +327 -0
  51. package/shared/analysis/lib/qa_analysis/discovery.py +113 -0
  52. package/shared/analysis/lib/qa_analysis/evidence.py +128 -0
  53. package/shared/analysis/lib/qa_analysis/har.py +58 -0
  54. package/shared/analysis/lib/qa_analysis/junit.py +80 -0
  55. package/shared/analysis/lib/qa_analysis/redaction.py +99 -0
  56. package/shared/analysis/lib/qa_analysis/taxonomy.py +98 -0
  57. package/shared/analysis/schemas/context.schema.json +82 -0
  58. package/shared/diagnostics/lib/qa_diagnostics/__init__.py +13 -0
  59. package/shared/diagnostics/lib/qa_diagnostics/cli.py +124 -0
  60. package/shared/diagnostics/lib/qa_diagnostics/engine.py +143 -0
  61. package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +75 -0
  62. package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +101 -0
  63. package/shared/diagnostics/lib/qa_diagnostics/repair.py +72 -0
  64. package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +89 -0
  65. package/shared/diagnostics/lib/qa_diagnostics/timeline.py +71 -0
  66. package/shared/diagnostics/schemas/internal/analysis-result.schema.json +28 -0
  67. package/shared/diagnostics/schemas/internal/diagnosis.schema.json +55 -0
  68. package/shared/diagnostics/schemas/internal/execution-result-min.schema.json +34 -0
  69. package/shared/frameworks/cypress/lib/cypress_analysis.py +27 -0
  70. package/shared/frameworks/playwright/lib/playwright_analysis.py +167 -0
  71. package/shared/frameworks/registry.json +150 -0
  72. package/shared/frameworks/registry.mjs +37 -0
  73. package/shared/frameworks/registry.schema.json +74 -0
  74. package/shared/frameworks/selenium/lib/selenium_analysis.py +28 -0
  75. package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +24 -0
  76. package/shared/tooling/qa_tool.py +127 -0
  77. package/skills/README.md +31 -0
  78. package/skills/qa/README.md +19 -0
  79. package/skills/qa/SKILL.md +52 -0
  80. package/skills/qa/examples/routing.md +86 -0
  81. package/skills/qa/references/routing-map.md +33 -0
  82. package/skills/qa-api/README.md +19 -0
  83. package/skills/qa-api/SKILL.md +68 -0
  84. package/skills/qa-api/contracts/api-result.schema.json +51 -0
  85. package/skills/qa-api/examples/graphql-review.md +43 -0
  86. package/skills/qa-api/references/authentication.md +45 -0
  87. package/skills/qa-api/references/deterministic-tooling.md +128 -0
  88. package/skills/qa-api/references/evidence-and-reporting.md +66 -0
  89. package/skills/qa-api/references/graphql.md +44 -0
  90. package/skills/qa-api/references/rest.md +45 -0
  91. package/skills/qa-api/references/websocket.md +44 -0
  92. package/skills/qa-audit/README.md +19 -0
  93. package/skills/qa-audit/SKILL.md +70 -0
  94. package/skills/qa-audit/contracts/audit-result.schema.json +63 -0
  95. package/skills/qa-audit/examples/accessibility-audit.md +46 -0
  96. package/skills/qa-audit/references/accessibility.md +44 -0
  97. package/skills/qa-audit/references/deterministic-tooling.md +128 -0
  98. package/skills/qa-audit/references/evidence-and-reporting.md +66 -0
  99. package/skills/qa-audit/references/performance.md +44 -0
  100. package/skills/qa-audit/references/security.md +43 -0
  101. package/skills/qa-audit/references/visual-testing.md +44 -0
  102. package/skills/qa-debug/README.md +19 -0
  103. package/skills/qa-debug/SKILL.md +76 -0
  104. package/skills/qa-debug/contracts/debug-result.schema.json +121 -0
  105. package/skills/qa-debug/examples/failed-login.md +61 -0
  106. package/skills/qa-debug/examples/locator-break.md +59 -0
  107. package/skills/qa-debug/examples/network-timeout.md +61 -0
  108. package/skills/qa-debug/examples/successful-debug.md +62 -0
  109. package/skills/qa-debug/references/confidence-model.md +26 -0
  110. package/skills/qa-debug/references/deterministic-tooling.md +128 -0
  111. package/skills/qa-debug/references/diagnostic-engine.md +36 -0
  112. package/skills/qa-debug/references/evidence-and-reporting.md +66 -0
  113. package/skills/qa-debug/references/evidence-model.md +41 -0
  114. package/skills/qa-debug/references/failure-taxonomy.md +44 -0
  115. package/skills/qa-debug/references/finding-prioritization.md +43 -0
  116. package/skills/qa-debug/references/investigation-workflow.md +48 -0
  117. package/skills/qa-debug/references/recommendation-ranking.md +34 -0
  118. package/skills/qa-debug/references/root-cause-analysis.md +49 -0
  119. package/skills/qa-debug/references/timeline-builder.md +37 -0
  120. package/skills/qa-example/README.md +19 -0
  121. package/skills/qa-example/SKILL.md +60 -0
  122. package/skills/qa-example/contracts/self-check-report.schema.json +65 -0
  123. package/skills/qa-example/examples/self-check.md +53 -0
  124. package/skills/qa-example/references/example-domain.md +28 -0
  125. package/skills/qa-example/references/skill-format-notes.md +26 -0
  126. package/skills/qa-explore/README.md +22 -0
  127. package/skills/qa-explore/SKILL.md +87 -0
  128. package/skills/qa-explore/contracts/explore-result.schema.json +249 -0
  129. package/skills/qa-explore/examples/attached-test-cases.md +70 -0
  130. package/skills/qa-explore/examples/screenshot-proof-finding.md +50 -0
  131. package/skills/qa-explore/examples/url-smoke-explore.md +80 -0
  132. package/skills/qa-explore/references/accessibility.md +44 -0
  133. package/skills/qa-explore/references/api-replay.md +48 -0
  134. package/skills/qa-explore/references/browser-adapters.md +51 -0
  135. package/skills/qa-explore/references/evidence-and-reporting.md +66 -0
  136. package/skills/qa-explore/references/evidence-capture.md +54 -0
  137. package/skills/qa-explore/references/exploratory-qa.md +52 -0
  138. package/skills/qa-explore/references/finding-taxonomy.md +47 -0
  139. package/skills/qa-explore/references/performance.md +44 -0
  140. package/skills/qa-explore/references/pipeline.md +74 -0
  141. package/skills/qa-explore/references/report-pipeline.md +69 -0
  142. package/skills/qa-explore/references/security.md +43 -0
  143. package/skills/qa-explore/references/test-case-intake.md +44 -0
  144. package/skills/qa-fix/README.md +19 -0
  145. package/skills/qa-fix/SKILL.md +70 -0
  146. package/skills/qa-fix/contracts/fix-result.schema.json +132 -0
  147. package/skills/qa-fix/examples/repair-plan.md +56 -0
  148. package/skills/qa-fix/references/deterministic-tooling.md +128 -0
  149. package/skills/qa-fix/references/diagnostic-engine.md +36 -0
  150. package/skills/qa-fix/references/evidence-and-reporting.md +66 -0
  151. package/skills/qa-fix/references/repair-strategy.md +48 -0
  152. package/skills/qa-fix/references/root-cause-analysis.md +49 -0
  153. package/skills/qa-fix/references/suite-extension.md +73 -0
  154. package/skills/qa-flaky/README.md +19 -0
  155. package/skills/qa-flaky/SKILL.md +67 -0
  156. package/skills/qa-flaky/contracts/flaky-result.schema.json +62 -0
  157. package/skills/qa-flaky/examples/flaky-locator.md +46 -0
  158. package/skills/qa-flaky/references/deterministic-tooling.md +128 -0
  159. package/skills/qa-flaky/references/evidence-and-reporting.md +66 -0
  160. package/skills/qa-flaky/references/flakiness.md +48 -0
  161. package/skills/qa-flaky/references/retry.md +44 -0
  162. package/skills/qa-flaky/references/waiting-strategies.md +44 -0
  163. package/skills/qa-generate/README.md +42 -0
  164. package/skills/qa-generate/SKILL.md +83 -0
  165. package/skills/qa-generate/contracts/generation-result.schema.json +124 -0
  166. package/skills/qa-generate/examples/bootstrap-new-framework.md +24 -0
  167. package/skills/qa-generate/examples/extend-existing-suite.md +26 -0
  168. package/skills/qa-generate/references/code-style.md +29 -0
  169. package/skills/qa-generate/references/evidence-and-reporting.md +66 -0
  170. package/skills/qa-generate/references/framework-selection.md +88 -0
  171. package/skills/qa-generate/references/generation-strategy.md +48 -0
  172. package/skills/qa-generate/references/naming-conventions.md +27 -0
  173. package/skills/qa-generate/references/playwright-conventions.md +23 -0
  174. package/skills/qa-generate/references/playwright-generation.md +41 -0
  175. package/skills/qa-generate/references/playwright-project-discovery.md +42 -0
  176. package/skills/qa-generate/references/project-bootstrap.md +54 -0
  177. package/skills/qa-generate/references/repository-analysis.md +43 -0
  178. package/skills/qa-generate/references/suite-extension.md +73 -0
  179. package/skills/qa-generate/references/template-selection.md +48 -0
  180. package/skills/qa-generate/templates/playwright/api.ts +36 -0
  181. package/skills/qa-generate/templates/playwright/base-page.ts +14 -0
  182. package/skills/qa-generate/templates/playwright/data.ts +25 -0
  183. package/skills/qa-generate/templates/playwright/env.example +16 -0
  184. package/skills/qa-generate/templates/playwright/example.spec.ts +14 -0
  185. package/skills/qa-generate/templates/playwright/fixtures.ts +28 -0
  186. package/skills/qa-generate/templates/playwright/framework-readme.md +39 -0
  187. package/skills/qa-generate/templates/playwright/login.page.ts +26 -0
  188. package/skills/qa-generate/templates/playwright/playwright.config.ts +28 -0
  189. package/skills/qa-generate/templates/playwright/utils.ts +18 -0
  190. package/skills/qa-init/README.md +20 -0
  191. package/skills/qa-init/SKILL.md +72 -0
  192. package/skills/qa-init/examples/initialize-a-repo.md +91 -0
  193. package/skills/qa-init/references/detection-guide.md +76 -0
  194. package/skills/qa-init/references/deterministic-tooling.md +128 -0
  195. package/skills/qa-init/references/evidence-and-reporting.md +66 -0
  196. package/skills/qa-init/templates/context.md +68 -0
  197. package/skills/qa-report/README.md +19 -0
  198. package/skills/qa-report/SKILL.md +72 -0
  199. package/skills/qa-report/contracts/report-result.schema.json +186 -0
  200. package/skills/qa-report/examples/release-report.md +68 -0
  201. package/skills/qa-report/references/deterministic-tooling.md +128 -0
  202. package/skills/qa-report/references/diagnostic-engine.md +36 -0
  203. package/skills/qa-report/references/evidence-and-reporting.md +66 -0
  204. package/skills/qa-report/references/finding-prioritization.md +43 -0
  205. package/skills/qa-report/references/recommendation-ranking.md +34 -0
  206. package/skills/qa-report/references/report-aggregation.md +44 -0
  207. package/skills/qa-review/README.md +19 -0
  208. package/skills/qa-review/SKILL.md +58 -0
  209. package/skills/qa-review/contracts/review-result.schema.json +59 -0
  210. package/skills/qa-review/examples/suite-review.md +51 -0
  211. package/skills/qa-review/references/anti-patterns.md +49 -0
  212. package/skills/qa-review/references/assertion-patterns.md +45 -0
  213. package/skills/qa-review/references/evidence-and-reporting.md +66 -0
  214. package/skills/qa-review/references/fixtures.md +45 -0
  215. package/skills/qa-review/references/page-objects.md +45 -0
  216. package/skills/qa-run/README.md +22 -0
  217. package/skills/qa-run/SKILL.md +92 -0
  218. package/skills/qa-run/contracts/execution-plan.schema.json +132 -0
  219. package/skills/qa-run/contracts/execution-result.schema.json +204 -0
  220. package/skills/qa-run/examples/execute-playwright.md +89 -0
  221. package/skills/qa-run/examples/plan-a-run.md +83 -0
  222. package/skills/qa-run/references/artifact-collector.md +40 -0
  223. package/skills/qa-run/references/browser-launch.md +39 -0
  224. package/skills/qa-run/references/command-builder.md +39 -0
  225. package/skills/qa-run/references/deterministic-tooling.md +128 -0
  226. package/skills/qa-run/references/environment-detection.md +37 -0
  227. package/skills/qa-run/references/evidence-and-reporting.md +66 -0
  228. package/skills/qa-run/references/execution-strategy.md +54 -0
  229. package/skills/qa-run/references/playwright-artifacts.md +29 -0
  230. package/skills/qa-run/references/playwright-execution.md +48 -0
  231. package/skills/qa-run/references/playwright-project-discovery.md +42 -0
  232. package/skills/qa-run/references/report-normalization.md +49 -0
@@ -0,0 +1,124 @@
1
+ """Unified CLI for the diagnostic engine.
2
+
3
+ The engine's reasoning is deterministic; this is how a skill reaches it. Every
4
+ subcommand reads JSON files, validates them against the internal seam contracts,
5
+ and writes JSON to stdout — so an agent never has to invent glue code, and the
6
+ input and output shapes are checked rather than assumed.
7
+
8
+ Mirrors `qa_analysis.cli` exactly: JSON to stdout, exit 0 on success, exit 2 on
9
+ bad input or usage. Standard library only.
10
+
11
+ Usage:
12
+ python -m qa_diagnostics.cli diagnose --execution-result <path> [--analysis-result <path>]
13
+ python -m qa_diagnostics.cli plan-repairs --diagnosis <path>
14
+ python -m qa_diagnostics.cli summarize --execution-result <path> --diagnosis <path>
15
+ python -m qa_diagnostics.cli report --execution-result <path> [--analysis-result <path>]
16
+
17
+ `report` is the one-shot path: it diagnoses, plans, and summarizes in a single
18
+ invocation, so the common case is one command instead of three.
19
+
20
+ Inputs
21
+ --execution-result a qa-run execution result (or the minimal subset:
22
+ `tests` counts plus `executed[]` with `status`)
23
+ --analysis-result an analysis result with `findings[]`; preferred over
24
+ `executed[]` when present
25
+ --diagnosis the object emitted by `diagnose`
26
+
27
+ Exit codes
28
+ 0 success
29
+ 2 unreadable file, malformed JSON, or a payload that fails its seam contract
30
+ """
31
+
32
+ import argparse
33
+ import json
34
+ import sys
35
+
36
+ from . import engine
37
+ from . import internal_contracts
38
+ from .internal_contracts import InternalContractError
39
+
40
+
41
+ def _emit(obj):
42
+ json.dump(obj, sys.stdout, indent=2, sort_keys=True)
43
+ sys.stdout.write("\n")
44
+
45
+
46
+ def _read(path):
47
+ with open(path, "r", encoding="utf-8") as handle:
48
+ return json.load(handle)
49
+
50
+
51
+ def _read_execution(path):
52
+ """Read an execution result and hold it to the seam contract."""
53
+ return internal_contracts.validate_execution_result_min(_read(path))
54
+
55
+
56
+ def _read_analysis(path):
57
+ """Read an analysis result and hold it to the seam contract."""
58
+ if path is None:
59
+ return None
60
+ return internal_contracts.validate_analysis_result(_read(path))
61
+
62
+
63
+ def _read_diagnosis(path):
64
+ """Read a diagnosis and hold it to the seam contract."""
65
+ return internal_contracts.validate_diagnosis(_read(path))
66
+
67
+
68
+ def main(argv=None):
69
+ parser = argparse.ArgumentParser(
70
+ prog="qa-diagnostics",
71
+ description="Deterministic diagnostic engine: root cause, timeline, priority, repairs",
72
+ )
73
+ sub = parser.add_subparsers(dest="command", required=True)
74
+
75
+ p = sub.add_parser("diagnose", help="root causes, timeline, prioritization, recommendations")
76
+ p.add_argument("--execution-result", required=True)
77
+ p.add_argument("--analysis-result")
78
+
79
+ p = sub.add_parser("plan-repairs", help="repair plans for a diagnosis (never code)")
80
+ p.add_argument("--diagnosis", required=True)
81
+
82
+ p = sub.add_parser("summarize", help="totals, breakdown, top priorities, release readiness")
83
+ p.add_argument("--execution-result", required=True)
84
+ p.add_argument("--diagnosis", required=True)
85
+
86
+ p = sub.add_parser("report", help="diagnose + plan-repairs + summarize in one call")
87
+ p.add_argument("--execution-result", required=True)
88
+ p.add_argument("--analysis-result")
89
+
90
+ args = parser.parse_args(argv)
91
+
92
+ try:
93
+ if args.command == "diagnose":
94
+ execution = _read_execution(args.execution_result)
95
+ analysis = _read_analysis(args.analysis_result)
96
+ _emit(engine.diagnose(execution, analysis))
97
+ elif args.command == "plan-repairs":
98
+ _emit({"plans": engine.plan_repairs(_read_diagnosis(args.diagnosis))})
99
+ elif args.command == "summarize":
100
+ _emit(engine.summarize(_read_execution(args.execution_result),
101
+ _read_diagnosis(args.diagnosis)))
102
+ elif args.command == "report":
103
+ execution = _read_execution(args.execution_result)
104
+ analysis = _read_analysis(args.analysis_result)
105
+ diagnosis = engine.diagnose(execution, analysis)
106
+ _emit({
107
+ "diagnosis": diagnosis,
108
+ "plans": engine.plan_repairs(diagnosis),
109
+ "summary": engine.summarize(execution, diagnosis),
110
+ })
111
+ except InternalContractError as exc:
112
+ _emit({"error": "invalid-payload", "detail": str(exc)})
113
+ return 2
114
+ except (OSError, json.JSONDecodeError) as exc:
115
+ _emit({"error": "io-error", "detail": str(exc)})
116
+ return 2
117
+ except (KeyError, TypeError) as exc:
118
+ _emit({"error": "unexpected-shape", "detail": f"{type(exc).__name__}: {exc}"})
119
+ return 2
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__":
124
+ sys.exit(main())
@@ -0,0 +1,143 @@
1
+ """The diagnostic engine — the one place failure reasoning happens.
2
+
3
+ Orchestrates the analysis platform and the diagnostics modules into a single
4
+ diagnosis: per-failure root cause, prioritization, a reconstructed timeline, and
5
+ ranked recommendations. qa-debug presents the diagnosis; qa-fix turns it into
6
+ repair plans; qa-report aggregates diagnoses. None of them re-implements this.
7
+ """
8
+
9
+ from qa_analysis import taxonomy
10
+
11
+ from . import root_cause, prioritization, repair, timeline
12
+ from . import internal_contracts
13
+
14
+ _PRIORITY_RANK = {"P1": 3, "P2": 2, "P3": 1}
15
+
16
+ # Classifications that block a release when they are the cause of a failure.
17
+ _RELEASE_BLOCKING = {
18
+ taxonomy.APPLICATION_BUG, taxonomy.NETWORK, taxonomy.INFRASTRUCTURE,
19
+ taxonomy.AUTH, taxonomy.AUTHORIZATION,
20
+ }
21
+
22
+
23
+ def diagnose(execution_result, analysis_result=None, generation_result=None):
24
+ """Produce a full diagnosis from the available results.
25
+
26
+ Returns {entries: [...], timeline: [...], recommendations: [...]}, where each
27
+ entry combines a root cause with its prioritization and affected tests.
28
+ """
29
+ signals = _signals(execution_result, analysis_result)
30
+ findings = (analysis_result or {}).get("findings", [])
31
+
32
+ entries = []
33
+ for signal in signals:
34
+ rc = root_cause.analyze(signal)
35
+ blocking = rc["classification"] in _RELEASE_BLOCKING
36
+ prio = prioritization.prioritize(rc, blocking=blocking)
37
+ entries.append({
38
+ "rootCause": rc,
39
+ "priority": prio,
40
+ "affectedTests": signal.get("affectedTests", []),
41
+ })
42
+
43
+ entries.sort(key=lambda e: (_PRIORITY_RANK.get(e["priority"]["priority"], 0),
44
+ e["rootCause"]["confidence"]), reverse=True)
45
+
46
+ diagnosis = {
47
+ "entries": entries,
48
+ "timeline": timeline.build_timeline(execution_result, findings),
49
+ "recommendations": _recommendations(entries),
50
+ }
51
+ # Mechanical seam enforcement: diagnosis must match the internal contract.
52
+ return internal_contracts.validate_diagnosis(diagnosis)
53
+
54
+
55
+ def plan_repairs(diagnosis):
56
+ """Turn a diagnosis into repair plans (for qa-fix). One plan per entry;
57
+ non-repairable causes yield an escalation plan. Never produces code."""
58
+ plans = []
59
+ for entry in diagnosis["entries"]:
60
+ plan = repair.plan_repair(entry["rootCause"], affected_files=entry.get("affectedTests"))
61
+ plan["priority"] = entry["priority"]["priority"]
62
+ plans.append(plan)
63
+ return plans
64
+
65
+
66
+ def summarize(execution_result, diagnosis):
67
+ """Aggregate a diagnosis for qa-report: totals, breakdown by classification,
68
+ the top-priority findings, and a deterministic release-readiness call."""
69
+ tests = (execution_result or {}).get("tests", {})
70
+ by_class = {}
71
+ for entry in diagnosis["entries"]:
72
+ cls = entry["rootCause"]["classification"]
73
+ by_class[cls] = by_class.get(cls, 0) + 1
74
+
75
+ return {
76
+ "totals": tests,
77
+ "byClassification": by_class,
78
+ "topPriority": [e for e in diagnosis["entries"] if e["priority"]["priority"] == "P1"],
79
+ "releaseReadiness": _release_readiness(execution_result, diagnosis),
80
+ }
81
+
82
+
83
+ def _signals(execution_result, analysis_result):
84
+ """Derive failure signals, preferring the analysis platform's findings."""
85
+ signals = []
86
+ findings = (analysis_result or {}).get("findings", [])
87
+ if findings:
88
+ for f in findings:
89
+ signals.append({
90
+ "message": f.get("reason", ""),
91
+ "classification": f.get("classification"),
92
+ "confidence": f.get("confidence"),
93
+ "reason": f.get("reason"),
94
+ "httpStatus": f.get("httpStatus"),
95
+ "retries": f.get("retries", 0),
96
+ "finalStatus": f.get("finalStatus"),
97
+ "evidence": f.get("evidence", []),
98
+ "affectedTests": f.get("affectedTests", []),
99
+ })
100
+ return signals
101
+
102
+ for test in (execution_result or {}).get("executed", []):
103
+ if test.get("status") in ("failed", "flaky"):
104
+ signals.append({
105
+ "message": test.get("message", ""),
106
+ "retries": test.get("retries", 0),
107
+ "finalStatus": test.get("status"),
108
+ "affectedTests": [test.get("title", "")],
109
+ "evidence": [{
110
+ "type": "junit",
111
+ "description": f"{test.get('status')}: {test.get('title', '')}",
112
+ "source": test.get("file", "execution-result"),
113
+ }],
114
+ })
115
+ return signals
116
+
117
+
118
+ def _recommendations(entries):
119
+ """Ranked, de-duplicated recommendations — highest priority first."""
120
+ seen, ranked = set(), []
121
+ for entry in entries:
122
+ rec = entry["rootCause"]["recommendation"]
123
+ if rec not in seen:
124
+ seen.add(rec)
125
+ ranked.append({
126
+ "action": rec,
127
+ "priority": entry["priority"]["priority"],
128
+ "owner": entry["rootCause"]["ownership"],
129
+ "classification": entry["rootCause"]["classification"],
130
+ })
131
+ return ranked
132
+
133
+
134
+ def _release_readiness(execution_result, diagnosis):
135
+ failed = (execution_result or {}).get("tests", {}).get("failed", 0)
136
+ if failed == 0 and not diagnosis["entries"]:
137
+ return "ready"
138
+ classes = {e["rootCause"]["classification"] for e in diagnosis["entries"]}
139
+ if classes & _RELEASE_BLOCKING:
140
+ return "not-ready"
141
+ if classes <= {taxonomy.UNKNOWN}:
142
+ return "insufficient-data"
143
+ return "ready-with-risks"
@@ -0,0 +1,75 @@
1
+ """Validate internal Analysis → Diagnostics seam payloads.
2
+
3
+ Uses the existing dependency-free qa_analysis.contracts validator against
4
+ schemas under shared/diagnostics/schemas/internal/.
5
+
6
+ The schemas must be reachable in **both** layouts this package runs in:
7
+
8
+ - the repository, where they live at `shared/diagnostics/schemas/internal/`; and
9
+ - a bundled skill, where the installer materializes the package into
10
+ `<skill>/scripts/lib/qa_diagnostics/` and copies the schemas alongside it as
11
+ package data (`qa_diagnostics/schemas/internal/`).
12
+
13
+ Resolution therefore tries package-local data first and falls back to the
14
+ canonical repository path. Missing schemas raise `InternalContractError` naming
15
+ the locations tried, rather than a bare FileNotFoundError from deep inside a
16
+ diagnosis.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from pathlib import Path
23
+
24
+ from qa_analysis import contracts
25
+
26
+ _HERE = Path(__file__).resolve().parent
27
+
28
+ # Ordered candidates: bundled package data first, then the repository layout.
29
+ _SCHEMA_DIRS = (
30
+ _HERE / "schemas" / "internal",
31
+ _HERE.parents[1] / "schemas" / "internal",
32
+ )
33
+
34
+
35
+ class InternalContractError(ValueError):
36
+ """Raised when an internal seam payload fails its schema."""
37
+
38
+
39
+ def schema_dir() -> Path:
40
+ """The directory holding the internal schemas, whichever layout applies."""
41
+ for candidate in _SCHEMA_DIRS:
42
+ if candidate.is_dir():
43
+ return candidate
44
+ tried = ", ".join(str(c) for c in _SCHEMA_DIRS)
45
+ raise InternalContractError(
46
+ f"internal schemas not found; tried: {tried}. "
47
+ "A bundled skill must carry qa_diagnostics/schemas/internal/ as package data."
48
+ )
49
+
50
+
51
+ def _load(name: str) -> dict:
52
+ path = schema_dir() / name
53
+ if not path.is_file():
54
+ raise InternalContractError(f"internal schema missing: {path}")
55
+ return json.loads(path.read_text(encoding="utf-8"))
56
+
57
+
58
+ def _require(payload: dict, schema_name: str) -> dict:
59
+ schema = _load(schema_name)
60
+ ok, errors = contracts.validate(payload, schema)
61
+ if not ok:
62
+ raise InternalContractError(f"{schema_name}: " + "; ".join(errors))
63
+ return payload
64
+
65
+
66
+ def validate_analysis_result(payload: dict) -> dict:
67
+ return _require(payload, "analysis-result.schema.json")
68
+
69
+
70
+ def validate_execution_result_min(payload: dict) -> dict:
71
+ return _require(payload, "execution-result-min.schema.json")
72
+
73
+
74
+ def validate_diagnosis(payload: dict) -> dict:
75
+ return _require(payload, "diagnosis.schema.json")
@@ -0,0 +1,101 @@
1
+ """Deterministic finding prioritization.
2
+
3
+ Assigns every finding a severity, priority, the three impacts, an owner, and an
4
+ estimated effort — by a fixed algorithm, so the same finding always ranks the
5
+ same way. Priority is not a feeling; it is a function of severity, confidence,
6
+ and business impact.
7
+ """
8
+
9
+ from qa_analysis import taxonomy
10
+
11
+ # Base severity per classification (before confidence adjustment).
12
+ _SEVERITY = {
13
+ taxonomy.APPLICATION_BUG: "high",
14
+ taxonomy.INFRASTRUCTURE: "high",
15
+ taxonomy.NETWORK: "high",
16
+ taxonomy.AUTH: "high",
17
+ taxonomy.AUTHORIZATION: "high",
18
+ taxonomy.ASSERTION: "medium",
19
+ taxonomy.LOCATOR: "medium",
20
+ taxonomy.TIMEOUT: "medium",
21
+ taxonomy.TEST_DATA: "medium",
22
+ taxonomy.CONFIGURATION: "medium",
23
+ taxonomy.ENVIRONMENT: "medium",
24
+ taxonomy.FRAMEWORK: "medium",
25
+ taxonomy.FLAKY: "medium",
26
+ taxonomy.UNKNOWN: "low",
27
+ }
28
+
29
+ # Where the impact predominantly lands, per classification.
30
+ _IMPACT = {
31
+ taxonomy.APPLICATION_BUG: {"business": "high", "technical": "high", "testing": "low"},
32
+ taxonomy.NETWORK: {"business": "high", "technical": "high", "testing": "medium"},
33
+ taxonomy.AUTH: {"business": "high", "technical": "medium", "testing": "medium"},
34
+ taxonomy.AUTHORIZATION: {"business": "high", "technical": "medium", "testing": "medium"},
35
+ taxonomy.INFRASTRUCTURE: {"business": "medium", "technical": "high", "testing": "high"},
36
+ taxonomy.LOCATOR: {"business": "low", "technical": "low", "testing": "high"},
37
+ taxonomy.ASSERTION: {"business": "medium", "technical": "medium", "testing": "medium"},
38
+ taxonomy.TIMEOUT: {"business": "low", "technical": "medium", "testing": "high"},
39
+ taxonomy.TEST_DATA: {"business": "low", "technical": "low", "testing": "high"},
40
+ taxonomy.CONFIGURATION: {"business": "low", "technical": "medium", "testing": "high"},
41
+ taxonomy.ENVIRONMENT: {"business": "low", "technical": "medium", "testing": "high"},
42
+ taxonomy.FRAMEWORK: {"business": "low", "technical": "medium", "testing": "high"},
43
+ taxonomy.FLAKY: {"business": "low", "technical": "low", "testing": "high"},
44
+ taxonomy.UNKNOWN: {"business": "low", "technical": "low", "testing": "medium"},
45
+ }
46
+
47
+ # Rough effort to resolve, per classification.
48
+ _EFFORT = {
49
+ taxonomy.LOCATOR: "low",
50
+ taxonomy.ASSERTION: "low",
51
+ taxonomy.CONFIGURATION: "low",
52
+ taxonomy.ENVIRONMENT: "low",
53
+ taxonomy.TEST_DATA: "medium",
54
+ taxonomy.TIMEOUT: "medium",
55
+ taxonomy.FLAKY: "medium",
56
+ taxonomy.AUTH: "medium",
57
+ taxonomy.AUTHORIZATION: "medium",
58
+ taxonomy.FRAMEWORK: "medium",
59
+ taxonomy.NETWORK: "high",
60
+ taxonomy.INFRASTRUCTURE: "high",
61
+ taxonomy.APPLICATION_BUG: "external",
62
+ taxonomy.UNKNOWN: "unknown",
63
+ }
64
+
65
+ _RANK = {"low": 1, "medium": 2, "high": 3}
66
+ _PRIORITY = {1: "P3", 2: "P2", 3: "P1", 4: "P1"}
67
+
68
+
69
+ def prioritize(root_cause, blocking=False):
70
+ """Prioritize a root cause. Returns a dict with severity, priority, the three
71
+ impacts, confidence, owner, and estimatedEffort.
72
+
73
+ priority derives from severity, business impact, and confidence, and is
74
+ escalated one step when the failure blocks a release (blocking=True).
75
+ """
76
+ classification = root_cause["classification"]
77
+ confidence = root_cause.get("confidence", 0.5)
78
+ severity = _SEVERITY.get(classification, "low")
79
+ impact = _IMPACT.get(classification, _IMPACT[taxonomy.UNKNOWN])
80
+
81
+ # Priority score: severity and business impact drive it; low confidence
82
+ # holds it back (an uncertain finding should not top the queue).
83
+ score = _RANK[severity]
84
+ if impact["business"] == "high":
85
+ score += 1
86
+ if confidence < 0.5:
87
+ score -= 1
88
+ if blocking:
89
+ score += 1
90
+ score = max(1, min(4, score))
91
+
92
+ return {
93
+ "severity": severity,
94
+ "priority": _PRIORITY[score],
95
+ "businessImpact": impact["business"],
96
+ "technicalImpact": impact["technical"],
97
+ "testingImpact": impact["testing"],
98
+ "confidence": confidence,
99
+ "owner": root_cause.get("ownership", "needs-triage"),
100
+ "estimatedEffort": _EFFORT.get(classification, "unknown"),
101
+ }
@@ -0,0 +1,72 @@
1
+ """Deterministic repair planning.
2
+
3
+ Turns a root cause into a repair *plan* — never code. It decides whether the
4
+ failure is test-side repairable at all, and if so proposes an abstract change,
5
+ the candidate type, the risk, and a rollback. qa-fix consumes these plans; the
6
+ plan is always gated by the diff guard and always requires permission before any
7
+ edit is applied.
8
+ """
9
+
10
+ from qa_analysis import taxonomy
11
+
12
+ # classification -> (repairable, candidate type, abstract change, risk).
13
+ # Only test-side causes are repairable; product, network, infra, auth, and
14
+ # environment failures are escalations, not repairs.
15
+ _PLANS = {
16
+ taxonomy.LOCATOR: (True, "locator-update",
17
+ "Update the failing locator to target the same element in the current DOM.", "low"),
18
+ taxonomy.ASSERTION: (True, "assertion-improvement",
19
+ "Correct the assertion to match the intended behavior, or confirm a product bug first.", "medium"),
20
+ taxonomy.TIMEOUT: (True, "wait-strategy",
21
+ "Replace a fixed or missing wait with a web-first wait on the awaited condition.", "medium"),
22
+ taxonomy.FLAKY: (True, "synchronization",
23
+ "Remove the race by awaiting the real condition; add a tracked quarantine only if needed.", "medium"),
24
+ taxonomy.TEST_DATA: (True, "test-data",
25
+ "Repair or reseed the test data the scenario depends on.", "medium"),
26
+ taxonomy.CONFIGURATION: (True, "configuration",
27
+ "Correct the test configuration the run depends on.", "low"),
28
+ taxonomy.ENVIRONMENT: (False, "environment",
29
+ "Fix the environment (base URL, service availability); not a test-side repair.", "n/a"),
30
+ taxonomy.AUTH: (True, "authentication",
31
+ "Repair the test's credentials or auth setup; do not weaken the check.", "medium"),
32
+ taxonomy.AUTHORIZATION: (False, "authorization",
33
+ "Grant the test account permission or use an authorized role; not a code repair.", "n/a"),
34
+ taxonomy.NETWORK: (False, "network",
35
+ "Investigate the upstream service; not a test-side repair.", "n/a"),
36
+ taxonomy.INFRASTRUCTURE: (False, "infrastructure",
37
+ "Escalate to CI/infra; not a test-side repair.", "n/a"),
38
+ taxonomy.APPLICATION_BUG: (False, "application-bug",
39
+ "File a product bug; the test correctly caught a real defect.", "n/a"),
40
+ taxonomy.FRAMEWORK: (False, "framework",
41
+ "Update or pin the framework/driver; not a test-side repair.", "n/a"),
42
+ taxonomy.UNKNOWN: (False, "unknown",
43
+ "Investigate further before any repair.", "n/a"),
44
+ }
45
+
46
+
47
+ def plan_repair(root_cause, affected_files=None):
48
+ """Produce a repair plan for a root cause. Returns a dict:
49
+ {repairable, candidateType, proposedChanges, affectedFiles, risk,
50
+ permissionRequired, rollbackStrategy, safetyReview}. Never contains code.
51
+ """
52
+ classification = root_cause["classification"]
53
+ repairable, candidate, change, risk = _PLANS.get(classification, _PLANS[taxonomy.UNKNOWN])
54
+
55
+ if repairable:
56
+ safety = ("Any edit will be checked by the diff guard before it is proposed as complete; "
57
+ "the guard rejects removed assertions, added skips, forced passes, and timeout inflation.")
58
+ rollback = "No source is changed without approval; revert the proposed edits to roll back."
59
+ else:
60
+ safety = "No test-side edit is appropriate; this is an escalation, not a repair."
61
+ rollback = "Not applicable — no change is proposed."
62
+
63
+ return {
64
+ "repairable": repairable,
65
+ "candidateType": candidate,
66
+ "proposedChanges": [change] if repairable else [],
67
+ "affectedFiles": list(affected_files or []),
68
+ "risk": risk,
69
+ "permissionRequired": True,
70
+ "rollbackStrategy": rollback,
71
+ "safetyReview": safety,
72
+ }
@@ -0,0 +1,89 @@
1
+ """Deterministic root-cause analysis.
2
+
3
+ Turns a failure signal into a classified root cause with the four things every
4
+ classification must carry: a taxonomy class (with confidence and evidence-backed
5
+ reason), a recommended action, and an owner. It reuses the analysis platform's
6
+ failure taxonomy; it adds the ownership and recommendation mappings and the
7
+ metadata-driven classes (flaky) the taxonomy cannot infer from a message alone.
8
+
9
+ No unsupported conclusions: a signal that matches no rule is `unknown`.
10
+ """
11
+
12
+ from qa_analysis import taxonomy
13
+
14
+ # classification -> the party that typically owns the fix.
15
+ OWNERSHIP = {
16
+ taxonomy.ASSERTION: "test-author-or-product",
17
+ taxonomy.LOCATOR: "test-author",
18
+ taxonomy.TIMEOUT: "test-author-or-environment",
19
+ taxonomy.NETWORK: "backend-or-infrastructure",
20
+ taxonomy.AUTH: "auth-or-test-setup",
21
+ taxonomy.AUTHORIZATION: "permissions-or-test-account",
22
+ taxonomy.ENVIRONMENT: "environment-owner",
23
+ taxonomy.CONFIGURATION: "config-owner",
24
+ taxonomy.INFRASTRUCTURE: "ci-or-infrastructure",
25
+ taxonomy.TEST_DATA: "test-data-owner",
26
+ taxonomy.APPLICATION_BUG: "product",
27
+ taxonomy.FRAMEWORK: "framework-or-driver",
28
+ taxonomy.FLAKY: "test-author",
29
+ taxonomy.UNKNOWN: "needs-triage",
30
+ }
31
+
32
+ # classification -> the safe recommended action (implements the analysis
33
+ # platform's recommendation-guidelines; never recommends forcing a pass).
34
+ RECOMMENDATION = {
35
+ taxonomy.ASSERTION: "Confirm whether the app or the expectation is wrong; fix whichever is genuinely incorrect.",
36
+ taxonomy.LOCATOR: "Inspect the current DOM and update the locator to target the same element.",
37
+ taxonomy.TIMEOUT: "Investigate the slowness; raise a wait only if the operation is legitimately slower.",
38
+ taxonomy.NETWORK: "Check the upstream service and the request; retry only if the failure is genuinely transient.",
39
+ taxonomy.AUTH: "Fix the credentials or auth setup; do not weaken the authentication check.",
40
+ taxonomy.AUTHORIZATION: "Grant the test account the needed permission or use an authorized role; do not bypass the check.",
41
+ taxonomy.ENVIRONMENT: "Fix the environment (base URL, service availability); the test is likely fine.",
42
+ taxonomy.CONFIGURATION: "Correct the configuration; do not work around it in the test.",
43
+ taxonomy.INFRASTRUCTURE: "Escalate to CI or infrastructure owners; add resources, do not shrink the suite.",
44
+ taxonomy.TEST_DATA: "Repair or reseed the data; do not delete the assertion that caught the gap.",
45
+ taxonomy.APPLICATION_BUG: "File a bug against the product; do NOT modify the test to pass.",
46
+ taxonomy.FRAMEWORK: "Update or pin the framework/driver; report upstream if it is a genuine defect.",
47
+ taxonomy.FLAKY: "Stabilize the test (fix the race or synchronization); quarantine only with a tracking issue.",
48
+ taxonomy.UNKNOWN: "Investigate further; the evidence was insufficient to classify.",
49
+ }
50
+
51
+
52
+ def analyze(signal):
53
+ """Classify a failure signal into a root cause.
54
+
55
+ signal is a dict that may contain: message, httpStatus, retries, finalStatus,
56
+ evidence (a list of evidence refs). Returns a dict:
57
+ {classification, confidence, reason, ownership, recommendation, evidence}.
58
+ """
59
+ message = signal.get("message", "")
60
+ http_status = signal.get("httpStatus")
61
+ retries = signal.get("retries", 0) or 0
62
+ final_status = signal.get("finalStatus")
63
+
64
+ provided = signal.get("classification")
65
+
66
+ # Flakiness is a metadata signal, not a message pattern: a test that needed a
67
+ # retry to pass, or is explicitly flagged flaky, is nondeterministic.
68
+ if final_status == "flaky" or (retries > 0 and final_status in ("passed", "flaky")):
69
+ classification, confidence, reason = (
70
+ taxonomy.FLAKY, 0.8,
71
+ "The test passed only after a retry, indicating nondeterministic behavior.",
72
+ )
73
+ elif provided in taxonomy.CLASSES:
74
+ # The analysis platform already classified this deterministically; trust it
75
+ # rather than re-deriving from the message.
76
+ classification = provided
77
+ confidence = signal.get("confidence", 0.8)
78
+ reason = signal.get("reason") or f"Classified {provided} by the analysis platform."
79
+ else:
80
+ classification, confidence, reason = taxonomy.classify(message, http_status=http_status)
81
+
82
+ return {
83
+ "classification": classification,
84
+ "confidence": confidence,
85
+ "reason": reason,
86
+ "ownership": OWNERSHIP.get(classification, "needs-triage"),
87
+ "recommendation": RECOMMENDATION.get(classification, RECOMMENDATION[taxonomy.UNKNOWN]),
88
+ "evidence": signal.get("evidence", []),
89
+ }