qaas-python 0.0.1__py3-none-any.whl

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 (96) hide show
  1. qaas/adapters/__init__.py +19 -0
  2. qaas/adapters/tracker.py +1783 -0
  3. qaas/adapters/vcs.py +555 -0
  4. qaas/cli.py +1757 -0
  5. qaas/config.py +409 -0
  6. qaas/defaults/config/agents/api.yaml +18 -0
  7. qaas/defaults/config/agents/architect.yaml +21 -0
  8. qaas/defaults/config/agents/auditor.yaml +19 -0
  9. qaas/defaults/config/agents/browser.yaml +15 -0
  10. qaas/defaults/config/agents/dba.yaml +20 -0
  11. qaas/defaults/config/agents/fixer.yaml +55 -0
  12. qaas/defaults/config/agents/guide.yaml +23 -0
  13. qaas/defaults/config/agents/load.yaml +26 -0
  14. qaas/defaults/config/agents/mapper.yaml +19 -0
  15. qaas/defaults/config/agents/reporter.yaml +19 -0
  16. qaas/defaults/config/agents/reproducer.yaml +21 -0
  17. qaas/defaults/config/agents/reviewer.yaml +18 -0
  18. qaas/defaults/config/agents/socket.yaml +23 -0
  19. qaas/defaults/config/agents/triage.yaml +20 -0
  20. qaas/defaults/config/agents/verifier.yaml +20 -0
  21. qaas/defaults/config/system.yaml +64 -0
  22. qaas/discover.py +242 -0
  23. qaas/envelope.py +318 -0
  24. qaas/envfile.py +100 -0
  25. qaas/guardrails.py +589 -0
  26. qaas/mcp/__init__.py +0 -0
  27. qaas/mcp/context.py +78 -0
  28. qaas/mcp/contract_diff.py +1011 -0
  29. qaas/mcp/defect_memory.py +495 -0
  30. qaas/mcp/env_control.py +925 -0
  31. qaas/mcp/envelope_server.py +463 -0
  32. qaas/mcp/test_runner.py +842 -0
  33. qaas/mcp/tracker.py +420 -0
  34. qaas/mcp/vcs.py +501 -0
  35. qaas/paths.py +317 -0
  36. qaas/plugin/.claude-plugin/plugin.json +9 -0
  37. qaas/plugin/skills/a11y-audit/SKILL.md +34 -0
  38. qaas/plugin/skills/adversarial-review/SKILL.md +120 -0
  39. qaas/plugin/skills/api-surface-extraction/SKILL.md +38 -0
  40. qaas/plugin/skills/authz-matrix-check/SKILL.md +46 -0
  41. qaas/plugin/skills/console-error-triage/SKILL.md +39 -0
  42. qaas/plugin/skills/contract-test-generation/SKILL.md +36 -0
  43. qaas/plugin/skills/dedupe-strategy/SKILL.md +39 -0
  44. qaas/plugin/skills/environment-pinning/SKILL.md +35 -0
  45. qaas/plugin/skills/error-taxonomy/SKILL.md +42 -0
  46. qaas/plugin/skills/exploratory-ui-walk/SKILL.md +46 -0
  47. qaas/plugin/skills/failing-test-authoring/SKILL.md +47 -0
  48. qaas/plugin/skills/flake-detection/SKILL.md +39 -0
  49. qaas/plugin/skills/form-state-probe/SKILL.md +36 -0
  50. qaas/plugin/skills/minimal-diff-discipline/SKILL.md +70 -0
  51. qaas/plugin/skills/openapi-diff/SKILL.md +45 -0
  52. qaas/plugin/skills/ownership-resolution/SKILL.md +31 -0
  53. qaas/plugin/skills/product-task-graph/SKILL.md +35 -0
  54. qaas/plugin/skills/regression-risk-scoring/SKILL.md +59 -0
  55. qaas/plugin/skills/regression-suite-selection/SKILL.md +36 -0
  56. qaas/plugin/skills/repo-cartography/SKILL.md +38 -0
  57. qaas/plugin/skills/repro-minimisation/SKILL.md +41 -0
  58. qaas/plugin/skills/rollback-plan-authoring/SKILL.md +81 -0
  59. qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +67 -0
  60. qaas/plugin/skills/routing-rules/SKILL.md +34 -0
  61. qaas/plugin/skills/severity-rubric/SKILL.md +42 -0
  62. qaas/plugin/skills/test-first-fix/SKILL.md +66 -0
  63. qaas/plugin/skills/test-quality-audit/SKILL.md +58 -0
  64. qaas/plugin/skills/ticket-writer/SKILL.md +40 -0
  65. qaas/plugin/skills/verdict-reporting/SKILL.md +35 -0
  66. qaas/plugin/skills/verification-protocol/SKILL.md +39 -0
  67. qaas/prompts/API.md +44 -0
  68. qaas/prompts/ARCHITECT.md +80 -0
  69. qaas/prompts/AUDITOR.md +62 -0
  70. qaas/prompts/BROWSER.md +46 -0
  71. qaas/prompts/DBA.md +59 -0
  72. qaas/prompts/FIXER.md +55 -0
  73. qaas/prompts/GUIDE.md +94 -0
  74. qaas/prompts/LOAD.md +109 -0
  75. qaas/prompts/MAPPER.md +46 -0
  76. qaas/prompts/REPORTER.md +61 -0
  77. qaas/prompts/REPRODUCER.md +43 -0
  78. qaas/prompts/REVIEWER.md +53 -0
  79. qaas/prompts/SOCKET.md +100 -0
  80. qaas/prompts/TRIAGE.md +45 -0
  81. qaas/prompts/VERIFIER.md +41 -0
  82. qaas/prompts/_shared.md +45 -0
  83. qaas/registry.py +496 -0
  84. qaas/router.py +581 -0
  85. qaas/runner.py +210 -0
  86. qaas/scorecard.py +448 -0
  87. qaas/sdk_compat.py +52 -0
  88. qaas/store.py +323 -0
  89. qaas/target.py +287 -0
  90. qaas/tasks.py +438 -0
  91. qaas/trace.py +342 -0
  92. qaas_python-0.0.1.dist-info/METADATA +429 -0
  93. qaas_python-0.0.1.dist-info/RECORD +96 -0
  94. qaas_python-0.0.1.dist-info/WHEEL +4 -0
  95. qaas_python-0.0.1.dist-info/entry_points.txt +2 -0
  96. qaas_python-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,18 @@
