qa-engineer 0.9.2 → 0.11.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.
- package/COMPATIBILITY.md +26 -12
- package/README.md +182 -84
- package/package.json +11 -20
- package/packages/engine/bin/qa-engine.mjs +560 -0
- package/packages/engine/lib/analysis/branding.mjs +187 -0
- package/packages/engine/lib/analysis/context.mjs +294 -0
- package/packages/engine/lib/analysis/contracts.mjs +149 -0
- package/packages/engine/lib/analysis/diff-guard.mjs +425 -0
- package/packages/engine/lib/analysis/discovery.mjs +220 -0
- package/packages/engine/lib/analysis/evidence.mjs +116 -0
- package/packages/engine/lib/analysis/har.mjs +124 -0
- package/packages/engine/lib/analysis/junit.mjs +126 -0
- package/packages/engine/lib/analysis/network.mjs +237 -0
- package/packages/engine/lib/analysis/redaction.mjs +127 -0
- package/packages/engine/lib/analysis/report-html.mjs +76 -0
- package/packages/engine/lib/analysis/taxonomy.mjs +109 -0
- package/packages/engine/lib/analysis/xml.mjs +153 -0
- package/packages/engine/lib/analysis/zip.mjs +107 -0
- package/packages/engine/lib/artifacts/manager.mjs +453 -0
- package/packages/engine/lib/artifacts/mime.mjs +109 -0
- package/packages/engine/lib/artifacts/zip-write.mjs +143 -0
- package/packages/engine/lib/diagnostics/engine.mjs +165 -0
- package/packages/engine/lib/diagnostics/internal-contracts.mjs +50 -0
- package/packages/engine/lib/diagnostics/prioritization.mjs +99 -0
- package/packages/engine/lib/diagnostics/repair.mjs +73 -0
- package/packages/engine/lib/diagnostics/root-cause.mjs +92 -0
- package/packages/engine/lib/diagnostics/timeline.mjs +101 -0
- package/packages/engine/lib/frameworks/junit-frameworks.mjs +48 -0
- package/packages/engine/lib/frameworks/playwright.mjs +158 -0
- package/packages/engine/lib/report/components/charts.mjs +424 -0
- package/packages/engine/lib/report/components/evidence.mjs +207 -0
- package/packages/engine/lib/report/components/findings.mjs +258 -0
- package/packages/engine/lib/report/components/nav.mjs +99 -0
- package/packages/engine/lib/report/components/primitives.mjs +246 -0
- package/packages/engine/lib/report/components/runtime.mjs +246 -0
- package/packages/engine/lib/report/components/timeline.mjs +65 -0
- package/packages/engine/lib/report/core/model.mjs +270 -0
- package/packages/engine/lib/report/core/normalize.mjs +226 -0
- package/packages/engine/lib/report/core/sections.mjs +978 -0
- package/packages/engine/lib/report/export/bundle.mjs +293 -0
- package/packages/engine/lib/report/export/html.mjs +183 -0
- package/packages/engine/lib/report/export/machine.mjs +290 -0
- package/packages/engine/lib/report/export/markdown.mjs +323 -0
- package/packages/engine/lib/report/schemas/qa-report.schema.json +555 -0
- package/packages/engine/lib/report/theme/css.mjs +529 -0
- package/packages/engine/lib/report/theme/tokens.mjs +137 -0
- package/packages/engine/lib/report/version.mjs +78 -0
- package/packages/engine/package.json +14 -0
- package/packages/installer/lib/agents/targets.mjs +90 -0
- package/packages/installer/lib/agents/user-level.mjs +80 -0
- package/packages/installer/lib/cli/flags.mjs +20 -2
- package/packages/installer/lib/commands/doctor.mjs +13 -20
- package/packages/installer/lib/commands/install.mjs +160 -91
- package/packages/installer/lib/commands/repair.mjs +10 -6
- package/packages/installer/lib/commands/self-test.mjs +4 -3
- package/packages/installer/lib/commands/uninstall.mjs +14 -8
- package/packages/installer/lib/commands/update.mjs +9 -4
- package/packages/installer/lib/commands/verify.mjs +13 -12
- package/packages/installer/lib/constants.mjs +13 -0
- package/packages/installer/lib/core/bundle.mjs +69 -92
- package/packages/installer/lib/core/conflict.mjs +5 -4
- package/packages/installer/lib/core/fs-safe.mjs +146 -6
- package/packages/installer/lib/core/integrity.mjs +59 -0
- package/packages/installer/lib/core/lockfile.mjs +19 -3
- package/packages/installer/lib/core/manifest.mjs +48 -57
- package/packages/installer/lib/core/plan.mjs +213 -0
- package/packages/installer/lib/core/qa-home.mjs +145 -0
- package/packages/installer/lib/core/scope.mjs +274 -0
- package/packages/installer/lib/core/validate-install.mjs +49 -31
- package/packages/installer/package.json +1 -1
- package/packages/installer/schemas/qa-lock.schema.json +119 -21
- package/shared/tooling/qa-tool.mjs +161 -0
- package/skills/qa-api/SKILL.md +5 -5
- package/skills/qa-api/references/deterministic-tooling.md +34 -32
- package/skills/qa-api/references/evidence-and-reporting.md +5 -5
- package/skills/qa-api/scripts/qa-tool.mjs +162 -0
- package/skills/qa-audit/SKILL.md +5 -5
- package/skills/qa-audit/references/deterministic-tooling.md +34 -32
- package/skills/qa-audit/references/evidence-and-reporting.md +5 -5
- package/skills/qa-audit/scripts/qa-tool.mjs +162 -0
- package/skills/qa-debug/SKILL.md +6 -6
- package/skills/qa-debug/references/deterministic-tooling.md +34 -32
- package/skills/qa-debug/references/diagnostic-engine.md +3 -3
- package/skills/qa-debug/references/evidence-and-reporting.md +5 -5
- package/skills/qa-debug/scripts/qa-tool.mjs +162 -0
- package/skills/qa-explore/SKILL.md +31 -15
- package/skills/qa-explore/contracts/explore-result.schema.json +517 -11
- package/skills/qa-explore/references/api-replay.md +40 -1
- package/skills/qa-explore/references/deterministic-tooling.md +34 -32
- package/skills/qa-explore/references/evidence-and-reporting.md +5 -5
- package/skills/qa-explore/references/report-pipeline.md +266 -96
- package/skills/qa-explore/scripts/qa-tool.mjs +162 -0
- package/skills/qa-fix/SKILL.md +4 -4
- package/skills/qa-fix/references/deterministic-tooling.md +34 -32
- package/skills/qa-fix/references/diagnostic-engine.md +3 -3
- package/skills/qa-fix/references/evidence-and-reporting.md +5 -5
- package/skills/qa-fix/scripts/qa-tool.mjs +162 -0
- package/skills/qa-flaky/SKILL.md +4 -4
- package/skills/qa-flaky/references/deterministic-tooling.md +34 -32
- package/skills/qa-flaky/references/evidence-and-reporting.md +5 -5
- package/skills/qa-flaky/scripts/qa-tool.mjs +162 -0
- package/skills/qa-generate/references/evidence-and-reporting.md +5 -5
- package/skills/qa-init/SKILL.md +4 -4
- package/skills/qa-init/references/deterministic-tooling.md +34 -32
- package/skills/qa-init/references/evidence-and-reporting.md +5 -5
- package/skills/qa-init/scripts/qa-tool.mjs +162 -0
- package/skills/qa-report/SKILL.md +7 -7
- package/skills/qa-report/references/deterministic-tooling.md +34 -32
- package/skills/qa-report/references/diagnostic-engine.md +3 -3
- package/skills/qa-report/references/evidence-and-reporting.md +5 -5
- package/skills/qa-report/scripts/qa-tool.mjs +162 -0
- package/skills/qa-review/references/evidence-and-reporting.md +5 -5
- package/skills/qa-run/SKILL.md +6 -6
- package/skills/qa-run/references/deterministic-tooling.md +34 -32
- package/skills/qa-run/references/evidence-and-reporting.md +5 -5
- package/skills/qa-run/scripts/qa-tool.mjs +162 -0
- package/shared/analysis/lib/qa_analysis/__init__.py +0 -11
- package/shared/analysis/lib/qa_analysis/branding.py +0 -175
- package/shared/analysis/lib/qa_analysis/cli.py +0 -144
- package/shared/analysis/lib/qa_analysis/context.py +0 -233
- package/shared/analysis/lib/qa_analysis/contracts.py +0 -158
- package/shared/analysis/lib/qa_analysis/diff_guard.py +0 -327
- package/shared/analysis/lib/qa_analysis/discovery.py +0 -113
- package/shared/analysis/lib/qa_analysis/evidence.py +0 -128
- package/shared/analysis/lib/qa_analysis/har.py +0 -87
- package/shared/analysis/lib/qa_analysis/junit.py +0 -104
- package/shared/analysis/lib/qa_analysis/redaction.py +0 -107
- package/shared/analysis/lib/qa_analysis/report_html.py +0 -781
- package/shared/analysis/lib/qa_analysis/taxonomy.py +0 -98
- package/shared/diagnostics/lib/qa_diagnostics/__init__.py +0 -13
- package/shared/diagnostics/lib/qa_diagnostics/cli.py +0 -124
- package/shared/diagnostics/lib/qa_diagnostics/engine.py +0 -143
- package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +0 -75
- package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +0 -101
- package/shared/diagnostics/lib/qa_diagnostics/repair.py +0 -72
- package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +0 -89
- package/shared/diagnostics/lib/qa_diagnostics/timeline.py +0 -71
- package/shared/frameworks/cypress/lib/cypress_analysis.py +0 -27
- package/shared/frameworks/playwright/lib/playwright_analysis.py +0 -167
- package/shared/frameworks/selenium/lib/selenium_analysis.py +0 -28
- package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +0 -24
- package/shared/tooling/qa_tool.py +0 -127
- /package/{shared/analysis/lib/qa_analysis → packages/engine/lib/analysis}/branding.json +0 -0
- /package/{shared → packages/engine/lib}/analysis/schemas/context.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/analysis-result.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/diagnosis.schema.json +0 -0
- /package/{shared → packages/engine/lib}/diagnostics/schemas/internal/execution-result-min.schema.json +0 -0
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""The canonical QA failure taxonomy and its deterministic classifier.
|
|
2
|
-
|
|
3
|
-
Classification maps observed signals to one of a closed set of failure classes.
|
|
4
|
-
It is rule-based and conservative: when no rule matches with sufficient signal,
|
|
5
|
-
the result is UNKNOWN. Unknown is preferable to incorrect.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import re
|
|
9
|
-
|
|
10
|
-
# The closed set of failure classifications. Every value implies a different
|
|
11
|
-
# owner and next action; if two would lead to the same action, they are merged.
|
|
12
|
-
ASSERTION = "assertion-failure"
|
|
13
|
-
LOCATOR = "locator-failure"
|
|
14
|
-
TIMEOUT = "timeout"
|
|
15
|
-
NETWORK = "network"
|
|
16
|
-
AUTH = "authentication"
|
|
17
|
-
AUTHORIZATION = "authorization"
|
|
18
|
-
ENVIRONMENT = "environment"
|
|
19
|
-
CONFIGURATION = "configuration"
|
|
20
|
-
INFRASTRUCTURE = "infrastructure"
|
|
21
|
-
TEST_DATA = "test-data"
|
|
22
|
-
APPLICATION_BUG = "application-bug"
|
|
23
|
-
FRAMEWORK = "framework-failure"
|
|
24
|
-
FLAKY = "flaky"
|
|
25
|
-
UNKNOWN = "unknown"
|
|
26
|
-
|
|
27
|
-
CLASSES = {
|
|
28
|
-
ASSERTION, LOCATOR, TIMEOUT, NETWORK, AUTH, AUTHORIZATION, ENVIRONMENT,
|
|
29
|
-
CONFIGURATION, INFRASTRUCTURE, TEST_DATA, APPLICATION_BUG, FRAMEWORK,
|
|
30
|
-
FLAKY, UNKNOWN,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
# Ordered rules: (classification, message-pattern, confidence, reason).
|
|
34
|
-
#
|
|
35
|
-
# Order encodes priority, and the first two rules exist because modern runners
|
|
36
|
-
# print a timeout budget in *every* assertion failure. Playwright's message for a
|
|
37
|
-
# plain text mismatch ends with "Timeout: 5000ms" even though nothing timed out,
|
|
38
|
-
# so a naive timeout rule captures assertion and locator failures alike and sends
|
|
39
|
-
# the reader to raise a timeout — the one action guaranteed not to help. The
|
|
40
|
-
# patterns below are derived from real captured runner output; see
|
|
41
|
-
# tests/test_analysis.py::RealRunnerMessageTests, which pins the exact strings.
|
|
42
|
-
#
|
|
43
|
-
# The discriminators, from that output:
|
|
44
|
-
# element missing -> "Error: element(s) not found" (no Received: value)
|
|
45
|
-
# value mismatch -> "Expected: X" + "Received: Y" (element was resolved)
|
|
46
|
-
# real timeout -> a timeout with neither of the above
|
|
47
|
-
_RULES = [
|
|
48
|
-
(LOCATOR, re.compile(r"(?i)(no such element|element(?:\(s\))?\s+not\s+(?:found|visible|attached)|locator.*(?:resolved to 0|not found)|waiting for (?:locator|selector)|unable to locate element)"), 0.8,
|
|
49
|
-
"Error indicates the target element could not be found or resolved."),
|
|
50
|
-
# A concrete expected-vs-received comparison is an assertion result, not a
|
|
51
|
-
# time budget: the runner resolved the target and compared values.
|
|
52
|
-
(ASSERTION, re.compile(r"(?is)expected:.*received:"), 0.8,
|
|
53
|
-
"Error shows a concrete expected-versus-received comparison, so the assertion did not hold."),
|
|
54
|
-
(TIMEOUT, re.compile(r"(?i)(timeout|timed out|exceeded .*ms|deadline exceeded)"), 0.75,
|
|
55
|
-
"Error indicates an operation exceeded its time budget."),
|
|
56
|
-
(AUTH, re.compile(r"(?i)(401 unauthorized|authentication failed|invalid (?:credentials|token)|login failed|not authenticated)"), 0.85,
|
|
57
|
-
"Error indicates an authentication failure — the identity could not be established."),
|
|
58
|
-
(AUTHORIZATION, re.compile(r"(?i)(403 forbidden|permission denied|access denied|not authorized|forbidden\b|insufficient (?:permission|privilege))"), 0.85,
|
|
59
|
-
"Error indicates an authorization failure — the identity lacked permission."),
|
|
60
|
-
(NETWORK, re.compile(r"(?i)(ECONNREFUSED| ENOTFOUND|net::ERR|connection (?:refused|reset)|5\d\d (?:internal server error|bad gateway|service unavailable)|fetch failed)"), 0.8,
|
|
61
|
-
"Error indicates a network or upstream service failure."),
|
|
62
|
-
(ASSERTION, re.compile(r"(?i)(assertion|expect(?:ed)?\b|toBe|toEqual|toHaveText|toBeVisible|AssertionError|expected .* (?:to|but))"), 0.7,
|
|
63
|
-
"Error indicates an assertion did not hold."),
|
|
64
|
-
(TEST_DATA, re.compile(r"(?i)(duplicate key|constraint violation|no rows|seed data|fixture .*(?:missing|not found)|invalid test data)"), 0.7,
|
|
65
|
-
"Error indicates a test-data problem."),
|
|
66
|
-
(CONFIGURATION, re.compile(r"(?i)(cannot find module|config(?:uration)? (?:error|not found)|missing (?:config|environment variable)|unknown option)"), 0.7,
|
|
67
|
-
"Error indicates a configuration problem."),
|
|
68
|
-
(ENVIRONMENT, re.compile(r"(?i)(base ?url|env(?:ironment)? .*(?:not set|missing)|ECONNREFUSED .*localhost|dev server)"), 0.6,
|
|
69
|
-
"Error indicates an environment problem such as a missing base URL or unreachable local service."),
|
|
70
|
-
(INFRASTRUCTURE, re.compile(r"(?i)(out of memory|OOM|disk (?:full|space)|browser .*(?:crash|closed unexpectedly)|worker .*(?:died|crashed))"), 0.7,
|
|
71
|
-
"Error indicates an infrastructure problem such as a crash or resource exhaustion."),
|
|
72
|
-
(FRAMEWORK, re.compile(r"(?i)(internal error|unexpected error in (?:playwright|selenium|cypress|webdriver)|driver .*mismatch)"), 0.6,
|
|
73
|
-
"Error indicates a fault in the test framework or driver itself."),
|
|
74
|
-
]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def classify(message, http_status=None):
|
|
78
|
-
"""Classify a failure from its error message and optional HTTP status.
|
|
79
|
-
|
|
80
|
-
Returns (classification, confidence, reason). Never guesses: an
|
|
81
|
-
unrecognized signal yields UNKNOWN at low confidence with an honest reason.
|
|
82
|
-
"""
|
|
83
|
-
text = message or ""
|
|
84
|
-
|
|
85
|
-
# A concrete HTTP status is stronger evidence than message text.
|
|
86
|
-
if http_status is not None:
|
|
87
|
-
if http_status == 401:
|
|
88
|
-
return (AUTH, 0.9, "HTTP 401 indicates an authentication failure.")
|
|
89
|
-
if http_status == 403:
|
|
90
|
-
return (AUTHORIZATION, 0.9, "HTTP 403 indicates an authorization failure.")
|
|
91
|
-
if 500 <= http_status <= 599:
|
|
92
|
-
return (NETWORK, 0.85, f"HTTP {http_status} indicates an upstream server failure.")
|
|
93
|
-
|
|
94
|
-
for classification, pattern, confidence, reason in _RULES:
|
|
95
|
-
if pattern.search(text):
|
|
96
|
-
return (classification, confidence, reason)
|
|
97
|
-
|
|
98
|
-
return (UNKNOWN, 0.2, "No classification rule matched the available signals; manual review needed.")
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""qa_diagnostics — the pack's shared diagnostic engine.
|
|
2
|
-
|
|
3
|
-
One place where failure reasoning lives: root-cause analysis, timeline
|
|
4
|
-
reconstruction, finding prioritization, and repair planning. The three
|
|
5
|
-
diagnostic skills (qa-debug, qa-fix, qa-report) consume this engine and differ
|
|
6
|
-
only in presentation — the reasoning is not duplicated across them.
|
|
7
|
-
|
|
8
|
-
Standard library only. Builds on qa_analysis (taxonomy, evidence, diff guard);
|
|
9
|
-
adds no framework-specific logic. Deterministic: the same inputs yield the same
|
|
10
|
-
diagnosis every time.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
__version__ = "0.1.0"
|
|
@@ -1,124 +0,0 @@
|
|
|
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())
|
|
@@ -1,143 +0,0 @@
|
|
|
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"
|
|
@@ -1,75 +0,0 @@
|
|
|
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")
|
|
@@ -1,101 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
}
|