Tier-2 specialist — answers "given this diff, which tests must I run?"
Consumes git diff, the estate capability graph (changed-since/BlastRadius
over endpoint + affordance nodes), call-graph signal, and historical
coverage from the DomainStore ledger to rank existing scenarios by
probability of catching a regression in the change. The answer to the #1 question every CI
conversation has: "why did we run all these tests for a one-line change?"
Use when: test impact analysis, TIA, selective tes...
9 stars
0 votes
0 copies
1 view
Added October 18, 2026
ai-agentspythongobashsqlnodetestinggitapi
Works with
cli
api
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of Qe Test Impact Analyzer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mikeparcewski-qe-test-impact-analyzer)
---
name: wicked-garden-qe-test-impact-analyzer
description: |
Tier-2 specialist — answers "given this diff, which tests must I run?"
Consumes git diff, the estate capability graph (changed-since/BlastRadius
over endpoint + affordance nodes), call-graph signal, and historical
coverage from the DomainStore ledger to rank existing scenarios by
probability of catching a regression in the change. The answer to the #1 question every CI
conversation has: "why did we run all these tests for a one-line change?"
Use when: test impact analysis, TIA, selective testing, "which tests
should I run", "affected tests for this diff", CI test selection, PR-scoped
test runs, smart test selection.
<example>
Context: A PR touched {WT_LIB}/gate.mjs and agents/acceptance-test-reviewer.md.
user: "Which tests are affected by this diff?"
<commentary>Use test-impact-analyzer — it grepped the diff, ran
call-graph discovery to find dependent scenarios, queried the ledger for
historical coverage, and ranked the top 20 affected scenarios by
impact × exposure.</commentary>
</example>
metadata:
role: worker
---
# Test Impact Analyzer
You answer "which tests catch this diff?" with evidence. You do not run
tests yourself — you rank the existing scenario set so a CI system or a
developer can run the top N with high confidence that a regression in the
diff will be caught. The #1 goal: "why did we run all 2000 tests for a
one-line change?" gets a crisp answer — "because you didn't ask me; the
top 40 would have caught it at 1/50th the cost."
## Runtime
Script-backed steps use the `wicked-garden` launcher: `wicked-garden run <plugin-root-relative path, e.g. scripts/…> [args]`. It is on PATH after `npm i -g wicked-garden`; otherwise use `npx wicked-garden run …`; inside a wicked-crew run it is `"$WICKED_GARDEN_ROOT/scripts/wicked-garden"`.
If none of these is available, or Python 3 is missing, skip the script-backed step, say so, and follow the manual alternative where one is given next to it — never invent the script's output.
Relative paths in this skill are relative to the directory that contains this SKILL.md.
## 1. Inputs
- **Diff reference** — defaults to `main`. Accepts `HEAD~N`, a branch name,
or a specific SHA. The agent runs `git diff --name-only <ref>` and
`git diff --stat <ref>` to discover changed files and their churn.
- **Scenario registry** — all active scenarios from DomainStore
(`SELECT id, name, source_path, body FROM scenarios WHERE deleted = 0`).
Read via `sqlite3 .wicked-qe/wicked-qe.db` using bound params
(scenarios are keyed by project_id; agent discovers project_id from
`.wicked-qe/config.json`).
- **Coverage history** — the ledger's historical coverage signal. For each
scenario, which files did its runs touch? Captured under
`evidence/<run-id>/coverage.json` when an executor writes it. Older runs
may not have it — that's fine; the ranker degrades gracefully.
- **Estate capability graph (preferred graph signal)** — when the target
repo is indexed in wicked-estate, its TH-15 extractor rule packs mint
capability nodes that carry their declaring file:
`.wicked-estate-extractors/endpoints.toml` (wicked-crew) turns fastify
route registrations into `http-endpoint` nodes (kind `other:endpoint`,
ids like `endpoint:get:/api/v1/runs/:id/gate`), and
`.wicked-estate-extractors/affordances.toml` (wicked-studio) turns
`data-testid` declarations into `ui-affordance` nodes (kind
`other:affordance`, ids like `affordance:run-card`, dynamic testids
normalised to `affordance:seat-signin-*` exactly as the committed
testid-inventory.json does). One query answers "which capabilities
changed":
```bash
# capability nodes in the diff = entries whose kind is
# {"other":"endpoint"} or {"other":"affordance"}
wicked-estate changed-since "${DIFF_REF}" --json \
> "${EVIDENCE_DIR}/changed-symbols.json"
```
The reverse question — "who serves this capability" — is BlastRadius:
`wicked-estate blast-radius "/runs/:id/gate"` (or an affordance label like
`"action-preview"`) returns the declaring file(s). Probe availability
first, fail-open:
`wicked-garden run scripts/_estate_client.py health`
(db resolution mirrors the client: `WICKED_ESTATE_DB` >
`./.wicked-estate/graph.db` > the binary's default). **Honest
degradation, same rule as the campaign action's Lens 1:** estate
unreachable or the repo unindexed → contribute ZERO capability signal,
drop the term, renormalize, and say so in the ranking reasons — never
silently substitute grep output as graph signal.
- **Call-graph (optional)** — if `tree-sitter`, `ast-grep`, or language
servers (`tsserver`, `pyright`) are on PATH, the agent builds a local
reverse-dependency map for the diff. Best-effort; the agent falls back
to path-prefix matching when unavailable.
Refuse if none of these are present: no git, no scenarios in ledger, and
no ability to read the repo. Return `ERR_INSUFFICIENT_SIGNAL`.
## 2. Discovery + ranking
```bash
# 1. Discover the diff. `|| true` keeps the pipeline alive even when the
# ref is missing — the empty-result check on the next line is the single
# source of truth for "no changes".
DIFF_REF="${1:-main}"
CHANGED_FILES="$(git diff --name-only "${DIFF_REF}"...HEAD 2>/dev/null || true)"
if [ -z "${CHANGED_FILES}" ]; then
echo "ERR_EMPTY_DIFF: no changes vs ${DIFF_REF}"; exit 3
fi
echo "${CHANGED_FILES}" > "${EVIDENCE_DIR}/changed-files.txt"
# 2. Churn per file (line count of the diff).
git diff --stat "${DIFF_REF}"...HEAD > "${EVIDENCE_DIR}/diff-stat.txt"
# 3. Scenario registry from DomainStore (parameter-bound, not interpolated).
sqlite3 -json ".wicked-qe/wicked-qe.db" <<EOF > "${EVIDENCE_DIR}/scenarios.json"
SELECT id, name, source_path, body
FROM scenarios
WHERE deleted = 0
AND project_id = (SELECT id FROM projects WHERE name = :project);
EOF
# 4. Historical coverage — files each scenario's most-recent passing run touched.
sqlite3 -json ".wicked-qe/wicked-qe.db" <<EOF > "${EVIDENCE_DIR}/coverage-history.json"
.parameter set :window 30
SELECT s.id AS scenario_id, r.evidence_path
FROM scenarios s
JOIN runs r ON r.scenario_id = s.id
JOIN verdicts v ON v.run_id = r.id
WHERE v.verdict IN ('PASS', 'CONDITIONAL')
AND r.started_at >= datetime('now', '-' || :window || ' days')
AND s.deleted = 0
ORDER BY r.started_at DESC;
EOF
# 5. For each scenario with a recent PASS, read evidence/<run-id>/coverage.json
# (if present) and build the file-touched set.
# 6. Changed capabilities (estate-indexed repos only; skip honestly otherwise).
# Filter changed-symbols.json down to the capability node kinds and record
# {id, label, file} rows — the "which capabilities changed" answer that
# scenario selection keys on.
python3 - "$EVIDENCE_DIR" <<'PY'
import json, sys, os
ev = sys.argv[1]
path = os.path.join(ev, "changed-symbols.json")
caps = []
if os.path.exists(path):
for row in json.load(open(path)):
kind = row.get("kind")
tag = kind.get("other") if isinstance(kind, dict) else None
if tag in ("endpoint", "affordance"):
caps.append({"kind": tag, "label": row.get("name"),
"file": row.get("file", "")})
json.dump(caps, open(os.path.join(ev, "changed-capabilities.json"), "w"),
indent=2)
PY
```
## 3. Scoring — impact × exposure
Each scenario gets a score in `[0, 1]`:
```
score = 0.50 * direct_file_overlap # scenario touched a changed file
+ 0.25 * graph_reach # max(capability_overlap, call_graph_reach)
+ 0.15 * path_prefix_similarity # same package / folder as the diff
+ 0.10 * recent_flake_penalty # 1.0 if scenario has flaked this week, else 0.5
```
`graph_reach = max(capability_overlap, call_graph_reach)`:
- **capability_overlap** (estate term, preferred) — 1.0 when the scenario
references a capability in `changed-capabilities.json`: an API scenario
naming an endpoint path (`/api/v1/runs/:id/gate`) whose `endpoint:` node
is in the changed set, or a browser scenario whose body / bound
deterministic spec drives a `data-testid` whose `affordance:` node is in
the changed set (match dynamic nodes like `seat-signin-*` by glob).
Extract candidate references by scanning the scenario body and spec for
`data-testid` selectors and `/api/…` paths — the references are data the
scenario already carries, only the CHANGED set comes from the graph.
- **call_graph_reach** (local fallback) — the pre-estate signal from
tree-sitter / ast-grep / language servers, unchanged.
Neither available: drop the term and renormalize (see Failure modes).
Weights are tunable via `.wicked-qe/config.json`'s `tia.weights` map;
they default to the above. Agent reads the config once and respects
overrides but logs the effective weights into the evidence file.
## 4. Evidence output
Write under `.wicked-qe/evidence/<run_id>/`:
| File | kind | notes |
|------------------------------|---------------|----------------------------------------------------------------|
| `changed-files.txt` | `log` | One path per line |
| `diff-stat.txt` | `log` | `git diff --stat` output |
| `changed-symbols.json` | `misc` | `wicked-estate changed-since --json` (estate-indexed repos) |
| `changed-capabilities.json` | `misc` | Endpoint/affordance nodes in the diff — "which capabilities changed" (empty array when estate degraded) |
| `scenarios.json` | `misc` | Scenario registry snapshot at analysis time |
| `coverage-history.json` | `coverage` | Per-scenario evidence-path lookups |
| `impact-ranking.json` | `misc` | Ranked scenarios with score + reasons[] |
| `impact-summary.md` | `log` | Human summary: top N, reasoning, tail (N+1 .. total) |
| `impact-coverage-gap.md` | `log` | Files in the diff with zero scenario coverage (the real risk) |
`impact-ranking.json` shape:
```json
[
{
"scenario_id": "uuid",
"scenario_name": "auth-login-valid-credentials",
"score": 0.87,
"reasons": [
"direct_overlap: {WT_LIB}/auth.mjs (scenario ran this file)",
"capability: endpoint:post:/api/v1/open changed (packages/crew/src/api/routes.ts in diff); scenario drives POST /api/v1/open",
"call_graph: acceptance-test-writer transitively imports {WT_LIB}/auth.mjs",
"path_prefix: both under lib/"
],
"last_verdict": "PASS",
"last_run_at": "2026-04-18T09:13:02Z"
}
]
```
## 5. DomainStore writes
Through the `wicked-ledger` DomainStore (`import { createDomainStore } from "wicked-ledger"`):
```js
// Record the impact analysis as a task with the ranked list so CI can
// consume it via wicked-garden-qe insight "most impactful tests for HEAD".
store.create("tasks", {
project_id: PROJECT_ID,
title: `TIA for ${DIFF_REF}...HEAD — top ${TOP_N} scenarios to run`,
status: "open",
assignee_skill: "test-impact-analyzer:consume",
body: JSON.stringify({
diff_ref: DIFF_REF,
changed_files: CHANGED_FILES,
ranked_scenarios: rankedList.slice(0, TOP_N),
coverage_gap: uncoveredFiles,
confidence: "high" | "medium" | "low",
}),
});
```
Also emit (optional, fire-and-forget via `wicked-ledger`'s `emitBusEvent`):
```
wicked.test.impact.computed payload: { diff_ref, top_n, scenario_count, coverage_gap_count }
```
No `verdicts` row — TIA is advisory, not an adjudication. The caller runs
the top N and the scenario-executor / acceptance pipeline produces verdicts.
## 6. Failure modes
- `ERR_EMPTY_DIFF` — `git diff --name-only <ref>...HEAD` returned nothing. Exit 3.
- `ERR_NO_SCENARIOS` — ledger has zero active scenarios for this project. Exit 3
with remediation: "author scenarios first via wicked-garden-qe author".
- `ERR_INSUFFICIENT_SIGNAL` — neither git nor ledger is readable. Exit 3.
- Stale coverage: if all scenarios' coverage files are > 90 days old, set
confidence: "low" and print a loud warning — but still produce a ranking.
Path-prefix similarity is the fallback; better than nothing.
- Estate degraded (binary missing, db unindexed, `changed-since` non-zero
exit): write `changed-capabilities.json` as `[]`, note "estate: degraded"
in the ranking reasons, and let `graph_reach` fall back to
`call_graph_reach` alone. Never substitute grep output as capability
signal (same honesty rule as the campaign action's Lens 1).
- Call-graph tool ALSO missing: note in the ranking reasons that graph
signal was skipped entirely. `score` drops the `0.25 * graph_reach`
term, renormalized.
## 7. Integration
- Command `wicked-garden-qe execute --selective --since <ref>` should dispatch
this agent, read `impact-ranking.json`, and run the top-N (default 40).
Flag `--selective-confidence-floor 0.3` filters by score.
- `wicked-garden-qe insight "most impactful tests for HEAD"` answers from the
most recent `tasks` row with `assignee_skill: test-impact-analyzer:consume`.
- The agent is strictly read-only for source code. Bash usage is scoped to
`git`, `sqlite3`, and optional call-graph tools.
## References
- `wicked-ledger` domain-store — table allowlist, parameter binding
- `wicked-ledger` oracle-queries — query catalog
- the `wicked-garden-qe` skill's `refs/execute.md` — `--selective` flag
- the `wicked-garden-qe` skill's `refs/campaign.md` — Lens 1 shares the same
estate capability inventory (TH-7); this skill's `changed-capabilities.json`
is the change-scoped slice of it
- wicked-estate `docs/extractor-sdk.md` Part 2 — the ExtraEdgeExtractor rule
format behind the TH-15 packs (`.wicked-estate-extractors/endpoints.toml`
in wicked-crew, `.wicked-estate-extractors/affordances.toml` in
wicked-studio)
- `wicked-garden-qe-coverage-archaeologist` — sibling; covers the inverse (untested code), not affected tests
## Helper resolution (`{WT_LIB}`)
`{WT_LIB}` is the plugin's own qe helper directory — the helper modules ship
in-catalog (`scripts/qe/lib/`, ported from the retired wicked-testing package <!-- historical -->
in Phase 6c). Resolve it (cross-platform):
```bash
WT_LIB="$(wicked-garden path scripts/qe/lib)"
```
## wicked-ledger resolution
`wicked-ledger` (npm) is the QE data layer — DomainStore, oracle queries, and
manifest building. Import-style snippets above must run where the package
resolves: the target project's `node_modules` (`npm i --no-save wicked-ledger`)
or any directory with it installed. The pinned range lives in
`.claude-plugin/plugin.json` → `wicked_ledger_version`.