qaas-python 0.1.0__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.
- qaas/adapters/__init__.py +19 -0
- qaas/adapters/tracker.py +1350 -0
- qaas/adapters/vcs.py +494 -0
- qaas/cli.py +1564 -0
- qaas/conductor.py +527 -0
- qaas/config.py +407 -0
- qaas/defaults/config/agents/arbiter.yaml +19 -0
- qaas/defaults/config/agents/cartographer.yaml +20 -0
- qaas/defaults/config/agents/clerk.yaml +21 -0
- qaas/defaults/config/agents/conduit.yaml +19 -0
- qaas/defaults/config/agents/forge.yaml +22 -0
- qaas/defaults/config/agents/mender.yaml +56 -0
- qaas/defaults/config/agents/proof.yaml +21 -0
- qaas/defaults/config/agents/surface.yaml +16 -0
- qaas/defaults/config/system.yaml +69 -0
- qaas/discover.py +227 -0
- qaas/envelope.py +290 -0
- qaas/guardrails.py +431 -0
- qaas/mcp/__init__.py +0 -0
- qaas/mcp/context.py +70 -0
- qaas/mcp/contract_diff.py +937 -0
- qaas/mcp/defect_memory.py +495 -0
- qaas/mcp/env_control.py +905 -0
- qaas/mcp/envelope_server.py +463 -0
- qaas/mcp/test_runner.py +773 -0
- qaas/mcp/tracker.py +412 -0
- qaas/mcp/vcs.py +506 -0
- qaas/paths.py +317 -0
- qaas/plugin/.claude-plugin/plugin.json +9 -0
- qaas/plugin/skills/a11y-audit/SKILL.md +34 -0
- qaas/plugin/skills/adversarial-review/SKILL.md +120 -0
- qaas/plugin/skills/api-surface-extraction/SKILL.md +38 -0
- qaas/plugin/skills/authz-matrix-check/SKILL.md +46 -0
- qaas/plugin/skills/console-error-triage/SKILL.md +39 -0
- qaas/plugin/skills/contract-test-generation/SKILL.md +36 -0
- qaas/plugin/skills/dedupe-strategy/SKILL.md +39 -0
- qaas/plugin/skills/environment-pinning/SKILL.md +35 -0
- qaas/plugin/skills/error-taxonomy/SKILL.md +42 -0
- qaas/plugin/skills/exploratory-ui-walk/SKILL.md +46 -0
- qaas/plugin/skills/failing-test-authoring/SKILL.md +47 -0
- qaas/plugin/skills/flake-detection/SKILL.md +39 -0
- qaas/plugin/skills/form-state-probe/SKILL.md +36 -0
- qaas/plugin/skills/minimal-diff-discipline/SKILL.md +70 -0
- qaas/plugin/skills/openapi-diff/SKILL.md +45 -0
- qaas/plugin/skills/ownership-resolution/SKILL.md +31 -0
- qaas/plugin/skills/product-task-graph/SKILL.md +35 -0
- qaas/plugin/skills/regression-risk-scoring/SKILL.md +59 -0
- qaas/plugin/skills/regression-suite-selection/SKILL.md +36 -0
- qaas/plugin/skills/repo-cartography/SKILL.md +38 -0
- qaas/plugin/skills/repro-minimisation/SKILL.md +41 -0
- qaas/plugin/skills/rollback-plan-authoring/SKILL.md +81 -0
- qaas/plugin/skills/root-cause-vs-symptom/SKILL.md +67 -0
- qaas/plugin/skills/routing-rules/SKILL.md +34 -0
- qaas/plugin/skills/severity-rubric/SKILL.md +42 -0
- qaas/plugin/skills/test-first-fix/SKILL.md +66 -0
- qaas/plugin/skills/test-quality-audit/SKILL.md +58 -0
- qaas/plugin/skills/ticket-writer/SKILL.md +40 -0
- qaas/plugin/skills/verdict-reporting/SKILL.md +35 -0
- qaas/plugin/skills/verification-protocol/SKILL.md +39 -0
- qaas/prompts/ARBITER.md +53 -0
- qaas/prompts/CARTOGRAPHER.md +46 -0
- qaas/prompts/CLERK.md +45 -0
- qaas/prompts/CONDUIT.md +44 -0
- qaas/prompts/FORGE.md +43 -0
- qaas/prompts/MENDER.md +55 -0
- qaas/prompts/PROOF.md +41 -0
- qaas/prompts/SURFACE.md +46 -0
- qaas/prompts/_shared.md +45 -0
- qaas/registry.py +465 -0
- qaas/runner.py +192 -0
- qaas/scorecard.py +425 -0
- qaas/sdk_compat.py +52 -0
- qaas/store.py +290 -0
- qaas/target.py +261 -0
- qaas/tasks.py +361 -0
- qaas/trace.py +270 -0
- qaas_python-0.1.0.dist-info/METADATA +388 -0
- qaas_python-0.1.0.dist-info/RECORD +81 -0
- qaas_python-0.1.0.dist-info/WHEEL +4 -0
- qaas_python-0.1.0.dist-info/entry_points.txt +2 -0
- qaas_python-0.1.0.dist-info/licenses/LICENSE +21 -0
qaas/paths.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Where qaas finds its own resources: config, prompts, skills, state.
|
|
2
|
+
|
|
3
|
+
This module exists because the package used to assume it was running from its
|
|
4
|
+
own git checkout. `config/` and `.claude/skills/` sat at the repo root, outside
|
|
5
|
+
the wheel, and were looked up relative to the process CWD or by climbing
|
|
6
|
+
`Path(__file__).parents[2]` -- which, once installed, lands in
|
|
7
|
+
`site-packages/../..`. A `pip install` therefore produced a CLI where every
|
|
8
|
+
command that needed config died, and `qaas validate` failed *always*, because
|
|
9
|
+
none of the 30 skills it checks for were anywhere on disk.
|
|
10
|
+
|
|
11
|
+
Three different ideas had been collapsed into `Path.cwd()`:
|
|
12
|
+
|
|
13
|
+
1. where qaas's own resources live -> packaged, or overridden by the user
|
|
14
|
+
2. where the user's project state lives -> `.qaas/`
|
|
15
|
+
3. where the application under test is -> the target profile's root
|
|
16
|
+
|
|
17
|
+
This module owns the first two. The third belongs to `TargetProfile.root_path`.
|
|
18
|
+
|
|
19
|
+
The precedence is the same for every kind of resource, and it is the ordinary
|
|
20
|
+
one: an explicit flag beats the project, and the project beats what we shipped.
|
|
21
|
+
|
|
22
|
+
1. explicit --config / QAAS_CONFIG_DIR
|
|
23
|
+
2. project <project>/.qaas/config, and the source-checkout <project>/config
|
|
24
|
+
3. packaged src/qaas/defaults/config, src/qaas/prompts, src/qaas/skills
|
|
25
|
+
|
|
26
|
+
Layering granularity differs by kind, and that difference is deliberate:
|
|
27
|
+
|
|
28
|
+
* `system.yaml` first hit wins **whole**. Merging run-mode dictionaries
|
|
29
|
+
across layers produces a configuration nobody wrote and
|
|
30
|
+
nobody can read back.
|
|
31
|
+
* `agents/*.yaml` union by filename, higher layer shadows. Someone who wants
|
|
32
|
+
MENDER's budget raised drops in one file; they do not fork
|
|
33
|
+
eight and freeze themselves on today's roster.
|
|
34
|
+
* prompts/skills union by name, higher layer shadows, same reasoning.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
from typing import Iterable
|
|
43
|
+
|
|
44
|
+
#: Overrides, in the order a reader will look for them.
|
|
45
|
+
CONFIG_DIR_ENV = "QAAS_CONFIG_DIR"
|
|
46
|
+
HOME_ENV = "QAAS_HOME"
|
|
47
|
+
|
|
48
|
+
#: The state directory, and the config dir inside it. `.qaas/` holds both a
|
|
49
|
+
#: user's committed config and their disposable run state, so `init` writes a
|
|
50
|
+
#: `.gitignore` inside it -- otherwise the obvious `.gitignore` line for `.qaas/`
|
|
51
|
+
#: would drop their configuration too.
|
|
52
|
+
STATE_DIRNAME = ".qaas"
|
|
53
|
+
PROJECT_CONFIG = "config"
|
|
54
|
+
|
|
55
|
+
#: How far up to look for a project before giving up and using packaged defaults.
|
|
56
|
+
MAX_WALK_UP = 24
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def package_root() -> Path:
|
|
60
|
+
"""The installed package directory -- the one `__file__` seam in the codebase.
|
|
61
|
+
|
|
62
|
+
`Path(__file__).parent` rather than `importlib.resources.files("qaas")`,
|
|
63
|
+
which was tried first and is wrong here: under a src-layout editable install
|
|
64
|
+
the package resolves to a `MultiplexedPath`, and `Path(str(...))` on one of
|
|
65
|
+
those yields the literal string `MultiplexedPath('/...')` -- a path that
|
|
66
|
+
exists nowhere. Every resource lookup then silently found nothing, which is
|
|
67
|
+
the same failure shape as the bug this module was written to fix.
|
|
68
|
+
|
|
69
|
+
This file lives inside the package, so its parent *is* the package, in an
|
|
70
|
+
editable install and a wheel alike. Zip-safety is not a consideration: the
|
|
71
|
+
skills directory is handed to a subprocess as a real filesystem path.
|
|
72
|
+
"""
|
|
73
|
+
return Path(__file__).resolve().parent
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def packaged_config() -> Path:
|
|
77
|
+
return package_root() / "defaults" / "config"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def packaged_prompts() -> Path:
|
|
81
|
+
return package_root() / "prompts"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def packaged_plugin() -> Path:
|
|
85
|
+
"""The skills plugin that ships in the wheel.
|
|
86
|
+
|
|
87
|
+
Skills reach an agent as a Claude Code *plugin* (`--plugin-dir`), not through
|
|
88
|
+
filesystem settings, so they travel in the package instead of depending on a
|
|
89
|
+
`.claude/skills/` directory in whatever repository the user happens to be in.
|
|
90
|
+
|
|
91
|
+
The layout is not optional and was established by testing the CLI rather
|
|
92
|
+
than by reading about it. A directory of bare `<skill>/SKILL.md` folders
|
|
93
|
+
loads NOTHING -- silently. A directory containing `skills/<name>/SKILL.md`
|
|
94
|
+
loads, but takes its namespace from the directory name. Only
|
|
95
|
+
`.claude-plugin/plugin.json` + `skills/<name>/SKILL.md` gives a stable
|
|
96
|
+
namespace, and it comes from the manifest's `name`.
|
|
97
|
+
"""
|
|
98
|
+
return package_root() / "plugin"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def plugin_name(plugin_dir: Path) -> str:
|
|
102
|
+
"""A plugin's namespace: its manifest name, else its directory name.
|
|
103
|
+
|
|
104
|
+
Skills load as `<plugin>:<skill>`, and that prefix is not cosmetic -- the
|
|
105
|
+
SDK matches skill names down two channels with different rules, so the name
|
|
106
|
+
used here has to be the one the CLI will actually register.
|
|
107
|
+
"""
|
|
108
|
+
manifest = plugin_dir / ".claude-plugin" / "plugin.json"
|
|
109
|
+
if manifest.is_file():
|
|
110
|
+
try:
|
|
111
|
+
import json
|
|
112
|
+
|
|
113
|
+
declared = json.loads(manifest.read_text()).get("name")
|
|
114
|
+
if declared:
|
|
115
|
+
return str(declared)
|
|
116
|
+
except Exception: # noqa: BLE001 - a broken manifest falls back, not crashes
|
|
117
|
+
pass
|
|
118
|
+
return plugin_dir.name
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def packaged_skills() -> Path:
|
|
122
|
+
"""The skills themselves, inside the plugin."""
|
|
123
|
+
return packaged_plugin() / "skills"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find_project(start: Path | None = None) -> Path | None:
|
|
127
|
+
"""Walk up looking for a qaas project. Returns None if there is not one.
|
|
128
|
+
|
|
129
|
+
Three shapes count:
|
|
130
|
+
|
|
131
|
+
* `<dir>/.qaas/config/` what `qaas init` writes
|
|
132
|
+
* `<dir>/config/system.yaml` a user who keeps config at the top level
|
|
133
|
+
* `<dir>/config/targets/` this repository, whose `system.yaml` now
|
|
134
|
+
ships inside the package and whose `config/`
|
|
135
|
+
holds only the bundled demo profile
|
|
136
|
+
|
|
137
|
+
The third looks incidental and is not: without it, developing qaas in its
|
|
138
|
+
own checkout stops finding the demo target the moment the defaults move
|
|
139
|
+
into the wheel.
|
|
140
|
+
"""
|
|
141
|
+
here = (start or Path.cwd()).resolve()
|
|
142
|
+
for parent in [here, *here.parents][:MAX_WALK_UP]:
|
|
143
|
+
if (parent / STATE_DIRNAME / PROJECT_CONFIG).is_dir():
|
|
144
|
+
return parent
|
|
145
|
+
if (parent / PROJECT_CONFIG / "system.yaml").is_file():
|
|
146
|
+
return parent
|
|
147
|
+
if (parent / PROJECT_CONFIG / "targets").is_dir():
|
|
148
|
+
return parent
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def project_root(start: Path | None = None) -> Path:
|
|
153
|
+
"""The qaas project directory, falling back to the cwd when there is none.
|
|
154
|
+
|
|
155
|
+
This is the base a *relative* target root resolves against. It has to be the
|
|
156
|
+
project rather than the cwd: a profile says `root: target-app` because that
|
|
157
|
+
is where the application sits relative to the project that configures it,
|
|
158
|
+
and running `qaas run` from a subdirectory must not silently point the whole
|
|
159
|
+
system at a directory that does not exist.
|
|
160
|
+
"""
|
|
161
|
+
here = (start or Path.cwd()).resolve()
|
|
162
|
+
return find_project(here) or here
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _existing(*candidates: Path | None) -> tuple[Path, ...]:
|
|
166
|
+
"""Keep the directories that exist, in order, without duplicates."""
|
|
167
|
+
seen: list[Path] = []
|
|
168
|
+
for c in candidates:
|
|
169
|
+
if c is None:
|
|
170
|
+
continue
|
|
171
|
+
r = c.resolve()
|
|
172
|
+
if r.is_dir() and r not in seen:
|
|
173
|
+
seen.append(r)
|
|
174
|
+
return tuple(seen)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class Workspace:
|
|
179
|
+
"""Every directory qaas reads from, resolved once and passed down.
|
|
180
|
+
|
|
181
|
+
Frozen because half the codebase reads these; a value that can be mutated
|
|
182
|
+
mid-run is a value that will be.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
project: Path | None
|
|
186
|
+
config_dirs: tuple[Path, ...]
|
|
187
|
+
prompt_dirs: tuple[Path, ...]
|
|
188
|
+
#: Plugin roots, as handed to `--plugin-dir`.
|
|
189
|
+
plugin_dirs: tuple[Path, ...]
|
|
190
|
+
#: The `skills/` inside each plugin, in the same order.
|
|
191
|
+
skill_dirs: tuple[Path, ...]
|
|
192
|
+
state_root: Path
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def resolve(
|
|
196
|
+
cls,
|
|
197
|
+
*,
|
|
198
|
+
config: Path | str | None = None,
|
|
199
|
+
state_root: Path | str | None = None,
|
|
200
|
+
cwd: Path | None = None,
|
|
201
|
+
) -> "Workspace":
|
|
202
|
+
"""Build the search paths. Never raises: a missing project is legitimate.
|
|
203
|
+
|
|
204
|
+
`pip install qaas-python` then `qaas --help` in an empty directory is a
|
|
205
|
+
supported state, and it resolves to packaged defaults only.
|
|
206
|
+
"""
|
|
207
|
+
here = (cwd or Path.cwd()).resolve()
|
|
208
|
+
project = find_project(here)
|
|
209
|
+
|
|
210
|
+
explicit = config or os.environ.get(CONFIG_DIR_ENV) or None
|
|
211
|
+
explicit_path = Path(explicit).resolve() if explicit else None
|
|
212
|
+
|
|
213
|
+
home = os.environ.get(HOME_ENV)
|
|
214
|
+
home_path = Path(home).resolve() if home else None
|
|
215
|
+
|
|
216
|
+
proj_state = (project / STATE_DIRNAME) if project else None
|
|
217
|
+
|
|
218
|
+
config_dirs = _existing(
|
|
219
|
+
explicit_path,
|
|
220
|
+
home_path / PROJECT_CONFIG if home_path else None,
|
|
221
|
+
proj_state / PROJECT_CONFIG if proj_state else None,
|
|
222
|
+
project / PROJECT_CONFIG if project else None,
|
|
223
|
+
packaged_config(),
|
|
224
|
+
)
|
|
225
|
+
prompt_dirs = _existing(
|
|
226
|
+
home_path / "prompts" if home_path else None,
|
|
227
|
+
proj_state / "prompts" if proj_state else None,
|
|
228
|
+
packaged_prompts(),
|
|
229
|
+
)
|
|
230
|
+
# Plugin directories, highest precedence first. A project's own plugin
|
|
231
|
+
# shadows the packaged one for any skill it provides.
|
|
232
|
+
plugin_dirs = _existing(
|
|
233
|
+
home_path / "plugin" if home_path else None,
|
|
234
|
+
proj_state / "plugin" if proj_state else None,
|
|
235
|
+
packaged_plugin(),
|
|
236
|
+
)
|
|
237
|
+
skill_dirs = _existing(*(d / "skills" for d in plugin_dirs))
|
|
238
|
+
|
|
239
|
+
if state_root is not None:
|
|
240
|
+
state = Path(state_root).resolve()
|
|
241
|
+
elif proj_state is not None:
|
|
242
|
+
state = proj_state
|
|
243
|
+
else:
|
|
244
|
+
state = (here / STATE_DIRNAME).resolve()
|
|
245
|
+
|
|
246
|
+
return cls(
|
|
247
|
+
project=project,
|
|
248
|
+
config_dirs=config_dirs,
|
|
249
|
+
prompt_dirs=prompt_dirs,
|
|
250
|
+
plugin_dirs=plugin_dirs,
|
|
251
|
+
skill_dirs=skill_dirs,
|
|
252
|
+
state_root=state,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# -- lookups ----------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def find_file(self, dirs: Iterable[Path], relative: str) -> Path | None:
|
|
258
|
+
"""First hit wins. Used for whole-file resources like `system.yaml`."""
|
|
259
|
+
for d in dirs:
|
|
260
|
+
candidate = d / relative
|
|
261
|
+
if candidate.is_file():
|
|
262
|
+
return candidate
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
def config_file(self, relative: str) -> Path | None:
|
|
266
|
+
return self.find_file(self.config_dirs, relative)
|
|
267
|
+
|
|
268
|
+
def prompt_file(self, relative: str) -> Path | None:
|
|
269
|
+
return self.find_file(self.prompt_dirs, relative)
|
|
270
|
+
|
|
271
|
+
def merged(self, dirs: Iterable[Path], subdir: str, pattern: str) -> dict[str, Path]:
|
|
272
|
+
"""Union by filename, earlier layers shadowing later ones.
|
|
273
|
+
|
|
274
|
+
For `agents/*.yaml`, `targets/*.yaml` and skills: a user shadows the one
|
|
275
|
+
file they care about and keeps receiving improvements to the rest.
|
|
276
|
+
Reversed iteration so the highest-precedence directory writes last.
|
|
277
|
+
"""
|
|
278
|
+
found: dict[str, Path] = {}
|
|
279
|
+
for d in reversed(list(dirs)):
|
|
280
|
+
base = d / subdir if subdir else d
|
|
281
|
+
if not base.is_dir():
|
|
282
|
+
continue
|
|
283
|
+
for path in sorted(base.glob(pattern)):
|
|
284
|
+
found[path.stem] = path
|
|
285
|
+
return found
|
|
286
|
+
|
|
287
|
+
def skill_names(self) -> dict[str, Path]:
|
|
288
|
+
"""Every skill directory visible, by name, nearest layer winning."""
|
|
289
|
+
found: dict[str, Path] = {}
|
|
290
|
+
for d in reversed(list(self.skill_dirs)):
|
|
291
|
+
if not d.is_dir():
|
|
292
|
+
continue
|
|
293
|
+
for skill in sorted(d.glob("*/SKILL.md")):
|
|
294
|
+
found[skill.parent.name] = skill.parent
|
|
295
|
+
return found
|
|
296
|
+
|
|
297
|
+
def qualify(self, skill: str) -> str | None:
|
|
298
|
+
"""`severity-rubric` -> `qaas:severity-rubric`, from whichever plugin
|
|
299
|
+
provides it. None when nothing does -- which is a configuration error
|
|
300
|
+
worth reporting, not a name to pass on and hope."""
|
|
301
|
+
for plugin_dir in self.plugin_dirs:
|
|
302
|
+
if (plugin_dir / "skills" / skill / "SKILL.md").is_file():
|
|
303
|
+
return f"{plugin_name(plugin_dir)}:{skill}"
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
def describe(self) -> str:
|
|
307
|
+
"""One line per search path, for `qaas doctor` and error messages."""
|
|
308
|
+
where = str(self.project) if self.project else "(none -- packaged defaults only)"
|
|
309
|
+
lines = [f"project: {where}", f"state: {self.state_root}"]
|
|
310
|
+
for label, dirs in (
|
|
311
|
+
("config", self.config_dirs),
|
|
312
|
+
("prompts", self.prompt_dirs),
|
|
313
|
+
("skills", self.skill_dirs),
|
|
314
|
+
):
|
|
315
|
+
for i, d in enumerate(dirs):
|
|
316
|
+
lines.append(f"{label + ':':9} {'*' if i == 0 else ' '} {d}")
|
|
317
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: a11y-audit
|
|
3
|
+
description: >
|
|
4
|
+
Check a rendered page against WCAG for failures that block or exclude users.
|
|
5
|
+
TRIGGER - read BEFORE reporting any accessibility issue, and whenever the task
|
|
6
|
+
mentions accessibility, a11y, WCAG, screen readers, contrast, keyboard
|
|
7
|
+
navigation, focus, or ARIA. Do NOT report an accessibility finding without
|
|
8
|
+
naming the specific success criterion it violates. SKIP only when no rendered
|
|
9
|
+
UI is in scope.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Auditing accessibility
|
|
13
|
+
|
|
14
|
+
These are real defects, scored on the normal severity scale. Major when they block someone from completing a task; minor when they inconvenience. Never dismissed as cosmetic, and never inflated past what the rubric supports.
|
|
15
|
+
|
|
16
|
+
## What to check, in value order
|
|
17
|
+
|
|
18
|
+
1. **Every input has an accessible name** (WCAG 1.3.1, 4.1.2). A `<label for>`, an `aria-label`, or `aria-labelledby`. **A placeholder is not a label** — it vanishes on focus and many screen readers skip it. An unlabelled search box is announced as "edit text, blank".
|
|
19
|
+
2. **Contrast** (1.4.3). 4.5:1 for body text, 3:1 for large text and UI component boundaries. Compute the ratio and state it; "looks light" is not a finding.
|
|
20
|
+
3. **Keyboard reachability** (2.1.1, 2.1.2). Tab through the whole task. Everything clickable must be reachable and operable, and you must be able to get back out of every widget. A focus trap is major — it strands the user with no recovery.
|
|
21
|
+
4. **Visible focus** (2.4.7). If focus is invisible, keyboard navigation is guesswork.
|
|
22
|
+
5. **Meaningful order** (1.3.2, 2.4.3). Tab order should follow the visual order.
|
|
23
|
+
6. **Status messages announced** (4.1.3). An error shown only in red text, with no `role="alert"` or `aria-live`, does not exist for a screen reader user — they submit and hear nothing.
|
|
24
|
+
7. **Images and icon buttons** (1.1.1). Decorative images `alt=""`; meaningful ones described. An icon-only button with no accessible name is unusable.
|
|
25
|
+
|
|
26
|
+
## Name the criterion
|
|
27
|
+
|
|
28
|
+
Every finding states the number and what a user experiences: "1.3.1 — the orders search input has only a placeholder, so screen reader users hear an unlabelled text field and cannot tell what it filters."
|
|
29
|
+
|
|
30
|
+
The number makes it checkable. The consequence makes it worth fixing. A finding with only the number reads as compliance box-ticking; a finding with only the consequence is hard to verify.
|
|
31
|
+
|
|
32
|
+
## Precision
|
|
33
|
+
|
|
34
|
+
Check the specific element pair you are reporting, not the theme generally. A design where one button variant fails contrast and the rest pass is one finding, not a sweeping claim about the palette — and a sweeping claim will be dismissed along with the real instance inside it.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: adversarial-review
|
|
3
|
+
description: >
|
|
4
|
+
Review a fix as an adversary in a fixed order, reading for what the diff is
|
|
5
|
+
missing rather than what it contains. TRIGGER - read BEFORE looking at any
|
|
6
|
+
diff you are asked to judge and BEFORE recording any review decision, and
|
|
7
|
+
whenever the task mentions reviewing a fix, APPROVE, REQUEST_CHANGES,
|
|
8
|
+
ESCALATE_TO_HUMAN, scope creep, or 'does this change look right'. Do NOT
|
|
9
|
+
approve because the change looks reasonable and the tests pass; that is the
|
|
10
|
+
exact failure this agent exists to prevent. SKIP only when no review decision
|
|
11
|
+
will be recorded.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Reviewing adversarially
|
|
15
|
+
|
|
16
|
+
You did not write this diff, you have no stake in it, and your judgement is the entire deliverable — you have no write access to code. A model reviewing work in the same context reliably talks itself into approving; the order below exists so that you cannot.
|
|
17
|
+
|
|
18
|
+
## The order
|
|
19
|
+
|
|
20
|
+
Do these in sequence. Each step's value depends on not having already formed a verdict.
|
|
21
|
+
|
|
22
|
+
**1. Read the criterion before the change.** The ticket, the reproduction, and the defining test named in `reproduction.failing_test`. You must know what "fixed" means before you see what was done, or you will judge the diff on its own terms and grade it against its own goal.
|
|
23
|
+
|
|
24
|
+
*Prevents:* accepting the author's framing of the problem.
|
|
25
|
+
|
|
26
|
+
**2. Get the diff yourself.** `pr_diff` for a PR number, or `list_changed_files` for the base/head pair. With the local vcs backend both are refused as remote-only — that refusal names the backend — so fall back to `diff` with `ref: main`. Never review from MENDER's summary of its own change.
|
|
27
|
+
|
|
28
|
+
*Prevents:* reviewing a description instead of a patch. Descriptions omit the hunks their authors have stopped seeing.
|
|
29
|
+
|
|
30
|
+
**3. Confirm the defining test was not touched.** Any hunk in that file is an immediate `REQUEST_CHANGES`, or `ESCALATE_TO_HUMAN` if the change was to weaken it. Check for the quiet versions too: a loosened assertion, a new `skip`/`xfail`, a changed parameter set, a renamed fixture.
|
|
31
|
+
|
|
32
|
+
*Prevents:* the §10 symptom fix, in its most direct form.
|
|
33
|
+
|
|
34
|
+
**4. Root cause.** Apply `root-cause-vs-symptom` in full. Require the PR body to state why the defect happens. Decide `root_cause_addressed` and be able to defend it.
|
|
35
|
+
|
|
36
|
+
*Prevents:* approving a change that suppresses the failure and leaves the defect.
|
|
37
|
+
|
|
38
|
+
**5. Read for what is missing.** The hardest step and the one with the most yield. For each changed function:
|
|
39
|
+
|
|
40
|
+
- **Callers.** Grep the symbol across the repo; for an endpoint use `find_consumers` (heuristic — it matches literal path strings, so a client assembling URLs from fragments is missed). Does every caller still hold? Are any of them in the diff, and should they be?
|
|
41
|
+
- **The other branch.** The fix handles the reproduced path. What about the error path, the empty case, the zero and the many, the unauthenticated caller, the second tenant?
|
|
42
|
+
- **The symmetric site.** If a read was fixed, was the write? If create was fixed, what about update? A defect in one half of a pair is usually in both.
|
|
43
|
+
- **Concurrency and ordering**, when shared state or async is involved.
|
|
44
|
+
- **The test that is absent.** Is there a case the change obviously affects and no test asserts?
|
|
45
|
+
|
|
46
|
+
*Prevents:* the single most common review outcome — every line present is correct, and the change is still wrong.
|
|
47
|
+
|
|
48
|
+
**6. Test quality.** `test-quality-audit` in full. Do not treat the suite result as the answer to this step.
|
|
49
|
+
|
|
50
|
+
**7. Blast radius.** `regression-risk-scoring`. Grade low, medium or high, and carry the grade into the verdict.
|
|
51
|
+
|
|
52
|
+
**8. Rollback note.** `rollback-plan-authoring` lists what it must contain. A note with no signal to watch, or no irreversibility section, is `REQUEST_CHANGES` on its own — it is cheap to ask for and expensive to be without.
|
|
53
|
+
|
|
54
|
+
**9. Scope.** Count files and lines against the budget (5 / 150). Then ask of each hunk not required by the fix: does it add risk? Unnecessary-and-risky is `REQUEST_CHANGES`. Unnecessary-and-inert is worth one line in `concerns`, not a round trip.
|
|
55
|
+
|
|
56
|
+
## "The tests pass" is the floor
|
|
57
|
+
|
|
58
|
+
A green suite tells you that the tests that were selected, and that actually ran, did not detect a problem. It does not tell you:
|
|
59
|
+
|
|
60
|
+
- that the right tests were selected — `affected_tests` is documented as a heuristic, not coverage-derived;
|
|
61
|
+
- that the new tests would fail against the unfixed code;
|
|
62
|
+
- that the tests assert behaviour rather than that a function returned;
|
|
63
|
+
- that anything at all covers the callers, the error branch, or the other tenant;
|
|
64
|
+
- that the failures which remain are pre-existing rather than caused here.
|
|
65
|
+
|
|
66
|
+
You are here to catch what the tests do not. If your review would have been identical without reading the diff, you have added nothing that CI did not already provide.
|
|
67
|
+
|
|
68
|
+
## The failure of approving because it looks reasonable
|
|
69
|
+
|
|
70
|
+
Plausible-looking diffs are the *default* output of a competent model, so plausibility carries almost no evidence about correctness in this system. The feeling of "this seems fine" arrives with equal strength for a correct fix and for a confident symptom fix, and it arrives before step 5 has been done.
|
|
71
|
+
|
|
72
|
+
The countermeasure is mechanical. Before deciding, write one sentence of the form:
|
|
73
|
+
|
|
74
|
+
> **This fix is wrong if \<X\>.**
|
|
75
|
+
|
|
76
|
+
Then go and check X. If you cannot construct an X, you have not understood the change well enough to approve it — that is itself the finding, and re-reading is cheaper than a wrong `APPROVE`. Good Xs are specific: *"...if any other caller passes a null there"*, *"...if the list can be empty"*, *"...if two requests race on that cache key"*.
|
|
77
|
+
|
|
78
|
+
## Never request a change the author is not permitted to make
|
|
79
|
+
|
|
80
|
+
Before you write `REQUEST_CHANGES`, check that the author can actually perform
|
|
81
|
+
the change you are asking for. Its write scope is declared in
|
|
82
|
+
`config/agents/<agent>.yaml` under `write_paths`, and you can read that file.
|
|
83
|
+
A path outside it will be refused by the guardrail no matter how right you are.
|
|
84
|
+
|
|
85
|
+
This is not a formality. A review that demands a forbidden edit deadlocks the
|
|
86
|
+
loop: the author tries, the guardrail refuses, the review repeats, and the two
|
|
87
|
+
round trips are spent before anyone can see that the requirement and the
|
|
88
|
+
permission contradict each other. It has happened here — a reviewer required a
|
|
89
|
+
calibration file to be updated in the same commit while the fixing agent's
|
|
90
|
+
sandbox excluded that file by design, and the ticket escalated twice with a
|
|
91
|
+
correct one-line fix sitting on the branch the whole time.
|
|
92
|
+
|
|
93
|
+
So when a change is genuinely needed but out of the author's reach:
|
|
94
|
+
|
|
95
|
+
- **Do not withhold approval on it.** Judge the diff you were given on its own
|
|
96
|
+
merits. If the fix is sound, `APPROVE` and record the outside work under
|
|
97
|
+
`concerns`, naming who has to do it.
|
|
98
|
+
- **If it is serious enough to block shipping**, `ESCALATE_TO_HUMAN` and say
|
|
99
|
+
plainly that it is out of the author's envelope — do not route it through
|
|
100
|
+
`REQUEST_CHANGES` first and burn a round trip discovering that.
|
|
101
|
+
- **Artifacts an agent is deliberately barred from** — answer keys, calibration
|
|
102
|
+
corpora, scoring oracles — are barred so the measured cannot edit the measure.
|
|
103
|
+
Their maintenance is someone else's job by design, never a defect in the diff
|
|
104
|
+
in front of you.
|
|
105
|
+
|
|
106
|
+
## The verdict
|
|
107
|
+
|
|
108
|
+
`record_review` takes exactly one decision and rejects reasoning under 40 characters, because an approval with no reasoning is the shape a rubber stamp takes.
|
|
109
|
+
|
|
110
|
+
| Situation | Decision |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Cause addressed, diff minimal, tests discriminate, risk low or mitigated | `APPROVE` |
|
|
113
|
+
| Something specific and nameable is wrong, and MENDER is permitted to change it — check `write_paths` | `REQUEST_CHANGES` |
|
|
114
|
+
| Right fix is outside MENDER's envelope, risk is high and unmitigated, or you cannot responsibly judge it | `ESCALATE_TO_HUMAN` |
|
|
115
|
+
|
|
116
|
+
- **Note a residual concern even when approving.** Use `concerns`. A reviewer with no concerns has usually not looked hard enough.
|
|
117
|
+
- **`REQUEST_CHANGES` goes back to MENDER verbatim** and it cannot act on vagueness. Name the file, name what is wrong, say what would make it right. "Consider improving error handling" is not a review.
|
|
118
|
+
- **Round trips are capped at two per ticket**, then the conductor escalates. A vague `REQUEST_CHANGES` spends half a ticket's remediation budget on a message nobody can act on.
|
|
119
|
+
- **Do not request changes on style, naming, or how you would have written it.** You have one question: should this change ship? Everything else costs a round trip and teaches the system that your reviews can be skimmed.
|
|
120
|
+
- Escalating is a legitimate outcome, not a failure to decide.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-surface-extraction
|
|
3
|
+
description: >
|
|
4
|
+
Extract every HTTP and WebSocket endpoint with its method, path, handler and
|
|
5
|
+
real auth requirement. TRIGGER - read BEFORE recording routes in a system map
|
|
6
|
+
or auditing an API surface, and whenever the task mentions endpoints, routes,
|
|
7
|
+
the API surface, handlers, or 'what does this service expose'. Do NOT trust a
|
|
8
|
+
spec file as the list of what exists. SKIP only when the route inventory is
|
|
9
|
+
already available from the system map.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Extracting the API surface
|
|
13
|
+
|
|
14
|
+
## Find routes three ways, then reconcile
|
|
15
|
+
|
|
16
|
+
1. **The router.** Decorators, route tables, `include_router` calls. Follow prefixes — a router mounted under `/v1` means every path inside it is longer than it looks.
|
|
17
|
+
2. **The declared spec.** OpenAPI, protobuf, GraphQL schema.
|
|
18
|
+
3. **The generated spec**, if the framework produces one at runtime (`/openapi.json`). This is what the framework thinks it implements, which is a third and distinct thing.
|
|
19
|
+
|
|
20
|
+
Reconciling these three is where the findings are. A route in the spec but not the router is a broken promise to consumers. A route in the router but not the spec is undocumented surface, often forgotten and unmaintained.
|
|
21
|
+
|
|
22
|
+
## Record auth as the code enforces it
|
|
23
|
+
|
|
24
|
+
This is the field most often recorded wrong, because it is the one people record from intent rather than observation.
|
|
25
|
+
|
|
26
|
+
- A dependency that extracts a user is **authentication**. It says who you are.
|
|
27
|
+
- A check on role, org, or ownership is **authorization**. It says what you may touch.
|
|
28
|
+
- An endpoint with the first and not the second is authenticated and unguarded — a very common and very serious defect.
|
|
29
|
+
|
|
30
|
+
Record what the handler actually checks, per endpoint, not what the router group implies. Middleware that "protects" a prefix must be verified to apply to each route under it, not assumed to.
|
|
31
|
+
|
|
32
|
+
## Order-sensitive registration
|
|
33
|
+
|
|
34
|
+
Where a literal path and a parameterised path overlap (`/orders/legacy` and `/orders/{id}`), **registration order decides which wins**. Record the order. A literal registered after a greedy parameterised route is unreachable — and reachability is not visible in a spec.
|
|
35
|
+
|
|
36
|
+
## Also record
|
|
37
|
+
|
|
38
|
+
Pagination parameters and whether they are actually applied; the error shape each endpoint returns; the status codes it can produce. Downstream agents test against these and cannot infer them.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: authz-matrix-check
|
|
3
|
+
description: >
|
|
4
|
+
Check every endpoint against the role matrix to find missing authorization.
|
|
5
|
+
TRIGGER - read BEFORE auditing any endpoint for permissions, and whenever the
|
|
6
|
+
task mentions authorization, roles, permissions, access control, IDOR,
|
|
7
|
+
tenancy, multi-tenant scoping, or 'who can call this'. Do NOT treat the
|
|
8
|
+
presence of an auth dependency as evidence that access is checked - that
|
|
9
|
+
confusion is the defect this skill exists to catch. SKIP only when no endpoint
|
|
10
|
+
has an access-control requirement.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Auditing authorization
|
|
14
|
+
|
|
15
|
+
## The distinction that produces the defect
|
|
16
|
+
|
|
17
|
+
**Authentication** proves who you are. **Authorization** decides what you may touch. An endpoint with the first and not the second is the most common serious API defect there is, and it looks correct at a glance because there is clearly an auth dependency on the handler.
|
|
18
|
+
|
|
19
|
+
Two questions per endpoint, always separately:
|
|
20
|
+
|
|
21
|
+
1. Does it establish an identity?
|
|
22
|
+
2. Does it check that *this* identity may act on *this* resource?
|
|
23
|
+
|
|
24
|
+
## Build the matrix
|
|
25
|
+
|
|
26
|
+
Rows are endpoints, columns are roles, plus a column for "resource belonging to another tenant". Fill each cell with what the code actually does, and compare against what the spec or product intent says it should. Every disagreement is a candidate finding.
|
|
27
|
+
|
|
28
|
+
Do not fill a cell from the router group or a middleware prefix. Verify per handler — a route added later under a "protected" prefix very often misses the check that its neighbours have.
|
|
29
|
+
|
|
30
|
+
## Three failure shapes
|
|
31
|
+
|
|
32
|
+
- **Missing role check.** Handler requires a user, never inspects the role. A viewer can refund.
|
|
33
|
+
- **Missing tenancy scope.** Lookup filters by resource id alone, not by the caller's organization. Any authenticated user reads any record by guessing an id. This is the highest-severity shape and the easiest to miss, because the endpoint behaves perfectly for the tester's own data.
|
|
34
|
+
- **Check that cannot fail.** The check exists but reads a client-supplied value, or compares the resource's own org against itself.
|
|
35
|
+
|
|
36
|
+
## Read the neighbours
|
|
37
|
+
|
|
38
|
+
The strongest signal is inconsistency. When five handlers in a file filter on `org_id` and the sixth does not, that sixth is almost certainly a dropped clause rather than a deliberate exception. Symmetry-breaking is the cheapest defect detector available here.
|
|
39
|
+
|
|
40
|
+
## Prove it, carefully
|
|
41
|
+
|
|
42
|
+
Demonstrate with two accounts: create a resource as one, read it as the other, capture the response. That is proof.
|
|
43
|
+
|
|
44
|
+
Do not escalate beyond demonstrating access. Do not extract data volume, do not chain into further systems, do not test how much you can reach. One captured cross-tenant read is complete evidence; anything past it is an incident of your own making.
|
|
45
|
+
|
|
46
|
+
Everything found here is `security_relevant: true` and routes restricted. See `routing-rules`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: console-error-triage
|
|
3
|
+
description: >
|
|
4
|
+
Decide which browser console output is a defect and which is noise. TRIGGER -
|
|
5
|
+
read BEFORE reporting anything seen in a browser console, and whenever the
|
|
6
|
+
task mentions console errors, warnings, unhandled rejections, or JavaScript
|
|
7
|
+
errors. Do NOT report console output without triaging it - warnings are not
|
|
8
|
+
defects and filing them destroys precision. SKIP only when no console output
|
|
9
|
+
is involved.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Triaging console output
|
|
13
|
+
|
|
14
|
+
## The classification
|
|
15
|
+
|
|
16
|
+
| Seen | Verdict |
|
|
17
|
+
|---|---|
|
|
18
|
+
| **Unhandled promise rejection** | **Always a defect.** A code path with no error handling, running in production. |
|
|
19
|
+
| **Uncaught exception during a normal journey** | **Defect.** Whatever came after it did not run. |
|
|
20
|
+
| **Failed network request the UI does not surface** | **Defect.** The user sees a blank or stale page and no explanation. |
|
|
21
|
+
| `console.error` from application code | Usually a defect — the developer marked it as one. |
|
|
22
|
+
| React key warnings, deprecation notices | Tech debt at most. Not a bug. |
|
|
23
|
+
| `console.warn` from application code | **Not a defect** unless it names a real failure. Often deliberate signposting. |
|
|
24
|
+
| Dev-only output behind an environment check | **Not a defect.** Never reaches users. |
|
|
25
|
+
| Extension noise, source map warnings, favicon 404 | Not a defect. Not even the app. |
|
|
26
|
+
|
|
27
|
+
## Read before you act
|
|
28
|
+
|
|
29
|
+
Capture the console **before** interacting. An error already present on load did not come from your click, and attributing it to your click sends someone hunting in the wrong handler.
|
|
30
|
+
|
|
31
|
+
## Dev-only is the trap
|
|
32
|
+
|
|
33
|
+
Output behind `if (import.meta.env.DEV)` or `NODE_ENV !== 'production'` is invisible to users by construction. Reporting it as a defect is a false positive, and a particularly damaging one because it signals the finder did not read the surrounding line.
|
|
34
|
+
|
|
35
|
+
## Report the consequence, not the text
|
|
36
|
+
|
|
37
|
+
A stack trace pasted into a ticket is not a finding. What did the user lose? "The orders list stays permanently blank after a failed request, with no error shown and no retry" is actionable. "TypeError: cannot read property 'map' of undefined" is a symptom that could arise ten ways.
|
|
38
|
+
|
|
39
|
+
Attach the trace as evidence. Lead with what broke.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contract-test-generation
|
|
3
|
+
description: >
|
|
4
|
+
Write a runnable test that asserts an endpoint's declared contract, to attach
|
|
5
|
+
as evidence. TRIGGER - read BEFORE producing any contract test or attaching
|
|
6
|
+
test evidence to an API finding, and whenever the task mentions contract
|
|
7
|
+
tests, evidence for an API defect, or proving a spec violation. Do NOT attach
|
|
8
|
+
a test you have not executed against both the violating and the conforming
|
|
9
|
+
behaviour. SKIP only when the finding is not a contract violation.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Generating a contract test
|
|
13
|
+
|
|
14
|
+
A contract test is the difference between a finding an engineer argues with and one they fix. It converts "the response is missing a field" into a red test with a name.
|
|
15
|
+
|
|
16
|
+
## What it must do
|
|
17
|
+
|
|
18
|
+
- **Fail now, for the stated reason.** Run it. A test that passes against the current implementation is not evidence of a defect — it is evidence you misread the code.
|
|
19
|
+
- **Pass once fixed.** Assert the contract, not the current behaviour inverted. `assert "currency" in body` is a contract; `assert "currency" not in body` pins the bug in place permanently.
|
|
20
|
+
- **Be readable by whoever picks up the ticket.** The test name states the contract: `test_invoice_response_includes_currency`, not `test_api_06`.
|
|
21
|
+
|
|
22
|
+
## Assert exactly one thing
|
|
23
|
+
|
|
24
|
+
One contract violation, one test. A test asserting status, shape, and auth together fails on whichever it hits first, and the reader learns one third of what you knew.
|
|
25
|
+
|
|
26
|
+
## Include the setup
|
|
27
|
+
|
|
28
|
+
Which fixture, which role, which token. A test that only passes on a machine where someone happened to log in first is not evidence, and it will be deleted the first time it fails in CI.
|
|
29
|
+
|
|
30
|
+
## Assert the shape, not the payload
|
|
31
|
+
|
|
32
|
+
Assert that `currency` is present and is a three-character string. Do not assert `currency == "USD"` unless the contract fixes that value — otherwise the test breaks on a legitimate data change and gets deleted, taking the real assertion with it.
|
|
33
|
+
|
|
34
|
+
## Name the source
|
|
35
|
+
|
|
36
|
+
A comment giving the spec path and the clause being asserted (`openapi.yaml: Invoice.required includes currency`) means the next reader can check whether the contract itself was wrong, which is sometimes the right answer.
|