@spec-wave/cli 0.29.0 → 0.32.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/package.json +5 -3
- package/protocol/qa-result.v1.json +62 -0
- package/protocol/qa-trail-report.v1.json +113 -0
- package/src/api/github-graphql.mjs +6 -1
- package/src/api/github-rest.mjs +21 -0
- package/src/cli.mjs +114 -9
- package/src/commands/decompose.mjs +29 -3
- package/src/commands/doctor.mjs +183 -3
- package/src/commands/generate-qa-plan.mjs +421 -0
- package/src/commands/implement.mjs +56 -44
- package/src/commands/merge.mjs +43 -14
- package/src/commands/order.mjs +350 -96
- package/src/commands/qa-lead.mjs +748 -0
- package/src/commands/qa-run.mjs +892 -0
- package/src/commands/run.mjs +5 -1
- package/src/config.mjs +32 -1
- package/src/lib/artifact-pr.mjs +2 -0
- package/src/lib/artifact-publish.mjs +5 -2
- package/src/lib/board.mjs +14 -0
- package/src/lib/critique.mjs +38 -9
- package/src/lib/decomposition-doc.mjs +5 -1
- package/src/lib/dependency-map.mjs +300 -0
- package/src/lib/doc-paths.mjs +9 -2
- package/src/lib/git-retry.mjs +82 -0
- package/src/lib/net-cache.mjs +142 -0
- package/src/lib/next-step.mjs +15 -3
- package/src/lib/qa-exec.mjs +335 -0
- package/src/lib/qa-lead-backend.mjs +213 -0
- package/src/lib/qa-lead.mjs +627 -0
- package/src/lib/qa-plan-doc.mjs +340 -0
- package/src/lib/qa-report.mjs +396 -0
- package/src/lib/skill-compose.mjs +234 -0
- package/src/lib/story-graph.mjs +256 -0
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/skills/merge/SKILL.md +1 -0
- package/src/plugin/skills/order/SKILL.md +21 -5
- package/src/plugin/skills/qa/SKILL.md +107 -0
- package/src/plugin/skills/qa/model-prompt.critique.md +44 -0
- package/src/plugin/skills/qa/model-prompt.md +68 -0
- package/src/plugin/skills/qa-executor/SKILL.md +76 -0
- package/src/plugin/skills/qa-lead/SKILL.md +89 -0
- package/src/templates/skill/SKILL.md +981 -279
- package/src/templates/skill/core.md +584 -0
- package/src/templates/workflows/generate-qa-plan.yml +64 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Carregamento do grafo de Stories e do snapshot do board — a CÓPIA ÚNICA do
|
|
2
|
+
// laço que order/implement/merge/qa-lead mantinham cada um por si (e que
|
|
3
|
+
// custava 1..3 chamadas POR STORY em cada comando).
|
|
4
|
+
//
|
|
5
|
+
// Fontes das ARESTAS, da mais barata para a mais cara:
|
|
6
|
+
// 1. `docs/features/<slug>/dependency-map.json` — artefato COMMITADO, escrito
|
|
7
|
+
// pelo decompose --apply e atualizado pelo `order --sync`;
|
|
8
|
+
// 2. `decomposition.md` aplicado (mesma informação, formato de prosa);
|
|
9
|
+
// 3. linhas "Depende de: #N" do corpo das sub-issues — que o listSubIssues já
|
|
10
|
+
// devolve DE GRAÇA (zero chamada extra);
|
|
11
|
+
// 4. `remote: true` → blocked_by nativo via API, 1 chamada por Story (cache
|
|
12
|
+
// `blockedby-*` com TTL). É a única fonte que enxerga aresta criada SÓ
|
|
13
|
+
// pela UI — por isso o merge --yes a exige fresca.
|
|
14
|
+
// 1–3 são sempre UNIDAS (são grátis); a origem informada ao usuário diz o que
|
|
15
|
+
// entrou.
|
|
16
|
+
//
|
|
17
|
+
// Etapa/estado vêm do snapshot (`listProjectItems`, 1 chamada paginada),
|
|
18
|
+
// cacheado com TTL — NUNCA do par addProjectItem+getItemSingleSelectValue, que
|
|
19
|
+
// além de custar 2 chamadas por item é MUTAÇÃO em caminho de leitura.
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { listSubIssues, listProjectItems } from '../api/github-graphql.mjs';
|
|
25
|
+
import { listBlockedBy, getIssue } from '../api/github-rest.mjs';
|
|
26
|
+
import { detectIssueType } from './issue-type.mjs';
|
|
27
|
+
import { featureDocPaths } from './doc-paths.mjs';
|
|
28
|
+
import { parseDecompositionDoc } from './decomposition-doc.mjs';
|
|
29
|
+
import { parseDependencies } from './dependencies.mjs';
|
|
30
|
+
import { indexBoardItems } from './board.mjs';
|
|
31
|
+
import {
|
|
32
|
+
parseDependencyMap, storiesFromAppliedDoc, mergeDependencyEdges, isFresh,
|
|
33
|
+
DEPENDENCY_MAP_FILE,
|
|
34
|
+
} from './dependency-map.mjs';
|
|
35
|
+
import {
|
|
36
|
+
readCacheEntry, writeCacheEntry, DEFAULT_CACHE_TTL_SEC,
|
|
37
|
+
} from './net-cache.mjs';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stories de uma Feature com as arestas resolvidas — fonte híbrida.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} params
|
|
43
|
+
* @param {string} params.token
|
|
44
|
+
* @param {string} params.owner
|
|
45
|
+
* @param {string} params.repo
|
|
46
|
+
* @param {string} params.root raiz do projeto (docs/ e cache moram nela)
|
|
47
|
+
* @param {{number:number, nodeId?:string, node_id?:string, title:string}} params.feature
|
|
48
|
+
* @param {boolean} [params.remote] une também o blocked_by da API (S chamadas)
|
|
49
|
+
* @param {boolean} [params.refresh] ignora TODO cache na leitura (mas grava)
|
|
50
|
+
* @param {number} [params.ttlSec]
|
|
51
|
+
* @returns {Promise<{
|
|
52
|
+
* stories: Array<{number, title, nodeId, body, state, stateReason, createdAt,
|
|
53
|
+
* milestone, labels, dependsOn:number[]}>,
|
|
54
|
+
* allSubs: Array<object>,
|
|
55
|
+
* origin: { edges: string, mapGeneratedAt: string|null, subsFrom: 'cache'|'api',
|
|
56
|
+
* fetchedAt: string|null },
|
|
57
|
+
* warnings: string[],
|
|
58
|
+
* }>}
|
|
59
|
+
*/
|
|
60
|
+
export async function loadFeatureStories({
|
|
61
|
+
token, owner, repo, root, feature, remote = false, refresh = false,
|
|
62
|
+
ttlSec = DEFAULT_CACHE_TTL_SEC,
|
|
63
|
+
} = {}) {
|
|
64
|
+
const warnings = [];
|
|
65
|
+
const nodeId = feature.nodeId || feature.node_id;
|
|
66
|
+
|
|
67
|
+
// ── sub-issues (com body!) — cache subissues-<n> ───────────────────────────
|
|
68
|
+
const subsKey = `subissues-${feature.number}`;
|
|
69
|
+
const cached = refresh ? null : readCacheEntry(root, subsKey, { owner, repo });
|
|
70
|
+
let allSubs;
|
|
71
|
+
let subsFrom;
|
|
72
|
+
let fetchedAt;
|
|
73
|
+
if (cached && isFresh(cached, ttlSec)) {
|
|
74
|
+
allSubs = cached.data;
|
|
75
|
+
subsFrom = 'cache';
|
|
76
|
+
fetchedAt = cached.fetchedAt;
|
|
77
|
+
} else {
|
|
78
|
+
allSubs = await listSubIssues(token, nodeId);
|
|
79
|
+
subsFrom = 'api';
|
|
80
|
+
fetchedAt = new Date().toISOString();
|
|
81
|
+
writeCacheEntry(root, subsKey, 'sub-issues', allSubs, { owner, repo });
|
|
82
|
+
}
|
|
83
|
+
const storySubs = allSubs
|
|
84
|
+
.filter(s => detectIssueType({ title: s.title, labels: s.labels }) === 'Story');
|
|
85
|
+
|
|
86
|
+
// ── fontes de aresta locais (grátis — sempre unidas) ───────────────────────
|
|
87
|
+
const sources = [];
|
|
88
|
+
const parts = [];
|
|
89
|
+
let mapGeneratedAt = null;
|
|
90
|
+
|
|
91
|
+
const paths = root ? safeDocPaths(root, feature.title) : null;
|
|
92
|
+
if (paths) {
|
|
93
|
+
const mapAbs = path.join(root, paths['dependency-map'].rel);
|
|
94
|
+
if (existsSync(mapAbs)) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = parseDependencyMap(JSON.parse(readFileSync(mapAbs, 'utf-8')));
|
|
97
|
+
if (parsed.ok) {
|
|
98
|
+
sources.push(parsed.map.stories);
|
|
99
|
+
parts.push('map');
|
|
100
|
+
mapGeneratedAt = parsed.map.generatedAt || null;
|
|
101
|
+
} else {
|
|
102
|
+
warnings.push(`${DEPENDENCY_MAP_FILE} ignorado: ${parsed.reason} — regenere com \`order ${feature.number} --sync\`.`);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
warnings.push(`${DEPENDENCY_MAP_FILE} ilegível — regenere com \`order ${feature.number} --sync\`.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (parts.length === 0) {
|
|
109
|
+
const docAbs = path.join(root, paths.decomposition.rel);
|
|
110
|
+
if (existsSync(docAbs)) {
|
|
111
|
+
try {
|
|
112
|
+
const converted = storiesFromAppliedDoc(parseDecompositionDoc(readFileSync(docAbs, 'utf-8')));
|
|
113
|
+
if (converted.ok) {
|
|
114
|
+
sources.push(converted.stories);
|
|
115
|
+
parts.push('doc');
|
|
116
|
+
}
|
|
117
|
+
} catch { /* doc ilegível → segue com o body */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Body: as linhas "Depende de: #N" que o apply gravou (ou o humano editou).
|
|
123
|
+
sources.push(storySubs.map(s => ({ number: s.number, dependsOn: parseDependencies(s.body) })));
|
|
124
|
+
parts.push('body');
|
|
125
|
+
|
|
126
|
+
// ── blocked_by remoto (opcional — a única fonte com aresta só-da-UI) ───────
|
|
127
|
+
if (remote) {
|
|
128
|
+
const remoteEdges = [];
|
|
129
|
+
for (const s of storySubs) {
|
|
130
|
+
const key = `blockedby-${s.number}`;
|
|
131
|
+
const hit = refresh ? null : readCacheEntry(root, key, { owner, repo });
|
|
132
|
+
if (hit && isFresh(hit, ttlSec)) {
|
|
133
|
+
remoteEdges.push({ number: s.number, dependsOn: hit.data });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const deps = (await listBlockedBy(token, owner, repo, s.number)).map(b => b.number);
|
|
138
|
+
remoteEdges.push({ number: s.number, dependsOn: deps });
|
|
139
|
+
writeCacheEntry(root, key, 'blocked-by', deps, { owner, repo });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
// Falha de rede NÃO entra no cache e não derruba: as fontes locais
|
|
142
|
+
// seguem valendo — mas o usuário precisa saber que o remoto ficou fora.
|
|
143
|
+
warnings.push(`blocked_by de #${s.number} não consultável agora (${err.message}).`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
sources.push(remoteEdges);
|
|
147
|
+
parts.push('remote');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const edges = mergeDependencyEdges(...sources);
|
|
151
|
+
return {
|
|
152
|
+
stories: storySubs.map(s => ({ ...s, dependsOn: edges.get(s.number) || [] })),
|
|
153
|
+
allSubs,
|
|
154
|
+
origin: { edges: parts.join('+'), mapGeneratedAt, subsFrom, fetchedAt },
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Sub-issues de um item, com cache `subissues-<n>` (TTL).
|
|
161
|
+
*
|
|
162
|
+
* Para quem precisa dos filhos CRUS (ex.: as Tasks de uma Story no implement)
|
|
163
|
+
* sem o resto do grafo.
|
|
164
|
+
*
|
|
165
|
+
* @returns {Promise<{subs:Array, fromCache:boolean, fetchedAt:string}>}
|
|
166
|
+
*/
|
|
167
|
+
export async function cachedSubIssues({
|
|
168
|
+
token, owner, repo, root, parent, refresh = false, ttlSec = DEFAULT_CACHE_TTL_SEC,
|
|
169
|
+
} = {}) {
|
|
170
|
+
const key = `subissues-${parent.number}`;
|
|
171
|
+
if (!refresh) {
|
|
172
|
+
const hit = readCacheEntry(root, key, { owner, repo });
|
|
173
|
+
if (hit && isFresh(hit, ttlSec)) return { subs: hit.data, fromCache: true, fetchedAt: hit.fetchedAt };
|
|
174
|
+
}
|
|
175
|
+
const subs = await listSubIssues(token, parent.nodeId || parent.node_id);
|
|
176
|
+
writeCacheEntry(root, key, 'sub-issues', subs, { owner, repo });
|
|
177
|
+
return { subs, fromCache: false, fetchedAt: new Date().toISOString() };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// featureDocPaths lança para título vazio/eslug inválido — aqui vira "sem docs".
|
|
181
|
+
function safeDocPaths(root, title) {
|
|
182
|
+
try {
|
|
183
|
+
return featureDocPaths(root, { title }, 'Feature');
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Snapshot do board (Etapa/estado/milestone de todos os itens) — 1 chamada
|
|
191
|
+
* paginada, cacheada com TTL.
|
|
192
|
+
*
|
|
193
|
+
* `fresh: true` = caminho de DECISÃO QUE ESCREVE (gates do implement, escopo do
|
|
194
|
+
* qa-lead run): ignora o cache na leitura, mas grava — o comando seguinte da
|
|
195
|
+
* sessão ganha o snapshot de graça.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} params
|
|
198
|
+
* @param {string} params.token
|
|
199
|
+
* @param {{id:string}} params.project
|
|
200
|
+
* @param {string} params.root
|
|
201
|
+
* @param {boolean} [params.refresh] força reconsulta (flag do usuário)
|
|
202
|
+
* @param {boolean} [params.fresh] exige dado novo (decisão de escrita)
|
|
203
|
+
* @param {number} [params.ttlSec]
|
|
204
|
+
* @returns {Promise<{items:Array, index:Map<number,object>, fetchedAt:string, fromCache:boolean}>}
|
|
205
|
+
*/
|
|
206
|
+
export async function loadBoardSnapshot({
|
|
207
|
+
token, project, root, refresh = false, fresh = false, ttlSec = DEFAULT_CACHE_TTL_SEC,
|
|
208
|
+
} = {}) {
|
|
209
|
+
const key = 'board-items';
|
|
210
|
+
if (!refresh && !fresh) {
|
|
211
|
+
const hit = readCacheEntry(root, key, { projectId: project.id });
|
|
212
|
+
if (hit && isFresh(hit, ttlSec)) {
|
|
213
|
+
return {
|
|
214
|
+
items: hit.data,
|
|
215
|
+
index: indexBoardItems(hit.data),
|
|
216
|
+
fetchedAt: hit.fetchedAt,
|
|
217
|
+
fromCache: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const items = await listProjectItems(token, project.id);
|
|
222
|
+
writeCacheEntry(root, key, 'board-items', items, { projectId: project.id });
|
|
223
|
+
return {
|
|
224
|
+
items,
|
|
225
|
+
index: indexBoardItems(items),
|
|
226
|
+
fetchedAt: new Date().toISOString(),
|
|
227
|
+
fromCache: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Estado (aberta/fechada, título) de uma issue externa — cache `issue-<n>`.
|
|
233
|
+
*
|
|
234
|
+
* Só para EXIBIÇÃO (a lista "Bloqueadas por fora" do order); decisão de escrita
|
|
235
|
+
* nunca passa por aqui.
|
|
236
|
+
*
|
|
237
|
+
* @returns {Promise<{title:string, state:string|null, aberta:boolean, fromCache:boolean}>}
|
|
238
|
+
*/
|
|
239
|
+
export async function externalIssueInfo({
|
|
240
|
+
token, owner, repo, root, number, refresh = false, ttlSec = DEFAULT_CACHE_TTL_SEC,
|
|
241
|
+
} = {}) {
|
|
242
|
+
const key = `issue-${number}`;
|
|
243
|
+
if (!refresh) {
|
|
244
|
+
const hit = readCacheEntry(root, key, { owner, repo });
|
|
245
|
+
if (hit && isFresh(hit, ttlSec)) return { ...hit.data, fromCache: true };
|
|
246
|
+
}
|
|
247
|
+
const issue = await getIssue(token, owner, repo, number).catch(() => null);
|
|
248
|
+
if (!issue) {
|
|
249
|
+
// Falha não entra no cache: "não foi possível ler" com cara de fresco por
|
|
250
|
+
// 10 minutos esconderia uma issue que voltou a ser legível.
|
|
251
|
+
return { title: '(não foi possível ler)', state: null, aberta: true, fromCache: false };
|
|
252
|
+
}
|
|
253
|
+
const data = { title: issue.title, state: issue.state, aberta: issue.state === 'open' };
|
|
254
|
+
writeCacheEntry(root, key, 'issue', data, { owner, repo });
|
|
255
|
+
return { ...data, fromCache: false };
|
|
256
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.32.0",
|
|
5
5
|
"description": "Fluxo spec-driven no GitHub (RFC-001): Projects v2, labels de gatilho, spec/plan gerados por Action, decomposição em duas etapas e implementação orientada a Stories/Tasks.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Astratech",
|
|
@@ -22,6 +22,7 @@ O `implement` empilha os PRs de propósito (cada Story revisável sozinha, diff
|
|
|
22
22
|
## O que saber antes de rodar
|
|
23
23
|
|
|
24
24
|
- **Sem `--yes` nada é mergeado** — a saída é o plano: a fila na ordem, qual base será reapontada, o que já está mergeado.
|
|
25
|
+
- O **plano** calcula a ordem pelas fontes locais (dependency-map.json/decomposition.md/corpo — barato); a **execução com `--yes` reconsulta tudo FRESCO da API**, incluindo o blocked_by nativo: merge de pilha é irreversível e não decide com cache.
|
|
25
26
|
- **PR em rascunho bloqueia o plano inteiro.** Marcar pronto é a revisão humana (e o que dispara o CI) — revise e marque cada PR como pronto antes. O comando não faz isso por você, de propósito.
|
|
26
27
|
- **Rodar de novo retoma.** PR mergeado sai do plano sozinho; uma falha no meio (check pendente, conflito) para a fila com as branches dos dependentes intactas.
|
|
27
28
|
- Story **sem PR** vira aviso, não bloqueio — mas se um PR da fila depende do código dela, o merge leva esse código junto; confira antes de confirmar.
|
|
@@ -11,13 +11,28 @@ allowed-tools:
|
|
|
11
11
|
Comando **local**:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx @spec-wave/cli@latest order <feature>
|
|
15
|
-
npx @spec-wave/cli@latest order
|
|
14
|
+
npx @spec-wave/cli@latest order <feature> # uma Feature
|
|
15
|
+
npx @spec-wave/cli@latest order # o mapa de todas as Features com trabalho
|
|
16
|
+
npx @spec-wave/cli@latest order --milestone v04 # só as Features da milestone
|
|
17
|
+
npx @spec-wave/cli@latest order --json # contrato estável p/ scripts e agentes
|
|
18
|
+
npx @spec-wave/cli@latest order <feature> --sync # blocked_by da API → doc + dependency-map.json
|
|
16
19
|
```
|
|
17
20
|
|
|
18
|
-
| Arg | Descrição |
|
|
21
|
+
| Arg/Flag | Descrição |
|
|
19
22
|
|-----|-----------|
|
|
20
23
|
| `<feature>` | Número da issue da **Feature**, ex.: `12` ou `#12`. Posicional, **opcional**. |
|
|
24
|
+
| `--milestone <ref>` | Filtra o mapa sem argumento pelas Features da milestone (número ou título). |
|
|
25
|
+
| `--json` | Saída JSON estável (sem ANSI) — **prefira-a para consumo programático**, em vez de parsear o texto. |
|
|
26
|
+
| `--remote` | Une também o blocked_by da API (1 chamada por Story) — pega arestas criadas SÓ pela UI. |
|
|
27
|
+
| `--refresh` | Ignora o cache local e reconsulta a API. |
|
|
28
|
+
| `--sync` | Reescreve as linhas `Depende de:` do decomposition.md e regenera o `dependency-map.json` a partir do estado VIVO (body ∪ blocked_by), com commit local. |
|
|
29
|
+
|
|
30
|
+
**De onde vem a informação (e por que é barato):** as arestas saem de fontes **locais** — o
|
|
31
|
+
`docs/features/<slug>/dependency-map.json` (escrito pelo `decompose --apply`), o
|
|
32
|
+
`decomposition.md` aplicado e as linhas `Depende de:` do corpo — zero chamada por Story. A
|
|
33
|
+
Etapa vem de **um** snapshot do board, cacheado por `cache.ttlSec` (default 600s; env
|
|
34
|
+
`SPEC_WAVE_CACHE_TTL`; `0` desliga) com a idade sempre impressa. Um `blocked_by` criado só
|
|
35
|
+
pela UI não está nas fontes locais: a saída avisa, e `--remote`/`--sync` o incluem.
|
|
21
36
|
|
|
22
37
|
**Sem argumento**, o conjunto vem do **board** (não dos arquivos): todas as Features abertas fora de 🎉 Done, com as Stories de todas num **grafo só** e a Feature de cada uma ao lado. É o modo para responder "por onde os devs pegam agora" quando o trabalho está espalhado por várias Features — nesse escopo, dependência entre Features deixa de ser "externa" e entra na ordenação.
|
|
23
38
|
|
|
@@ -25,7 +40,7 @@ npx @spec-wave/cli@latest order # o mapa de todas as Features com tr
|
|
|
25
40
|
|
|
26
41
|
## O que a saída traz
|
|
27
42
|
|
|
28
|
-
- As Stories da Feature em **ordem topológica** pelas dependências —
|
|
43
|
+
- As Stories da Feature em **ordem topológica** pelas dependências — dependency-map.json ∪ decomposition.md ∪ linha `Depende de: #N` do corpo (e, com `--remote`, a relação nativa *blocked by*)
|
|
29
44
|
- A **Etapa atual** de cada Story no board
|
|
30
45
|
- Avisos de **ciclo de dependência** — essas Stories ficam **fora da ordem**; corrija as linhas `Depende de:`
|
|
31
46
|
- Avisos de **dependência fora de ordem** — Story já em 🚧 Desenvolvimento ou além dependendo de outra que não está em 🎉 Done (no modo de uma Feature, também quando a bloqueadora é de **outra** Feature e continua aberta)
|
|
@@ -38,4 +53,5 @@ npx @spec-wave/cli@latest order # o mapa de todas as Features com tr
|
|
|
38
53
|
2. Apresente a ordem ao usuário, marcando o que já está concluído e o que está pendente.
|
|
39
54
|
3. **Se houver ciclo**, isso é bloqueante para a skill **implement** no modo Feature (o comando aborta com exit 1). Ajude a quebrar o ciclo editando as linhas `Depende de:` nos corpos das Stories.
|
|
40
55
|
4. **Se houver dependência fora de ordem**, aponte o risco ao usuário antes de seguir.
|
|
41
|
-
5.
|
|
56
|
+
5. Se o usuário criou/removeu `blocked_by` **pela UI do GitHub**, rode `order <feature> --sync` para gravar isso nos artefatos — as consultas seguintes voltam a ser locais.
|
|
57
|
+
6. Com a ordem clara, siga para a skill **implement**.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-wave-qa
|
|
3
|
+
description: "Use para a etapa 🧪 QA do spec-wave — gerar o plano de QA de uma Feature (label spec-wave:qa → qa-plan.md), revisá-lo, e EXECUTAR os cenários localmente com `spec-wave qa <issue>` (Feature, Story ou Bug). Verde move o board sozinho; vermelho abre um Bug por cenário reprovado. Gatilhos: 'rodar o QA', 'validar a feature 12', 'gerar o plano de QA', 'testar a story 34', 're-testar o cenário 2'."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash(npx @spec-wave/cli@latest qa *)
|
|
6
|
+
- Bash(npx @spec-wave/cli@latest generate-qa-plan *)
|
|
7
|
+
- Bash(npx @spec-wave/cli@latest doctor)
|
|
8
|
+
- Bash(gh issue *)
|
|
9
|
+
- Read
|
|
10
|
+
- Write
|
|
11
|
+
- Edit
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# spec-wave qa — etapa 🧪 QA
|
|
15
|
+
|
|
16
|
+
Duas metades, em ordem obrigatória:
|
|
17
|
+
|
|
18
|
+
1. **Gerar o plano** (label + Action): `spec-wave:qa` na **Feature** → o Action gera `docs/features/<slug>/qa-plan.md`, valida, critica e aplica `spec-wave:qa-ready`.
|
|
19
|
+
2. **Executar** (sempre local): `npx @spec-wave/cli@latest qa <issue>` roda os cenários contra o checkout e emite o veredito.
|
|
20
|
+
|
|
21
|
+
**Nunca rode o `qa` sem a `spec-wave:qa-ready` na Feature** — o comando recusa, e o motivo é de desenho (D-QA3/D-QA4): o veredito **verde avança a Etapa sozinho, sem confirmação humana**, então o portão humano é a **revisão do plano**, antes de executar.
|
|
22
|
+
|
|
23
|
+
## 0. Detecção de configuração (antes de qualquer coisa)
|
|
24
|
+
|
|
25
|
+
Confirme que existe `.spec-wave.json` no repositório (senão → skill **setup**). Para executar de fato, o comando do executor precisa estar em `qa.command` no `.spec-wave.json` (ou na env `SPEC_WAVE_QA_CMD`, que tem precedência):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"qa": {
|
|
30
|
+
"command": "claude -p \"Execute o QA descrito em {contextFile}\"",
|
|
31
|
+
"setup": "npm ci && npm run build",
|
|
32
|
+
"env": { "BASE_URL": "http://localhost:3000" },
|
|
33
|
+
"defaultBugPriority": "P2"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Placeholders: `{contextFile} {qaPlanFile} {specFile} {issue} {type} {title}`. Sem `qa.command`, o comando monta o contexto, imprime como configurar e **não executa** — não é erro.
|
|
39
|
+
|
|
40
|
+
## 1. Gerar o plano (Feature)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
gh issue edit <feature> --add-label "spec-wave:qa"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Só em Feature** (D-QA1: o plano é por Feature, arquivo único, seções `## Cenário N — Story #X`). Numa Story o Action comenta apontando a Feature-pai; num Bug, não se aplica (ver §5).
|
|
47
|
+
- Arquivo **ausente** → gera via IA e publica em Pull Request. Arquivo **presente** → valida + critica **COMO ESTÁ** (edições manuais são preservadas — mesma semântica do decompose). Para regerar do zero: apague o arquivo e reaplique a label.
|
|
48
|
+
- Requer `spec.md` e `plan.md` já na base.
|
|
49
|
+
- Desfechos: limpo → `spec-wave:qa-ready` + comentário com o resumo; grave → `spec-wave:critique-failed`; reprovas repetidas → `spec-wave:needs-human`.
|
|
50
|
+
|
|
51
|
+
## 2. Revisar e editar o qa-plan.md (o portão humano)
|
|
52
|
+
|
|
53
|
+
Leia o plano com o usuário ANTES de executar. Regras do arquivo:
|
|
54
|
+
|
|
55
|
+
- A estrutura é só `## Cenário N — Story #X`; o corpo aceita markdown livre.
|
|
56
|
+
- **A posição manda, não o número escrito** — inserir um cenário no meio sem renumerar funciona.
|
|
57
|
+
- `Story #X` é obrigatório e precisa ser sub-issue da Feature.
|
|
58
|
+
- `**Critério:**` e `**Esperado:**` são obrigatórios; `**Pré-condições:**` e `**Passos:**` opcionais.
|
|
59
|
+
- Depois de editar, reaplique `spec-wave:qa` para uma nova crítica (o arquivo NÃO é regerado).
|
|
60
|
+
|
|
61
|
+
> ⚠️ O slug vem do **título** da Feature. Renomeá-la depois da geração órfã o arquivo — o comando falha citando o slug órfão.
|
|
62
|
+
|
|
63
|
+
## 3. Executar — SEMPRE `--dry-run` primeiro
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx @spec-wave/cli@latest qa <issue> --dry-run # mostra cenários-alvo e o comando; ZERO escrita no GitHub
|
|
67
|
+
npx @spec-wave/cli@latest qa <issue> # executa de fato
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Mostre ao usuário o contexto montado em `.spec-wave/qa-<n>.md` antes de rodar sem `--dry-run`.
|
|
71
|
+
|
|
72
|
+
Alvos: `qa <feature>` roda todos os cenários das Stories ainda sem `qa-approved`; `qa <story>` só os daquela Story; `qa <bug>` o Teste de Regressão do `bug.md`; `--only <n[,m]>` filtra por número posicional.
|
|
73
|
+
|
|
74
|
+
**PROIBIDO corrigir código durante a execução.** QA não conserta: cenário reprovado vira Bug, e alterar o código no meio invalida o veredito. Se você for o executor (via `qa.command`), siga a skill **qa-executor** e as instruções do contexto à risca — um cenário por vez, evidência bruta, `blocked` (nunca `fail`) com o `blockedReason` do enum para o que não pôde rodar.
|
|
75
|
+
|
|
76
|
+
Para validar uma **trilha inteira** (todas as Features de um milestone, em containers paralelos), use a skill **qa-lead** — o `qa` continua sendo a unidade que ela orquestra.
|
|
77
|
+
|
|
78
|
+
## 4. Os três desfechos
|
|
79
|
+
|
|
80
|
+
| Veredito | O que aconteceu | O que fazer |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| ✅ **verde** (todos pass) | `qa-approved` aplicada; Story → 📋 Homologação; Feature move quando TODAS as Stories liberarem; Bug → 🚀 Deploy | Nada — o board já andou. Se a Story tinha **Bug filho aberto**, ela NÃO avança (guarda dura): feche o Bug primeiro. |
|
|
83
|
+
| ❌ **vermelho** (algum fail) | 1 Bug por cenário reprovado (filho da Story dona, ✅ Ready, `bug.md` commitado, `bug-approved` aplicada); item fica em 🧪 QA; exit 1 | Corrija os Bugs (skill **implement**) e re-teste (§6). |
|
|
84
|
+
| ⚪ **inconclusivo** (blocked, sem fail) | Ambiente quebrado não é defeito: nada move, nenhum Bug; exit 1 | Destrave o ambiente (seed, serviço fora do ar) e rode de novo. |
|
|
85
|
+
|
|
86
|
+
## 5. Bug — exceção documentada (§2.1 da spec)
|
|
87
|
+
|
|
88
|
+
O QA de um **Bug** usa a seção `Teste de Regressão` do próprio `bug.md` (não há qa-plan). Verde move o Bug para **🚀 Deploy** (D-QA6 — Bug não passa por Homologação).
|
|
89
|
+
|
|
90
|
+
E quando um cenário reprova, o `bug.md` do Bug novo **é escrito pelo comando, sem IA e sem a label `spec-wave:bug`** — exceção explícita à regra "nunca escreva o bug.md à mão": a reprodução, o esperado/obtido e o teste de regressão já existem e são determinísticos (são o cenário + a saída real). O arquivo é commitado e a `spec-wave:bug-approved` aplicada pelo próprio comando. **Não** aplique `spec-wave:bug` nesses Bugs — regeraria por IA um documento que registra uma execução observada.
|
|
91
|
+
|
|
92
|
+
## 6. Ciclo de re-teste
|
|
93
|
+
|
|
94
|
+
1. Fix do Bug (skill **implement**, PR, merge).
|
|
95
|
+
2. Re-teste só o cenário: `npx @spec-wave/cli@latest qa <story> --only <cenário>`.
|
|
96
|
+
3. O comando NÃO duplica Bug: se o cenário reprovar de novo, ele comenta no Bug existente (marcador `spec-wave:qa-origin`).
|
|
97
|
+
4. Verde com o Bug ainda aberto não avança a Story — feche o Bug (o fix mergeado + regressão verde justificam) e rode o `qa` de novo.
|
|
98
|
+
|
|
99
|
+
## Recusas que você vai encontrar (e o que significam)
|
|
100
|
+
|
|
101
|
+
- Sem `.spec-wave.json` → rode a skill **setup**.
|
|
102
|
+
- `spec-wave:qa` ainda na issue → geração em voo; aguarde o Action.
|
|
103
|
+
- `critique-failed`/`needs-human` → portão humano da crítica; corrija o documento apontado.
|
|
104
|
+
- Feature sem `qa-ready` → gere/critique o plano primeiro (§1).
|
|
105
|
+
- Etapa **anterior** a 🧪 QA → o `qa` não promove item; quem move até QA é o merge (`spec-wave merge` / `run --pr`).
|
|
106
|
+
- Etapa **posterior** → informa e sai 0 (a Etapa nunca retrocede).
|
|
107
|
+
- Nenhum cenário casando com a Story → plano desatualizado (re-decompose criou Stories novas) ou Feature renomeada (slug órfão).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: critique-qa-plan
|
|
3
|
+
action: critique
|
|
4
|
+
description: Critério da crítica adversarial do qa-plan.md contra spec, plan e decomposição, antes da liberação para execução.
|
|
5
|
+
tools: [Read, Glob, Grep]
|
|
6
|
+
maxTurns: 20
|
|
7
|
+
lenses:
|
|
8
|
+
- "cobertura — algum critério de aceite da spec ficou sem cenário correspondente?"
|
|
9
|
+
- "vacuidade — algum cenário passaria mesmo sem a implementação, ou tem esperado não observável?"
|
|
10
|
+
- "fidelidade — algum cenário testa implementação em vez de comportamento, ou contradiz a spec?"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Crítica adversarial do plano de QA
|
|
14
|
+
|
|
15
|
+
Você audita o `qa-plan.md` proposto contra o `spec.md`, o `plan.md` e a decomposição. Seu papel é encontrar problemas, não elogiar.
|
|
16
|
+
|
|
17
|
+
Este portão importa mais que os outros: o veredito **verde da execução avança a Etapa sozinho, sem confirmação humana** (D-QA3). Um plano fraco aprovado aqui vira aprovação automática de trabalho não validado.
|
|
18
|
+
|
|
19
|
+
## O que caracteriza um achado GRAVE
|
|
20
|
+
|
|
21
|
+
- **Critério de aceite da spec sem cenário correspondente** — a lacuna faz a Feature ser aprovada sem aquele comportamento ter sido validado.
|
|
22
|
+
- **Cenário que passaria SEM a implementação** — ex.: "a página carrega", "o comando não dá erro" num fluxo que existia antes da Feature. É o mesmo vício já caçado no bug.md: um teste que não falharia não valida nada.
|
|
23
|
+
- **Resultado esperado não observável** — "verificar se funciona", "conferir se está correto", "validar o comportamento". Esperado sem critério objetivo torna o veredito arbitrário.
|
|
24
|
+
- **Cenário que testa implementação em vez de comportamento** — "a classe PedidoService existe", "a função retorna um array" — em vez do que o usuário/consumidor observa.
|
|
25
|
+
- **Cenário que contradiz a spec** — esperado que inverte uma regra de negócio explícita.
|
|
26
|
+
- **Cenário atribuído à Story errada** — o comportamento validado pertence a outra Story da decomposição (a reprova abriria o Bug no lugar errado).
|
|
27
|
+
|
|
28
|
+
## O que é MENOR
|
|
29
|
+
|
|
30
|
+
Pré-condições vagas mas completáveis, passos que poderiam ser mais concretos, ordem subótima, redundância parcial entre cenários.
|
|
31
|
+
|
|
32
|
+
<!-- requires-tools -->
|
|
33
|
+
## Como verificar
|
|
34
|
+
|
|
35
|
+
Você tem `Read`, `Glob` e `Grep`. Confira os passos contra o repositório real: a rota citada existe? O comando citado existe? Um plano cujos passos apontam para o que não existe produziria só `blocked` — e isso é um achado.
|
|
36
|
+
<!-- /requires-tools -->
|
|
37
|
+
|
|
38
|
+
## Barra de rigor
|
|
39
|
+
|
|
40
|
+
NÃO invente problemas. Um plano enxuto que cobre os critérios de aceite com esperados observáveis é um resultado legítimo e frequente — diga isso. O inverso também vale: "não consegui confirmar" não é aprovação.
|
|
41
|
+
|
|
42
|
+
## Consequência
|
|
43
|
+
|
|
44
|
+
Um achado **grave** aplica `spec-wave:critique-failed` e o plano NÃO é liberado (`spec-wave:qa-ready` não é aplicada): nenhuma execução de QA roda até a correção. Um achado **menor** é comentado e o plano segue. Reserve o grave para o que tornaria o verde automático enganoso.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa-plan
|
|
3
|
+
action: qa
|
|
4
|
+
description: Gera o plano de QA (qa-plan.md) de uma Feature a partir dos critérios de aceite da spec, com um ou mais cenários por Story.
|
|
5
|
+
tools: [Read, Glob, Grep]
|
|
6
|
+
maxTurns: 30
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Plano de QA de uma Feature
|
|
10
|
+
|
|
11
|
+
Você é um analista de QA experiente. A partir da Feature fornecida (spec.md, plan.md e a lista de Stories reais), escreva o **qa-plan.md**: os cenários de validação funcional que um executor vai rodar contra o checkout, um por critério de aceite relevante.
|
|
12
|
+
|
|
13
|
+
## Entrada
|
|
14
|
+
|
|
15
|
+
O payload JSON traz:
|
|
16
|
+
|
|
17
|
+
- `feature` — número e título da issue da Feature
|
|
18
|
+
- `stories` — as Stories REAIS (número da issue, título, corpo). **Todo cenário pertence a exatamente uma destas Stories, pelo número.**
|
|
19
|
+
- `spec` — o conteúdo de `spec.md` (critérios de aceite, fluxos, regras de negócio)
|
|
20
|
+
- `plan` — o conteúdo de `plan.md` (estratégia técnica — útil para saber COMO exercitar: endpoints, comandos, telas)
|
|
21
|
+
|
|
22
|
+
## Formato de saída (obrigatório)
|
|
23
|
+
|
|
24
|
+
Responda APENAS com o documento markdown, neste formato exato:
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
# Plano de QA — [FEATURE] Título da Feature
|
|
28
|
+
<!-- spec-wave:qa-plan v1 issue=NUMERO_DA_FEATURE -->
|
|
29
|
+
|
|
30
|
+
## Cenário 1 — Story #412
|
|
31
|
+
|
|
32
|
+
**Critério:** AC-2 — cliente vê apenas os próprios pedidos
|
|
33
|
+
**Pré-condições:** dois usuários autenticáveis, cada um com ao menos um pedido
|
|
34
|
+
**Passos:**
|
|
35
|
+
1. Autenticar como usuário A
|
|
36
|
+
2. `GET /pedidos`
|
|
37
|
+
**Esperado:** resposta 200 contendo somente pedidos de A; nenhum pedido de B
|
|
38
|
+
|
|
39
|
+
## Cenário 2 — Story #412
|
|
40
|
+
...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Regras do formato:
|
|
44
|
+
|
|
45
|
+
- O título de cada seção é `## Cenário N — Story #X`, onde `#X` é o número REAL de uma Story da lista `stories`. **Nunca invente números de issue.**
|
|
46
|
+
- `**Critério:**` e `**Esperado:**` são obrigatórios em todo cenário. `**Pré-condições:**` e `**Passos:**` são opcionais, mas quase sempre necessários.
|
|
47
|
+
- **Toda Story da lista precisa de ao menos um cenário** — uma Story sem cenário reprova o plano na validação determinística.
|
|
48
|
+
- Escreva em português (pt-BR).
|
|
49
|
+
|
|
50
|
+
## O que faz um cenário BOM
|
|
51
|
+
|
|
52
|
+
- **Deriva de um critério de aceite da spec** — cite-o no campo `**Critério:**` (ex.: "AC-3 — pedido cancelado não aparece na listagem"). Todo critério de aceite relevante da spec deve ter um cenário correspondente.
|
|
53
|
+
- **Resultado esperado OBSERVÁVEL**: um status HTTP, uma saída de comando, um registro no banco, um texto na tela. "Verificar se funciona" e "conferir se está correto" não são resultados — são a ausência de um.
|
|
54
|
+
- **Falharia sem a implementação**: um cenário que passaria num checkout sem a Feature não valida nada. Pense: "se ninguém tivesse implementado isso, este cenário quebraria?"
|
|
55
|
+
- **Testa COMPORTAMENTO, não implementação**: valide o que o usuário/consumidor observa, não a existência de uma classe ou o nome de uma função.
|
|
56
|
+
- **Executável por alguém sem contexto**: passos concretos (comandos, URLs, payloads), pré-condições explícitas (dados de teste, usuários, estado inicial).
|
|
57
|
+
|
|
58
|
+
<!-- requires-tools -->
|
|
59
|
+
## Exploração antes de escrever
|
|
60
|
+
|
|
61
|
+
Você tem `Read`, `Glob` e `Grep` no repositório. Use-os para ancorar os passos no que existe de verdade: os endpoints reais (rotas registradas), os comandos reais (scripts do package.json, Makefile), os seeds/fixtures disponíveis. Um passo `GET /api/pedidos` só vale se essa rota existe com esse nome. Não gaste mais do que o necessário: explore o suficiente para os passos serem executáveis, e então escreva o documento completo.
|
|
62
|
+
<!-- /requires-tools -->
|
|
63
|
+
|
|
64
|
+
## O que NÃO fazer
|
|
65
|
+
|
|
66
|
+
- Não inventar suíte de testes automatizada: o plano descreve validação funcional executável contra o checkout, não `describe/it`.
|
|
67
|
+
- Não cobrir requisitos não-funcionais genéricos (performance, segurança) a menos que a spec traga critério de aceite explícito e testável sobre eles.
|
|
68
|
+
- Não duplicar o mesmo critério em vários cenários — um cenário por comportamento distinto.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-wave-qa-executor
|
|
3
|
+
description: "Use quando você for o agente EXECUTOR de QA do spec-wave — acionado via `qa.command` com um contexto `.spec-wave/qa-<n>.md`. Executa os cenários um a um, registra evidência bruta e grava o veredito em `.spec-wave/qa-result-<n>.json`. NUNCA corrige código nem escreve no GitHub. Gatilhos: 'execute o QA descrito em', contexto de QA do spec-wave, arquivo qa-<n>.md."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash
|
|
6
|
+
- Read
|
|
7
|
+
- Write
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# spec-wave qa-executor — o agente que executa os cenários
|
|
11
|
+
|
|
12
|
+
Você foi acionado pelo `spec-wave qa <n>` (diretamente ou por um container do
|
|
13
|
+
`qa-lead`). Seu trabalho é **executar cenários e registrar o que observou** — o
|
|
14
|
+
veredito volta pelo **arquivo de resultados**, nunca por texto ou exit code.
|
|
15
|
+
|
|
16
|
+
## O que ler
|
|
17
|
+
|
|
18
|
+
1. O **contexto** (`.spec-wave/qa-<n>.md`, indicado no prompt): traz os cenários
|
|
19
|
+
a executar NESTA ORDEM, o setup do ambiente, o caminho do arquivo de
|
|
20
|
+
resultados e os comentários relevantes das issues.
|
|
21
|
+
2. O **plano** (`docs/features/<slug>/qa-plan.md`) e a **spec**
|
|
22
|
+
(`docs/features/<slug>/spec.md`), apontados no contexto — leia-os para
|
|
23
|
+
entender os critérios de aceite antes do primeiro cenário.
|
|
24
|
+
|
|
25
|
+
## Regras de execução (OBRIGATÓRIAS)
|
|
26
|
+
|
|
27
|
+
1. **Um cenário por vez, na ordem do contexto.** Não paralelize, não pule.
|
|
28
|
+
2. **Evidência bruta, sem paráfrase** — o comando executado, a saída relevante e
|
|
29
|
+
o código de status. A evidência de um `fail` vira o `bug.md` do Bug aberto:
|
|
30
|
+
"não funcionou" não reproduz nada; `500 — TypeError: cannot read 'id' of
|
|
31
|
+
undefined` reproduz.
|
|
32
|
+
3. **PROIBIDO corrigir código.** QA não conserta. Alterar qualquer código
|
|
33
|
+
durante a execução **invalida o veredito de todos os cenários da corrida**,
|
|
34
|
+
inclusive os que já passaram.
|
|
35
|
+
4. **PROIBIDO escrever no GitHub.** Não comente, não abra issue, não mova card —
|
|
36
|
+
isso é do comando `qa`, depois de ler seu resultado. Com o `qa-lead`, N
|
|
37
|
+
executores rodam em paralelo: dois agentes escrevendo no board é exatamente o
|
|
38
|
+
que esta regra impede.
|
|
39
|
+
5. **`blocked` nunca é `fail`.** Cenário que não pôde ser executado
|
|
40
|
+
(pré-condição ausente, serviço fora, seed que falhou, dependência não
|
|
41
|
+
entregue) é `blocked` com o `blockedReason` do enum. Marcar como `fail` cria
|
|
42
|
+
um Bug falso e custa investigação de dev.
|
|
43
|
+
|
|
44
|
+
## Como registrar o veredito
|
|
45
|
+
|
|
46
|
+
Ao terminar (ou ao abortar no meio), grave `.spec-wave/qa-result-<n>.json` — o
|
|
47
|
+
caminho exato está no contexto:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"scenarios": [
|
|
52
|
+
{ "cenario": 1, "verdict": "pass", "evidencia": "GET /pedidos → 200; 2 pedidos, ambos de A" },
|
|
53
|
+
{ "cenario": 2, "verdict": "fail", "evidencia": "500 — TypeError: cannot read 'id' of undefined" },
|
|
54
|
+
{ "cenario": 3, "verdict": "blocked", "blockedReason": "massa-de-dados",
|
|
55
|
+
"evidencia": "seed falhou: tabela `pedidos` inexistente" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- **Um objeto por cenário-alvo**, com o número POSICIONAL. Nenhum pode ser
|
|
61
|
+
omitido: se você abortar no meio, os cenários não alcançados entram como
|
|
62
|
+
`blocked` — resultado parcial honesto vale mais que ausência de resultado.
|
|
63
|
+
- `fail` **exige** `evidencia` não vazia.
|
|
64
|
+
- `blocked` **exige** `blockedReason`, um de: `ambiente` · `setup-falhou` ·
|
|
65
|
+
`massa-de-dados` · `dependencia-nao-entregue` · `bloqueado-por-bug` ·
|
|
66
|
+
`credencial` · `outro` (este exige `evidencia` com o motivo em texto livre).
|
|
67
|
+
- A CLI **recusa** o arquivo fora do contrato (schema em
|
|
68
|
+
`protocol/qa-result.v1.json` do pacote `@spec-wave/cli`) — e arquivo ausente é
|
|
69
|
+
falha alta: nada é aprovado nem reprovado sem ele.
|
|
70
|
+
|
|
71
|
+
## Setup do ambiente
|
|
72
|
+
|
|
73
|
+
Se o contexto trouxer uma seção de setup, rode-a **antes do primeiro cenário**.
|
|
74
|
+
Se o setup falhar, TODOS os cenários são `blocked` com `blockedReason:
|
|
75
|
+
setup-falhou` e a saída do erro como evidência — não tente "consertar o
|
|
76
|
+
ambiente" mudando código.
|