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.
- package/COMPATIBILITY.md +71 -0
- package/LICENSE +21 -0
- package/README.md +508 -0
- package/package.json +90 -0
- package/packages/installer/README.md +46 -0
- package/packages/installer/bin/qa.mjs +136 -0
- package/packages/installer/lib/agents/registry.mjs +209 -0
- package/packages/installer/lib/cli/commands.mjs +29 -0
- package/packages/installer/lib/cli/flags.mjs +64 -0
- package/packages/installer/lib/commands/doctor.mjs +190 -0
- package/packages/installer/lib/commands/install.mjs +291 -0
- package/packages/installer/lib/commands/onboard.mjs +163 -0
- package/packages/installer/lib/commands/repair.mjs +68 -0
- package/packages/installer/lib/commands/self-test.mjs +45 -0
- package/packages/installer/lib/commands/uninstall.mjs +167 -0
- package/packages/installer/lib/commands/update.mjs +69 -0
- package/packages/installer/lib/commands/verify.mjs +62 -0
- package/packages/installer/lib/constants.mjs +34 -0
- package/packages/installer/lib/core/bundle.mjs +124 -0
- package/packages/installer/lib/core/config.mjs +73 -0
- package/packages/installer/lib/core/conflict.mjs +29 -0
- package/packages/installer/lib/core/errors.mjs +29 -0
- package/packages/installer/lib/core/fs-safe.mjs +236 -0
- package/packages/installer/lib/core/hash.mjs +19 -0
- package/packages/installer/lib/core/lockfile.mjs +75 -0
- package/packages/installer/lib/core/logger.mjs +46 -0
- package/packages/installer/lib/core/manifest.mjs +89 -0
- package/packages/installer/lib/core/paths.mjs +74 -0
- package/packages/installer/lib/core/schema-validate.mjs +142 -0
- package/packages/installer/lib/core/skill-meta.mjs +75 -0
- package/packages/installer/lib/core/validate-install.mjs +152 -0
- package/packages/installer/lib/core/wrappers.mjs +91 -0
- package/packages/installer/lib/detect/environment.mjs +54 -0
- package/packages/installer/lib/detect/frameworks.mjs +94 -0
- package/packages/installer/lib/detect/project.mjs +126 -0
- package/packages/installer/lib/detect/recommend.mjs +85 -0
- package/packages/installer/lib/detect/scan.mjs +48 -0
- package/packages/installer/lib/ui/progress.mjs +32 -0
- package/packages/installer/lib/ui/theme.mjs +81 -0
- package/packages/installer/lib/version.mjs +47 -0
- package/packages/installer/package.json +28 -0
- package/packages/installer/schemas/qa-lock.schema.json +89 -0
- package/packages/installer/schemas/qa.config.schema.json +91 -0
- package/shared/analysis/lib/qa_analysis/__init__.py +11 -0
- package/shared/analysis/lib/qa_analysis/branding.json +9 -0
- package/shared/analysis/lib/qa_analysis/branding.py +175 -0
- package/shared/analysis/lib/qa_analysis/cli.py +129 -0
- package/shared/analysis/lib/qa_analysis/context.py +233 -0
- package/shared/analysis/lib/qa_analysis/contracts.py +158 -0
- package/shared/analysis/lib/qa_analysis/diff_guard.py +327 -0
- package/shared/analysis/lib/qa_analysis/discovery.py +113 -0
- package/shared/analysis/lib/qa_analysis/evidence.py +128 -0
- package/shared/analysis/lib/qa_analysis/har.py +58 -0
- package/shared/analysis/lib/qa_analysis/junit.py +80 -0
- package/shared/analysis/lib/qa_analysis/redaction.py +99 -0
- package/shared/analysis/lib/qa_analysis/taxonomy.py +98 -0
- package/shared/analysis/schemas/context.schema.json +82 -0
- package/shared/diagnostics/lib/qa_diagnostics/__init__.py +13 -0
- package/shared/diagnostics/lib/qa_diagnostics/cli.py +124 -0
- package/shared/diagnostics/lib/qa_diagnostics/engine.py +143 -0
- package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +75 -0
- package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +101 -0
- package/shared/diagnostics/lib/qa_diagnostics/repair.py +72 -0
- package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +89 -0
- package/shared/diagnostics/lib/qa_diagnostics/timeline.py +71 -0
- package/shared/diagnostics/schemas/internal/analysis-result.schema.json +28 -0
- package/shared/diagnostics/schemas/internal/diagnosis.schema.json +55 -0
- package/shared/diagnostics/schemas/internal/execution-result-min.schema.json +34 -0
- package/shared/frameworks/cypress/lib/cypress_analysis.py +27 -0
- package/shared/frameworks/playwright/lib/playwright_analysis.py +167 -0
- package/shared/frameworks/registry.json +150 -0
- package/shared/frameworks/registry.mjs +37 -0
- package/shared/frameworks/registry.schema.json +74 -0
- package/shared/frameworks/selenium/lib/selenium_analysis.py +28 -0
- package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +24 -0
- package/shared/tooling/qa_tool.py +127 -0
- package/skills/README.md +31 -0
- package/skills/qa/README.md +19 -0
- package/skills/qa/SKILL.md +52 -0
- package/skills/qa/examples/routing.md +86 -0
- package/skills/qa/references/routing-map.md +33 -0
- package/skills/qa-api/README.md +19 -0
- package/skills/qa-api/SKILL.md +68 -0
- package/skills/qa-api/contracts/api-result.schema.json +51 -0
- package/skills/qa-api/examples/graphql-review.md +43 -0
- package/skills/qa-api/references/authentication.md +45 -0
- package/skills/qa-api/references/deterministic-tooling.md +128 -0
- package/skills/qa-api/references/evidence-and-reporting.md +66 -0
- package/skills/qa-api/references/graphql.md +44 -0
- package/skills/qa-api/references/rest.md +45 -0
- package/skills/qa-api/references/websocket.md +44 -0
- package/skills/qa-audit/README.md +19 -0
- package/skills/qa-audit/SKILL.md +70 -0
- package/skills/qa-audit/contracts/audit-result.schema.json +63 -0
- package/skills/qa-audit/examples/accessibility-audit.md +46 -0
- package/skills/qa-audit/references/accessibility.md +44 -0
- package/skills/qa-audit/references/deterministic-tooling.md +128 -0
- package/skills/qa-audit/references/evidence-and-reporting.md +66 -0
- package/skills/qa-audit/references/performance.md +44 -0
- package/skills/qa-audit/references/security.md +43 -0
- package/skills/qa-audit/references/visual-testing.md +44 -0
- package/skills/qa-debug/README.md +19 -0
- package/skills/qa-debug/SKILL.md +76 -0
- package/skills/qa-debug/contracts/debug-result.schema.json +121 -0
- package/skills/qa-debug/examples/failed-login.md +61 -0
- package/skills/qa-debug/examples/locator-break.md +59 -0
- package/skills/qa-debug/examples/network-timeout.md +61 -0
- package/skills/qa-debug/examples/successful-debug.md +62 -0
- package/skills/qa-debug/references/confidence-model.md +26 -0
- package/skills/qa-debug/references/deterministic-tooling.md +128 -0
- package/skills/qa-debug/references/diagnostic-engine.md +36 -0
- package/skills/qa-debug/references/evidence-and-reporting.md +66 -0
- package/skills/qa-debug/references/evidence-model.md +41 -0
- package/skills/qa-debug/references/failure-taxonomy.md +44 -0
- package/skills/qa-debug/references/finding-prioritization.md +43 -0
- package/skills/qa-debug/references/investigation-workflow.md +48 -0
- package/skills/qa-debug/references/recommendation-ranking.md +34 -0
- package/skills/qa-debug/references/root-cause-analysis.md +49 -0
- package/skills/qa-debug/references/timeline-builder.md +37 -0
- package/skills/qa-example/README.md +19 -0
- package/skills/qa-example/SKILL.md +60 -0
- package/skills/qa-example/contracts/self-check-report.schema.json +65 -0
- package/skills/qa-example/examples/self-check.md +53 -0
- package/skills/qa-example/references/example-domain.md +28 -0
- package/skills/qa-example/references/skill-format-notes.md +26 -0
- package/skills/qa-explore/README.md +22 -0
- package/skills/qa-explore/SKILL.md +87 -0
- package/skills/qa-explore/contracts/explore-result.schema.json +249 -0
- package/skills/qa-explore/examples/attached-test-cases.md +70 -0
- package/skills/qa-explore/examples/screenshot-proof-finding.md +50 -0
- package/skills/qa-explore/examples/url-smoke-explore.md +80 -0
- package/skills/qa-explore/references/accessibility.md +44 -0
- package/skills/qa-explore/references/api-replay.md +48 -0
- package/skills/qa-explore/references/browser-adapters.md +51 -0
- package/skills/qa-explore/references/evidence-and-reporting.md +66 -0
- package/skills/qa-explore/references/evidence-capture.md +54 -0
- package/skills/qa-explore/references/exploratory-qa.md +52 -0
- package/skills/qa-explore/references/finding-taxonomy.md +47 -0
- package/skills/qa-explore/references/performance.md +44 -0
- package/skills/qa-explore/references/pipeline.md +74 -0
- package/skills/qa-explore/references/report-pipeline.md +69 -0
- package/skills/qa-explore/references/security.md +43 -0
- package/skills/qa-explore/references/test-case-intake.md +44 -0
- package/skills/qa-fix/README.md +19 -0
- package/skills/qa-fix/SKILL.md +70 -0
- package/skills/qa-fix/contracts/fix-result.schema.json +132 -0
- package/skills/qa-fix/examples/repair-plan.md +56 -0
- package/skills/qa-fix/references/deterministic-tooling.md +128 -0
- package/skills/qa-fix/references/diagnostic-engine.md +36 -0
- package/skills/qa-fix/references/evidence-and-reporting.md +66 -0
- package/skills/qa-fix/references/repair-strategy.md +48 -0
- package/skills/qa-fix/references/root-cause-analysis.md +49 -0
- package/skills/qa-fix/references/suite-extension.md +73 -0
- package/skills/qa-flaky/README.md +19 -0
- package/skills/qa-flaky/SKILL.md +67 -0
- package/skills/qa-flaky/contracts/flaky-result.schema.json +62 -0
- package/skills/qa-flaky/examples/flaky-locator.md +46 -0
- package/skills/qa-flaky/references/deterministic-tooling.md +128 -0
- package/skills/qa-flaky/references/evidence-and-reporting.md +66 -0
- package/skills/qa-flaky/references/flakiness.md +48 -0
- package/skills/qa-flaky/references/retry.md +44 -0
- package/skills/qa-flaky/references/waiting-strategies.md +44 -0
- package/skills/qa-generate/README.md +42 -0
- package/skills/qa-generate/SKILL.md +83 -0
- package/skills/qa-generate/contracts/generation-result.schema.json +124 -0
- package/skills/qa-generate/examples/bootstrap-new-framework.md +24 -0
- package/skills/qa-generate/examples/extend-existing-suite.md +26 -0
- package/skills/qa-generate/references/code-style.md +29 -0
- package/skills/qa-generate/references/evidence-and-reporting.md +66 -0
- package/skills/qa-generate/references/framework-selection.md +88 -0
- package/skills/qa-generate/references/generation-strategy.md +48 -0
- package/skills/qa-generate/references/naming-conventions.md +27 -0
- package/skills/qa-generate/references/playwright-conventions.md +23 -0
- package/skills/qa-generate/references/playwright-generation.md +41 -0
- package/skills/qa-generate/references/playwright-project-discovery.md +42 -0
- package/skills/qa-generate/references/project-bootstrap.md +54 -0
- package/skills/qa-generate/references/repository-analysis.md +43 -0
- package/skills/qa-generate/references/suite-extension.md +73 -0
- package/skills/qa-generate/references/template-selection.md +48 -0
- package/skills/qa-generate/templates/playwright/api.ts +36 -0
- package/skills/qa-generate/templates/playwright/base-page.ts +14 -0
- package/skills/qa-generate/templates/playwright/data.ts +25 -0
- package/skills/qa-generate/templates/playwright/env.example +16 -0
- package/skills/qa-generate/templates/playwright/example.spec.ts +14 -0
- package/skills/qa-generate/templates/playwright/fixtures.ts +28 -0
- package/skills/qa-generate/templates/playwright/framework-readme.md +39 -0
- package/skills/qa-generate/templates/playwright/login.page.ts +26 -0
- package/skills/qa-generate/templates/playwright/playwright.config.ts +28 -0
- package/skills/qa-generate/templates/playwright/utils.ts +18 -0
- package/skills/qa-init/README.md +20 -0
- package/skills/qa-init/SKILL.md +72 -0
- package/skills/qa-init/examples/initialize-a-repo.md +91 -0
- package/skills/qa-init/references/detection-guide.md +76 -0
- package/skills/qa-init/references/deterministic-tooling.md +128 -0
- package/skills/qa-init/references/evidence-and-reporting.md +66 -0
- package/skills/qa-init/templates/context.md +68 -0
- package/skills/qa-report/README.md +19 -0
- package/skills/qa-report/SKILL.md +72 -0
- package/skills/qa-report/contracts/report-result.schema.json +186 -0
- package/skills/qa-report/examples/release-report.md +68 -0
- package/skills/qa-report/references/deterministic-tooling.md +128 -0
- package/skills/qa-report/references/diagnostic-engine.md +36 -0
- package/skills/qa-report/references/evidence-and-reporting.md +66 -0
- package/skills/qa-report/references/finding-prioritization.md +43 -0
- package/skills/qa-report/references/recommendation-ranking.md +34 -0
- package/skills/qa-report/references/report-aggregation.md +44 -0
- package/skills/qa-review/README.md +19 -0
- package/skills/qa-review/SKILL.md +58 -0
- package/skills/qa-review/contracts/review-result.schema.json +59 -0
- package/skills/qa-review/examples/suite-review.md +51 -0
- package/skills/qa-review/references/anti-patterns.md +49 -0
- package/skills/qa-review/references/assertion-patterns.md +45 -0
- package/skills/qa-review/references/evidence-and-reporting.md +66 -0
- package/skills/qa-review/references/fixtures.md +45 -0
- package/skills/qa-review/references/page-objects.md +45 -0
- package/skills/qa-run/README.md +22 -0
- package/skills/qa-run/SKILL.md +92 -0
- package/skills/qa-run/contracts/execution-plan.schema.json +132 -0
- package/skills/qa-run/contracts/execution-result.schema.json +204 -0
- package/skills/qa-run/examples/execute-playwright.md +89 -0
- package/skills/qa-run/examples/plan-a-run.md +83 -0
- package/skills/qa-run/references/artifact-collector.md +40 -0
- package/skills/qa-run/references/browser-launch.md +39 -0
- package/skills/qa-run/references/command-builder.md +39 -0
- package/skills/qa-run/references/deterministic-tooling.md +128 -0
- package/skills/qa-run/references/environment-detection.md +37 -0
- package/skills/qa-run/references/evidence-and-reporting.md +66 -0
- package/skills/qa-run/references/execution-strategy.md +54 -0
- package/skills/qa-run/references/playwright-artifacts.md +29 -0
- package/skills/qa-run/references/playwright-execution.md +48 -0
- package/skills/qa-run/references/playwright-project-discovery.md +42 -0
- package/skills/qa-run/references/report-normalization.md +49 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""The evidence and finding model shared by every analyzer.
|
|
2
|
+
|
|
3
|
+
Every finding an analyzer produces carries the same structure, so downstream
|
|
4
|
+
skills (qa-debug, qa-report, qa-fix) consume one shape regardless of which
|
|
5
|
+
analyzer or framework produced it. Text fields are redacted at construction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field, asdict
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from .redaction import redact_text
|
|
12
|
+
|
|
13
|
+
EVIDENCE_TYPES = {
|
|
14
|
+
"trace", "har", "junit", "report", "console", "network", "stdout",
|
|
15
|
+
"stderr", "screenshot", "video", "log", "file", "diff",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def utc_now():
|
|
20
|
+
"""ISO 8601 UTC timestamp. Isolated so tests can monkeypatch it."""
|
|
21
|
+
return datetime.now(timezone.utc).isoformat()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Evidence:
|
|
26
|
+
"""One observation supporting a finding. Excerpts are redacted."""
|
|
27
|
+
type: str
|
|
28
|
+
description: str
|
|
29
|
+
source: str
|
|
30
|
+
excerpt: str = ""
|
|
31
|
+
|
|
32
|
+
def __post_init__(self):
|
|
33
|
+
if self.type not in EVIDENCE_TYPES:
|
|
34
|
+
raise ValueError(f"unknown evidence type: {self.type}")
|
|
35
|
+
self.excerpt = redact_text(self.excerpt)
|
|
36
|
+
|
|
37
|
+
def to_dict(self):
|
|
38
|
+
data = {"type": self.type, "description": self.description, "source": self.source}
|
|
39
|
+
if self.excerpt:
|
|
40
|
+
data["excerpt"] = self.excerpt
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Finding:
|
|
46
|
+
"""A single diagnostic conclusion, traceable to a specific artifact.
|
|
47
|
+
|
|
48
|
+
Carries everything the evidence model requires: the artifact and location
|
|
49
|
+
it came from, when, why, the supporting evidence, a calibrated confidence,
|
|
50
|
+
the affected tests, related artifacts, and recommended actions.
|
|
51
|
+
"""
|
|
52
|
+
classification: str
|
|
53
|
+
reason: str
|
|
54
|
+
artifact: str
|
|
55
|
+
location: str
|
|
56
|
+
confidence: float = None
|
|
57
|
+
timestamp: str = field(default_factory=utc_now)
|
|
58
|
+
evidence: list = field(default_factory=list)
|
|
59
|
+
affected_tests: list = field(default_factory=list)
|
|
60
|
+
related_artifacts: list = field(default_factory=list)
|
|
61
|
+
recommendations: list = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
def to_dict(self):
|
|
64
|
+
data = {
|
|
65
|
+
"classification": self.classification,
|
|
66
|
+
"reason": self.reason,
|
|
67
|
+
"artifact": self.artifact,
|
|
68
|
+
"location": self.location,
|
|
69
|
+
"timestamp": self.timestamp,
|
|
70
|
+
"evidence": [e.to_dict() if isinstance(e, Evidence) else e for e in self.evidence],
|
|
71
|
+
"affectedTests": self.affected_tests,
|
|
72
|
+
"relatedArtifacts": self.related_artifacts,
|
|
73
|
+
"recommendations": self.recommendations,
|
|
74
|
+
}
|
|
75
|
+
if self.confidence is not None:
|
|
76
|
+
data["confidence"] = self.confidence
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class AnalyzerOutput:
|
|
82
|
+
"""The envelope an analyzer emits: findings plus the artifacts it examined.
|
|
83
|
+
|
|
84
|
+
A downstream skill wraps this in its own output contract; on its own it is
|
|
85
|
+
the deterministic, machine-readable result of one analysis.
|
|
86
|
+
"""
|
|
87
|
+
analyzer: str
|
|
88
|
+
findings: list = field(default_factory=list)
|
|
89
|
+
artifacts: list = field(default_factory=list)
|
|
90
|
+
warnings: list = field(default_factory=list)
|
|
91
|
+
generated_at: str = field(default_factory=utc_now)
|
|
92
|
+
|
|
93
|
+
def to_dict(self):
|
|
94
|
+
return {
|
|
95
|
+
"analyzer": self.analyzer,
|
|
96
|
+
"generatedAt": self.generated_at,
|
|
97
|
+
"findings": [f.to_dict() if isinstance(f, Finding) else f for f in self.findings],
|
|
98
|
+
"artifacts": [a.to_dict() if hasattr(a, "to_dict") else a for a in self.artifacts],
|
|
99
|
+
"warnings": self.warnings,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Artifact:
|
|
105
|
+
"""A discovered artifact, in the common model shared with the execution engine."""
|
|
106
|
+
type: str
|
|
107
|
+
location: str
|
|
108
|
+
framework: str = "unknown"
|
|
109
|
+
ownership: str = "qa-analysis"
|
|
110
|
+
timestamp: str = field(default_factory=utc_now)
|
|
111
|
+
media_type: str = ""
|
|
112
|
+
test_ref: str = ""
|
|
113
|
+
present: bool = True
|
|
114
|
+
|
|
115
|
+
def to_dict(self):
|
|
116
|
+
data = {
|
|
117
|
+
"type": self.type,
|
|
118
|
+
"location": self.location,
|
|
119
|
+
"framework": self.framework,
|
|
120
|
+
"timestamp": self.timestamp,
|
|
121
|
+
"ownership": self.ownership,
|
|
122
|
+
"present": self.present,
|
|
123
|
+
}
|
|
124
|
+
if self.media_type:
|
|
125
|
+
data["mediaType"] = self.media_type
|
|
126
|
+
if self.test_ref:
|
|
127
|
+
data["testRef"] = self.test_ref
|
|
128
|
+
return data
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""HAR (HTTP Archive) parser.
|
|
2
|
+
|
|
3
|
+
Framework-agnostic: a HAR is a standard JSON format, whoever produced it.
|
|
4
|
+
Extracts request outcomes, flags failures and slow calls, and redacts headers
|
|
5
|
+
and credentialed URLs before anything is exposed. A malformed HAR raises.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from .redaction import redact_text, redact_headers
|
|
11
|
+
from .junit import MalformedArtifact
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_har(path, slow_ms=1000):
|
|
15
|
+
"""Parse a HAR file into a redacted network summary.
|
|
16
|
+
|
|
17
|
+
Returns {entries, failures, slow, redacted: True}, where each entry is
|
|
18
|
+
{method, url, status, durationMs, headersRedacted}. Failures are entries
|
|
19
|
+
with status >= 400 or status 0 (no response). Raises MalformedArtifact on
|
|
20
|
+
unreadable input.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
24
|
+
data = json.load(handle)
|
|
25
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as exc:
|
|
26
|
+
raise MalformedArtifact(f"could not parse HAR at {path}: {exc}") from exc
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
raw_entries = data["log"]["entries"]
|
|
30
|
+
except (KeyError, TypeError) as exc:
|
|
31
|
+
raise MalformedArtifact(f"not a HAR document at {path}") from exc
|
|
32
|
+
|
|
33
|
+
entries = []
|
|
34
|
+
for item in raw_entries:
|
|
35
|
+
request = item.get("request", {})
|
|
36
|
+
response = item.get("response", {})
|
|
37
|
+
status = _int(response.get("status"))
|
|
38
|
+
entry = {
|
|
39
|
+
"method": request.get("method", ""),
|
|
40
|
+
# Redaction strips any credentials embedded in the URL.
|
|
41
|
+
"url": redact_text(request.get("url", "")),
|
|
42
|
+
"status": status,
|
|
43
|
+
"durationMs": int(round(float(item.get("time", 0) or 0))),
|
|
44
|
+
"requestHeaders": redact_headers(request.get("headers", [])),
|
|
45
|
+
"responseHeaders": redact_headers(response.get("headers", [])),
|
|
46
|
+
}
|
|
47
|
+
entries.append(entry)
|
|
48
|
+
|
|
49
|
+
failures = [e for e in entries if e["status"] == 0 or e["status"] >= 400]
|
|
50
|
+
slow = [e for e in entries if e["durationMs"] >= slow_ms]
|
|
51
|
+
return {"entries": entries, "failures": failures, "slow": slow, "redacted": True}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _int(value, default=0):
|
|
55
|
+
try:
|
|
56
|
+
return int(value)
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return default
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""JUnit XML parser.
|
|
2
|
+
|
|
3
|
+
Framework-agnostic: Playwright, Selenium, Cypress, WebdriverIO, and most unit
|
|
4
|
+
runners emit JUnit XML, so this one parser normalizes them all into the pack's
|
|
5
|
+
per-test result shape. This is the concrete proof that different frameworks
|
|
6
|
+
share one contract — only where the file lives differs, not how it is read.
|
|
7
|
+
|
|
8
|
+
Parses deterministically; a malformed document raises rather than guessing.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
12
|
+
|
|
13
|
+
from .redaction import redact_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MalformedArtifact(ValueError):
|
|
17
|
+
"""Raised when an artifact cannot be parsed. Never swallowed into a guess."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _int(value, default=0):
|
|
21
|
+
try:
|
|
22
|
+
return int(value)
|
|
23
|
+
except (TypeError, ValueError):
|
|
24
|
+
return default
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_junit(path):
|
|
28
|
+
"""Parse a JUnit XML file into a normalized result.
|
|
29
|
+
|
|
30
|
+
Returns a dict: {tests: {total, passed, failed, skipped}, executed: [...]}.
|
|
31
|
+
Each executed entry: {title, file, status, durationMs, message?}.
|
|
32
|
+
Raises MalformedArtifact if the XML is unreadable or not JUnit-shaped.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
tree = ET.parse(path)
|
|
36
|
+
except (ET.ParseError, OSError) as exc:
|
|
37
|
+
raise MalformedArtifact(f"could not parse JUnit XML at {path}: {exc}") from exc
|
|
38
|
+
|
|
39
|
+
root = tree.getroot()
|
|
40
|
+
# Accept either a <testsuites> root or a single <testsuite> root.
|
|
41
|
+
if root.tag == "testsuites":
|
|
42
|
+
suites = root.findall("testsuite")
|
|
43
|
+
elif root.tag == "testsuite":
|
|
44
|
+
suites = [root]
|
|
45
|
+
else:
|
|
46
|
+
raise MalformedArtifact(f"not a JUnit document (root <{root.tag}>) at {path}")
|
|
47
|
+
|
|
48
|
+
executed = []
|
|
49
|
+
for suite in suites:
|
|
50
|
+
for case in suite.findall("testcase"):
|
|
51
|
+
failure = case.find("failure")
|
|
52
|
+
error = case.find("error")
|
|
53
|
+
skipped = case.find("skipped")
|
|
54
|
+
if failure is not None or error is not None:
|
|
55
|
+
status = "failed"
|
|
56
|
+
node = failure if failure is not None else error
|
|
57
|
+
message = redact_text((node.get("message") or node.text or "").strip())
|
|
58
|
+
elif skipped is not None:
|
|
59
|
+
status = "skipped"
|
|
60
|
+
message = redact_text((skipped.get("message") or "").strip())
|
|
61
|
+
else:
|
|
62
|
+
status = "passed"
|
|
63
|
+
message = ""
|
|
64
|
+
entry = {
|
|
65
|
+
"title": case.get("name", ""),
|
|
66
|
+
"file": case.get("classname", ""),
|
|
67
|
+
"status": status,
|
|
68
|
+
"durationMs": int(round(float(case.get("time", "0") or "0") * 1000)),
|
|
69
|
+
}
|
|
70
|
+
if message:
|
|
71
|
+
entry["message"] = message
|
|
72
|
+
executed.append(entry)
|
|
73
|
+
|
|
74
|
+
counts = {
|
|
75
|
+
"total": len(executed),
|
|
76
|
+
"passed": sum(1 for e in executed if e["status"] == "passed"),
|
|
77
|
+
"failed": sum(1 for e in executed if e["status"] == "failed"),
|
|
78
|
+
"skipped": sum(1 for e in executed if e["status"] == "skipped"),
|
|
79
|
+
}
|
|
80
|
+
return {"tests": counts, "executed": executed}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Credential, secret, token, and PII redaction.
|
|
2
|
+
|
|
3
|
+
Every analyzer runs artifact text through redaction before it appears in a
|
|
4
|
+
finding, a report, or stdout. Redaction happens as evidence is captured, not
|
|
5
|
+
after — a secret must never reach a model's context or a log. Standard library
|
|
6
|
+
only; deterministic.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# Ordered (name, pattern, replacement). Order matters: match structured,
|
|
12
|
+
# high-signal secrets (JWTs, provider keys) before generic fallbacks.
|
|
13
|
+
_RULES = [
|
|
14
|
+
("jwt", re.compile(r"eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}")),
|
|
15
|
+
("aws-access-key", re.compile(r"AKIA[0-9A-Z]{16}")),
|
|
16
|
+
("github-token", re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}")),
|
|
17
|
+
("slack-token", re.compile(r"xox[baprs]-[A-Za-z0-9-]{10,}")),
|
|
18
|
+
("openai-key", re.compile(r"sk-[A-Za-z0-9]{20,}")),
|
|
19
|
+
("bearer", re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/-]+=*")),
|
|
20
|
+
# Sensitive header lines: keep the header name, mask the value.
|
|
21
|
+
("auth-header", re.compile(r"(?im)^(\s*(?:authorization|proxy-authorization)\s*[:=]\s*).+$")),
|
|
22
|
+
("cookie-header", re.compile(r"(?im)^(\s*(?:set-cookie|cookie)\s*[:=]\s*).+$")),
|
|
23
|
+
# Secret-like assignments: key=value / "key": "value".
|
|
24
|
+
("assigned-secret", re.compile(
|
|
25
|
+
r'(?i)(\b(?:password|passwd|pwd|secret|token|api[_-]?key|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*["\']?)'
|
|
26
|
+
r'([^\s"\'&]{4,})')),
|
|
27
|
+
# Credentials in URLs and query strings.
|
|
28
|
+
("url-credential", re.compile(r"(?i)(://[^:/@\s]+:)([^@/\s]+)(@)")),
|
|
29
|
+
("query-secret", re.compile(
|
|
30
|
+
r"(?i)([?&](?:password|passwd|pwd|token|secret|api[_-]?key|access[_-]?key)=)([^&\s#]+)")),
|
|
31
|
+
# PII: email addresses.
|
|
32
|
+
("email", re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Rules whose replacement preserves a leading captured group (the label/prefix).
|
|
36
|
+
_PREFIX_PRESERVING = {
|
|
37
|
+
"auth-header": r"\1[REDACTED:auth-header]",
|
|
38
|
+
"cookie-header": r"\1[REDACTED:cookie-header]",
|
|
39
|
+
"assigned-secret": r"\1[REDACTED:secret]",
|
|
40
|
+
"url-credential": r"\1[REDACTED:credential]\3",
|
|
41
|
+
"query-secret": r"\1[REDACTED:secret]",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def redact_text(text):
|
|
46
|
+
"""Return text with every recognized secret or PII value masked."""
|
|
47
|
+
if not text:
|
|
48
|
+
return text
|
|
49
|
+
result = text
|
|
50
|
+
for name, pattern in _RULES:
|
|
51
|
+
if name in _PREFIX_PRESERVING:
|
|
52
|
+
result = pattern.sub(_PREFIX_PRESERVING[name], result)
|
|
53
|
+
else:
|
|
54
|
+
result = pattern.sub(f"[REDACTED:{name}]", result)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def detect_secrets(text):
|
|
59
|
+
"""Return a list of {type, start, end} for secrets found — never the values.
|
|
60
|
+
|
|
61
|
+
Used to decide whether an artifact is safe to expose, without surfacing the
|
|
62
|
+
secret itself.
|
|
63
|
+
"""
|
|
64
|
+
findings = []
|
|
65
|
+
if not text:
|
|
66
|
+
return findings
|
|
67
|
+
for name, pattern in _RULES:
|
|
68
|
+
for match in pattern.finditer(text):
|
|
69
|
+
findings.append({"type": name, "start": match.start(), "end": match.end()})
|
|
70
|
+
findings.sort(key=lambda f: f["start"])
|
|
71
|
+
return findings
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
_SENSITIVE_HEADERS = {
|
|
75
|
+
"authorization", "proxy-authorization", "cookie", "set-cookie",
|
|
76
|
+
"x-api-key", "x-auth-token", "api-key", "x-csrf-token",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def redact_headers(headers):
|
|
81
|
+
"""Mask the values of sensitive HTTP headers.
|
|
82
|
+
|
|
83
|
+
Accepts a dict, or a list of {"name","value"} entries (HAR shape), and
|
|
84
|
+
returns the same shape with sensitive values replaced.
|
|
85
|
+
"""
|
|
86
|
+
def mask(name, value):
|
|
87
|
+
return "[REDACTED:header]" if name.strip().lower() in _SENSITIVE_HEADERS else redact_text(value)
|
|
88
|
+
|
|
89
|
+
if isinstance(headers, dict):
|
|
90
|
+
return {k: mask(k, str(v)) for k, v in headers.items()}
|
|
91
|
+
if isinstance(headers, list):
|
|
92
|
+
out = []
|
|
93
|
+
for entry in headers:
|
|
94
|
+
if isinstance(entry, dict) and "name" in entry:
|
|
95
|
+
out.append({**entry, "value": mask(str(entry["name"]), str(entry.get("value", "")))})
|
|
96
|
+
else:
|
|
97
|
+
out.append(entry)
|
|
98
|
+
return out
|
|
99
|
+
return headers
|
|
@@ -0,0 +1,98 @@
|
|
|
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.")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "urn:qa-pack:schema:context:1",
|
|
4
|
+
"title": "QA project context frontmatter",
|
|
5
|
+
"description": "The machine-readable frontmatter of .qa/context.md, produced by qa-init and read by every skill. Fields are always present; an undetermined value is null, never omitted. See docs/architecture/context-contract.md for semantics.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": [
|
|
9
|
+
"schemaVersion", "generatedBy", "generatedAt", "repository", "language",
|
|
10
|
+
"packageManager", "testFramework", "browserAutomation", "apiStyles", "ci",
|
|
11
|
+
"conventions", "confidence"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"schemaVersion": { "const": 1 },
|
|
15
|
+
"generatedBy": { "type": "string", "minLength": 1 },
|
|
16
|
+
"generatedAt": { "type": "string", "format": "date-time" },
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"required": ["root", "monorepo", "packages"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"root": { "type": "string" },
|
|
22
|
+
"monorepo": { "type": "boolean" },
|
|
23
|
+
"packages": { "type": "array" }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"language": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"required": ["primary"],
|
|
29
|
+
"properties": {
|
|
30
|
+
"primary": { "type": ["string", "null"] },
|
|
31
|
+
"others": { "type": "array", "items": { "type": "string" } }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"runtime": { "type": "object" },
|
|
35
|
+
"packageManager": {
|
|
36
|
+
"type": ["string", "null"],
|
|
37
|
+
"enum": ["npm", "pnpm", "yarn", "maven", "gradle", "pip", "poetry", "nuget", null]
|
|
38
|
+
},
|
|
39
|
+
"buildTool": { "type": ["string", "null"] },
|
|
40
|
+
"testFramework": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"required": ["unit", "e2e", "bdd"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"unit": { "type": ["string", "null"] },
|
|
45
|
+
"e2e": { "type": ["string", "null"], "enum": ["playwright", "selenium", "cypress", "webdriverio", null] },
|
|
46
|
+
"bdd": { "type": ["string", "null"] }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"browserAutomation": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"required": ["tool", "mcp"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"tool": { "type": ["string", "null"] },
|
|
54
|
+
"mcp": { "type": "boolean" }
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"apiStyles": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": { "type": "string", "enum": ["rest", "graphql", "websocket"] }
|
|
60
|
+
},
|
|
61
|
+
"ci": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"required": ["provider", "workflows"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"provider": { "type": ["string", "null"], "enum": ["github-actions", "jenkins", "gitlab-ci", "azure-devops", null] },
|
|
66
|
+
"workflows": { "type": "array", "items": { "type": "string" } }
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"apiStylesDetail": { "type": "object" },
|
|
70
|
+
"conventions": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"required": ["testDir", "specGlob", "configFiles"],
|
|
73
|
+
"properties": {
|
|
74
|
+
"testDir": { "type": ["string", "null"] },
|
|
75
|
+
"specGlob": { "type": ["string", "null"] },
|
|
76
|
+
"configFiles": { "type": "array", "items": { "type": "string" } }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"existingAutomation": { "type": "boolean" },
|
|
80
|
+
"confidence": { "type": "string", "enum": ["high", "medium", "low"] }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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"
|