1
+ name: REVIEWER
2
+ layer: remediation
3
+ role: >
4
+ Review and risk gate. Reads FIXER's diff as an adversarial reviewer and judges
5
+ correctness, scope creep, hidden regressions and risk. Separate from FIXER for
6
+ the same reason discovery is separate from triage: a model reviewing its own
7
+ diff in the same context reliably rationalises it.
8
+ prompt: REVIEWER.md
9
+ model: claude-opus-5
10
+ effort: high
11
+ max_turns: 50
12
+ mcp_servers: [envelope, vcs, contract_diff, test_runner]
13
+ builtin_tools: [Read, Grep, Glob]
14
+ skills: [adversarial-review, root-cause-vs-symptom, regression-risk-scoring, test-quality-audit]
15
+
16
+ policy: {} # no write access to code, ever — the whole point of the separation
17
+
18
+ must_call: [mcp__envelope__record_review]
@@ -0,0 +1,23 @@
1
+ name: SOCKET
2
+ layer: discovery
3
+ role: >
4
+ Realtime and WebSocket analyst. Finds auth bypass on the upgrade handshake,
5
+ reconnect without jittered backoff, message loss with no resume token,
6
+ order-dependent consumers with nothing carrying order, missing heartbeats,
7
+ absent backpressure, and channel authorization never re-checked after
8
+ subscribe. The WebSocket harness the design gives this role does not exist
9
+ here, so SOCKET reads the connection code and reports at the confidence of a
10
+ source reading, not of a measurement.
11
+ prompt: SOCKET.md
12
+ model: claude-opus-5
13
+ effort: high
14
+ max_turns: 60
15
+ mcp_servers: [envelope, env_control, defect_memory]
16
+ builtin_tools: [Read, Grep, Glob]
17
+ policy: {} # read-only
18
+
19
+ skills: [api-surface-extraction, authz-matrix-check, environment-pinning, severity-rubric]
20
+
21
+ # No must_call: see DBA. It matters more here than anywhere -- a target with no
22
+ # realtime surface should produce zero envelopes, and a required emission would
23
+ # turn "there are no websockets" into a manufactured finding about one.
@@ -0,0 +1,20 @@
1
+ name: TRIAGE
2
+ layer: triage
3
+ role: >
4
+ Triage and ticket scribe. The only agent with tracker write access. Dedupes,
5
+ scores severity against the rubric, resolves ownership from the system map,
6
+ routes by class, and files.
7
+ prompt: TRIAGE.md
8
+ model: claude-sonnet-5 # composition against a fixed rubric and house format
9
+ effort: high
10
+ max_turns: 50
11
+ mcp_servers: [envelope, tracker, defect_memory]
12
+ builtin_tools: [Read]
13
+ policy:
14
+ may_create_tickets: true
15
+ max_tickets_per_run: 10 # §4.12: over cap, pause and escalate
16
+
17
+ skills: [severity-rubric, dedupe-strategy, ticket-writer, routing-rules, ownership-resolution]
18
+
19
+ # Filing nothing is allowed; filing without checking for duplicates is not.
20
+ must_call: [mcp__defect_memory__search_similar]
@@ -0,0 +1,20 @@
1
+ name: VERIFIER
2
+ layer: remediation
3
+ role: >
4
+ Verification and regression gate, and the closing authority. Re-runs the
5
+ original failing test against the patched build, runs the regression suite for
6
+ affected areas, and returns VERIFIED, NOT_FIXED or REGRESSED.
7
+ prompt: VERIFIER.md
8
+ model: claude-opus-5
9
+ effort: high
10
+ max_turns: 60
11
+ mcp_servers: [envelope, test_runner, env_control, tracker, vcs]
12
+ builtin_tools: [Read, Grep, Glob, Bash]
13
+ policy:
14
+ may_transition_tickets: true # transition only, never create (§8.1)
15
+
16
+ skills: [verification-protocol, regression-suite-selection, verdict-reporting, severity-rubric]
17
+
18
+ # A verification that ends without transitioning the ticket has left the ticket
19
+ # in review forever, which is the one outcome worse than a wrong verdict.
20
+ must_call: [mcp__envelope__record_verdict, mcp__tracker__transition]
@@ -0,0 +1,64 @@
1
+ # System configuration. Run modes follow architecture §9; the Phase 1 roster is
2
+ # the six agents from §11, so modes name only those until Phase 2 lands.
3
+ project: qaas
4
+ # Which target profile to run against, from <config>/targets/<name>.yaml.
5
+ # Unset means "not pointed at anything yet", which is exactly what a fresh
6
+ # `pip install` is. `qaas init` writes this for you; QAAS_TARGET overrides it.
7
+ target:
8
+
9
+ tracker: local # local | jira -- swap to file against real Jira
10
+ vcs: local # local | github
11
+
12
+ # No spend cap by default. A dollar figure bakes one vendor's pricing into
13
+ # the config, and this is meant to run against local models too. `max_turns`
14
+ # is the model-agnostic bound. Set `max_budget_usd` here if you want a
15
+ # ceiling; the governor enforces one whenever it is present.
16
+
17
+ thresholds:
18
+ min_confidence_to_file: 0.6 # §7 confidence gate
19
+ max_findings_per_agent_run: 25 # §8.3 loop breaker: pause and escalate, don't file
20
+ max_tickets_per_run: 10 # §4.12 rate limit
21
+ flake_runs: 5 # REPRODUCER runs a repro N times to measure flake
22
+ max_mender_arbiter_round_trips: 2
23
+ max_proof_reopens: 1
24
+
25
+ run_modes:
26
+ pr-check:
27
+ trigger: pull_request
28
+ # ARCHITECT joins the PR sweep because it is pure static analysis and needs
29
+ # no running app. GUIDE, LOAD and REPORTER do not: the design says LOAD is
30
+ # "nightly and pre-release only -- too expensive and too noisy per-PR", and
31
+ # the same argument holds for a UX walk and a report about the week.
32
+ agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, REPRODUCER, TRIAGE]
33
+ max_wall_clock_s: 900
34
+ max_concurrency: 2
35
+
36
+ nightly:
37
+ trigger: cron
38
+ agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, SOCKET, GUIDE, LOAD, REPRODUCER, TRIAGE, REPORTER]
39
+ max_wall_clock_s: 7200
40
+ max_concurrency: 3
41
+
42
+ incident:
43
+ trigger: alert
44
+ agents: [API]
45
+ max_wall_clock_s: 600
46
+ max_concurrency: 2
47
+ files_tickets: false # §9: diagnostic only, read-only, no filing
48
+
49
+ # §9: the remediation loop. VERIFIER verifies first — a ticket whose defect no
50
+ # longer reproduces needs no fix, and finding that out costs one cheap run
51
+ # instead of a whole FIXER/REVIEWER cycle.
52
+ fix-cycle:
53
+ trigger: agent_ready_ticket
54
+ agents: [VERIFIER, FIXER, REVIEWER]
55
+ max_wall_clock_s: 3600
56
+ max_concurrency: 1
57
+
58
+ # Everything, end to end: discover, triage, file, fix, review, verify. The most
59
+ # expensive mode in the system and the only one that closes the loop.
60
+ full-loop:
61
+ trigger: on_demand
62
+ agents: [MAPPER, ARCHITECT, API, BROWSER, DBA, AUDITOR, SOCKET, GUIDE, LOAD, REPRODUCER, TRIAGE, FIXER, REVIEWER, VERIFIER, REPORTER]
63
+ max_wall_clock_s: 10800
64
+ max_concurrency: 3
qaas/discover.py ADDED
@@ -0,0 +1,242 @@
1
+ """Inspecting an unfamiliar repository well enough to write a target profile.
2
+
3
+ This is deliberately dumb pattern-matching, not analysis. Its job is to save a
4
+ person ten minutes of typing and to be obviously wrong when it is wrong — every
5
+ value it produces is a guess a human is expected to correct, and `qaas init`
6
+ says so. MAPPER does the real mapping later, with a model and the whole
7
+ repository in front of it.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+
17
+ from qaas.target import DEFAULT_EXCLUDES, Auth, Environment, Layout, Role, TargetProfile
18
+
19
+ SPEC_NAMES = [
20
+ "openapi.yaml", "openapi.yml", "openapi.json",
21
+ "swagger.yaml", "swagger.yml", "swagger.json",
22
+ "api/openapi.yaml", "docs/openapi.yaml", "spec/openapi.yaml",
23
+ ]
24
+ OWNERSHIP_NAMES = ["CODEOWNERS", ".github/CODEOWNERS", "docs/CODEOWNERS", ".gitlab/CODEOWNERS"]
25
+ COMPOSE_NAMES = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]
26
+
27
+ BACKEND_MARKERS = {
28
+ "python": ("requirements.txt", "pyproject.toml", "manage.py", "Pipfile"),
29
+ "node": ("package.json",),
30
+ "go": ("go.mod",),
31
+ "java": ("pom.xml", "build.gradle", "build.gradle.kts"),
32
+ "ruby": ("Gemfile",),
33
+ "rust": ("Cargo.toml",),
34
+ "php": ("composer.json",),
35
+ }
36
+ BACKEND_HINTS = re.compile(
37
+ r"\b(fastapi|flask|django|express|nestjs|gin|echo|spring|rails|sinatra|laravel|actix|axum)\b",
38
+ re.I,
39
+ )
40
+ FRONTEND_HINTS = re.compile(r'"(react|vue|svelte|@angular/core|next|nuxt|solid-js)"', re.I)
41
+
42
+ TEST_DIR_NAMES = {"tests", "test", "__tests__", "spec", "e2e", "integration_tests"}
43
+ MIGRATION_DIR_NAMES = {"migrations", "migrate", "alembic", "db/migrate", "prisma/migrations"}
44
+
45
+ #: `Layout.docs` existed and nothing ever filled it, so a documentation-heavy
46
+ #: repository profiled as having no documentation at all and ARCHITECT was told
47
+ #: to go and find it. Prose is a real surface: specs that contradict the code,
48
+ #: tickets that describe behaviour nobody built, standards nothing follows.
49
+ DOC_DIR_NAMES = {"docs", "doc", "documentation", "adr", "rfcs", "specs"}
50
+
51
+
52
+ @dataclass
53
+ class Discovery:
54
+ """What inspection found, with a note on anything it could not settle."""
55
+
56
+ layout: Layout
57
+ environment: Environment
58
+ auth: Auth
59
+ languages: set[str] = field(default_factory=set)
60
+ notes: list[str] = field(default_factory=list)
61
+
62
+
63
+ def _rel(path: Path, root: Path) -> str:
64
+ return str(path.relative_to(root)).replace("\\", "/")
65
+
66
+
67
+ def _walk(root: Path, excludes: set[str], max_depth: int = 4):
68
+ """Directories worth looking at, breadth-first, skipping vendored trees."""
69
+ stack = [(root, 0)]
70
+ while stack:
71
+ current, depth = stack.pop()
72
+ if depth > max_depth:
73
+ continue
74
+ try:
75
+ children = list(current.iterdir())
76
+ except (PermissionError, OSError):
77
+ continue
78
+ yield current, children
79
+ for child in children:
80
+ if child.is_dir() and child.name not in excludes and not child.name.startswith("."):
81
+ stack.append((child, depth + 1))
82
+
83
+
84
+ def _has_component_source(path: Path, excludes: set[str]) -> bool:
85
+ """Whether a directory holds component source of its own.
86
+
87
+ Deliberately not `rglob`: that descends into `node_modules`, where a great
88
+ many packages ship .tsx, so it answers yes for almost any directory in a
89
+ JavaScript repository — and walks a vendored tree to get there. `_walk`
90
+ honours the same exclusions as the rest of this module.
91
+ """
92
+ return any(
93
+ c.suffix in {".tsx", ".jsx"}
94
+ for _, children in _walk(path, excludes)
95
+ for c in children
96
+ )
97
+
98
+
99
+ def inspect(root: Path) -> Discovery:
100
+ """Guess a repository's shape. Every field is a hint, not a conclusion."""
101
+ root = root.resolve()
102
+ excludes = set(DEFAULT_EXCLUDES)
103
+ notes: list[str] = []
104
+ languages: set[str] = set()
105
+
106
+ backend: list[str] = []
107
+ frontend: list[str] = []
108
+ tests: list[str] = []
109
+ migrations: list[str] = []
110
+ docs: list[str] = []
111
+
112
+ for directory, children in _walk(root, excludes):
113
+ names = {c.name for c in children}
114
+ rel = _rel(directory, root) if directory != root else "."
115
+
116
+ for language, markers in BACKEND_MARKERS.items():
117
+ if names & set(markers):
118
+ languages.add(language)
119
+ if language == "node" and (directory / "package.json").exists():
120
+ # package.json alone says nothing; the dependencies do.
121
+ try:
122
+ pkg = (directory / "package.json").read_text(encoding="utf-8")
123
+ except OSError:
124
+ pkg = ""
125
+ if FRONTEND_HINTS.search(pkg):
126
+ frontend.append(rel)
127
+ continue
128
+ if BACKEND_HINTS.search(pkg):
129
+ backend.append(rel)
130
+ continue
131
+ elif rel not in backend:
132
+ backend.append(rel)
133
+
134
+ if directory.name in TEST_DIR_NAMES and rel not in tests:
135
+ tests.append(rel)
136
+ if directory.name in MIGRATION_DIR_NAMES and rel not in migrations:
137
+ migrations.append(rel)
138
+ # Only the top of a documentation tree: `docs` and `docs/specs` and
139
+ # `docs/tickets` are one surface, and listing all three says nothing
140
+ # the first does not.
141
+ if directory.name in DOC_DIR_NAMES and not any(
142
+ rel == d or rel.startswith(f"{d}/") for d in docs
143
+ ):
144
+ docs.append(rel)
145
+
146
+ # A source directory with .tsx/.jsx in it is a frontend even without a
147
+ # package.json of its own — monorepos often hoist dependencies.
148
+ if not frontend:
149
+ for candidate in ("web", "frontend", "client", "ui", "app"):
150
+ path = root / candidate
151
+ if path.is_dir() and _has_component_source(path, excludes):
152
+ frontend.append(candidate)
153
+ break
154
+
155
+ spec = next((s for s in SPEC_NAMES if (root / s).exists()), None)
156
+ ownership = next((o for o in OWNERSHIP_NAMES if (root / o).exists()), None)
157
+ compose = next((c for c in COMPOSE_NAMES if (root / c).exists()), None)
158
+
159
+ if not spec:
160
+ notes.append(
161
+ "No OpenAPI document found. API can still audit the API, but it has "
162
+ "no declared contract to diff against — set layout.spec if one exists "
163
+ "somewhere this did not look."
164
+ )
165
+ if not ownership:
166
+ notes.append(
167
+ "No CODEOWNERS file. Tickets will be filed unassigned unless you add one "
168
+ "or set layout.ownership."
169
+ )
170
+ if not backend and not frontend:
171
+ notes.append(
172
+ "Could not identify backend or frontend directories. Set layout.backend "
173
+ "and layout.frontend by hand — leaving them empty makes MAPPER "
174
+ "explore blind, which is slower and less accurate."
175
+ )
176
+
177
+ environment = Environment(mode="none")
178
+ if compose:
179
+ notes.append(
180
+ f"Found {compose}. Environment mode is still 'none': review the compose "
181
+ "file, then set mode to 'compose' with the service names and URLs. This "
182
+ "is not switched on automatically because bringing up someone's stack "
183
+ "unasked is not a decision a tool should make."
184
+ )
185
+
186
+ # "." is not a service. A root pyproject.toml or package.json is usually
187
+ # tooling or workspace config, and listing the root as a backend directory
188
+ # tells MAPPER nothing while making it read the whole repository.
189
+ if len(set(backend)) > 1:
190
+ backend = [b for b in backend if b != "."]
191
+
192
+ return Discovery(
193
+ layout=Layout(
194
+ backend=sorted(set(backend))[:6],
195
+ frontend=sorted(set(frontend))[:4],
196
+ tests=sorted(set(tests))[:4],
197
+ migrations=sorted(set(migrations))[:3],
198
+ docs=sorted(set(docs))[:4],
199
+ spec=spec,
200
+ ownership=ownership,
201
+ exclude=list(DEFAULT_EXCLUDES),
202
+ ),
203
+ environment=environment,
204
+ auth=Auth(mode="none"),
205
+ languages=languages,
206
+ notes=notes,
207
+ )
208
+
209
+
210
+ def build_profile(
211
+ name: str,
212
+ root: Path,
213
+ *,
214
+ repo_url: str | None = None,
215
+ description: str = "",
216
+ default_branch: str = "main",
217
+ ) -> tuple[TargetProfile, list[str]]:
218
+ found = inspect(root)
219
+ profile = TargetProfile(
220
+ name=name,
221
+ root=str(root),
222
+ repo_url=repo_url,
223
+ description=description or _describe(root, found),
224
+ default_branch=default_branch,
225
+ layout=found.layout,
226
+ environment=found.environment,
227
+ auth=found.auth,
228
+ )
229
+ return profile, found.notes
230
+
231
+
232
+ def _describe(root: Path, found: Discovery) -> str:
233
+ langs = ", ".join(sorted(found.languages)) or "unknown stack"
234
+ readme = next((root / n for n in ("README.md", "readme.md", "README.rst") if (root / n).exists()), None)
235
+ first_line = ""
236
+ if readme:
237
+ for line in readme.read_text(errors="ignore").splitlines():
238
+ stripped = line.strip().lstrip("#").strip()
239
+ if stripped and not stripped.startswith(("!", "[", "<")):
240
+ first_line = stripped
241
+ break
242
+ return f"{first_line} ({langs})".strip() if first_line else f"A {langs} project."