@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spec-wave/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
|
-
"src"
|
|
14
|
+
"src",
|
|
15
|
+
"protocol"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
|
-
"test": "node --test test/*.test.mjs"
|
|
18
|
+
"test": "node --test test/*.test.mjs",
|
|
19
|
+
"skill:gen": "node scripts/generate-skill.mjs"
|
|
18
20
|
},
|
|
19
21
|
"engines": {
|
|
20
22
|
"node": ">=20"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/astratech-net-br/spec-wave-cli/blob/main/packages/spec-wave/protocol/qa-result.v1.json",
|
|
4
|
+
"title": "spec-wave qa result v1",
|
|
5
|
+
"description": "Contrato do arquivo `.spec-wave/qa-result-<n>.json`, escrito pelo agente executor e lido por `spec-wave qa <n>`. Formato ADOTADO da implementação existente do comando `qa` (Tarefa Zero da rfc/spec-qa-lead.md): os campos são `cenario`/`evidencia` — não `n`/`evidence` como a §4.2 da spec propunha — estendidos com `blockedReason` (D-QAL6). `version`, `issue`, `planSha` e `headSha` são aceitos e ignorados pela CLI (a integridade do plano é conferida pela própria CLI, que re-lê o qa-plan.md após a execução).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["scenarios"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": { "type": "integer", "const": 1 },
|
|
10
|
+
"issue": { "type": "integer", "minimum": 1 },
|
|
11
|
+
"planSha": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
|
|
12
|
+
"headSha": { "type": "string", "pattern": "^[0-9a-f]{7,40}$" },
|
|
13
|
+
"scenarios": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"minItems": 1,
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"required": ["cenario", "verdict"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"cenario": {
|
|
21
|
+
"type": "integer",
|
|
22
|
+
"minimum": 1,
|
|
23
|
+
"description": "Número POSICIONAL do cenário no escopo da execução."
|
|
24
|
+
},
|
|
25
|
+
"verdict": {
|
|
26
|
+
"enum": ["pass", "fail", "blocked"]
|
|
27
|
+
},
|
|
28
|
+
"evidencia": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Evidência bruta (comando, saída, código de status). OBRIGATÓRIA e não vazia em `fail` (vira o bug.md determinístico) e em `blocked` com `blockedReason: outro`."
|
|
31
|
+
},
|
|
32
|
+
"blockedReason": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"ambiente",
|
|
35
|
+
"setup-falhou",
|
|
36
|
+
"massa-de-dados",
|
|
37
|
+
"dependencia-nao-entregue",
|
|
38
|
+
"bloqueado-por-bug",
|
|
39
|
+
"credencial",
|
|
40
|
+
"outro"
|
|
41
|
+
],
|
|
42
|
+
"description": "OBRIGATÓRIO quando `verdict` é `blocked` (D-QAL6). Proibido nos demais vereditos."
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"allOf": [
|
|
46
|
+
{
|
|
47
|
+
"if": { "properties": { "verdict": { "const": "blocked" } } },
|
|
48
|
+
"then": { "required": ["blockedReason"] }
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"if": { "properties": { "verdict": { "const": "fail" } } },
|
|
52
|
+
"then": { "required": ["evidencia"], "properties": { "evidencia": { "minLength": 1 } } }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"if": { "properties": { "blockedReason": { "const": "outro" } } },
|
|
56
|
+
"then": { "required": ["evidencia"], "properties": { "evidencia": { "minLength": 1 } } }
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/astratech-net-br/spec-wave-cli/blob/main/packages/spec-wave/protocol/qa-trail-report.v1.json",
|
|
4
|
+
"title": "spec-wave qa trail report v1",
|
|
5
|
+
"description": "Contrato do `docs/qa/<slug-milestone>/cycle-<n>/report.json`, escrito por `spec-wave qa-lead run` (rfc/spec-qa-lead.md §4.3). É o que a UI do SpecWave e o `qa-lead report` consomem.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["version", "milestone", "cycle", "features", "totals", "verdict"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": { "type": "integer", "const": 1 },
|
|
10
|
+
"milestone": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["number", "title"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"number": { "type": "integer", "minimum": 1 },
|
|
15
|
+
"title": { "type": "string" }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"cycle": { "type": "integer", "minimum": 1 },
|
|
19
|
+
"startedAt": { "type": ["string", "null"], "format": "date-time" },
|
|
20
|
+
"finishedAt": { "type": ["string", "null"], "format": "date-time" },
|
|
21
|
+
"preflight": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["ok", "checks"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"ok": { "type": "boolean" },
|
|
26
|
+
"checks": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["name", "ok"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"name": { "type": "string" },
|
|
33
|
+
"ok": { "type": "boolean" },
|
|
34
|
+
"detail": { "type": "string" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"features": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"required": ["issue", "title", "verdict", "scenarios"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"issue": { "type": "integer", "minimum": 1 },
|
|
47
|
+
"title": { "type": "string" },
|
|
48
|
+
"verdict": {
|
|
49
|
+
"enum": ["verde", "vermelho", "inconclusivo", "execucao-abortada", "fora-do-ciclo", "ja-aprovada"]
|
|
50
|
+
},
|
|
51
|
+
"scenarios": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"required": ["pass", "fail", "blocked", "total"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"pass": { "type": "integer", "minimum": 0 },
|
|
56
|
+
"fail": { "type": "integer", "minimum": 0 },
|
|
57
|
+
"blocked": { "type": "integer", "minimum": 0 },
|
|
58
|
+
"total": { "type": "integer", "minimum": 0 }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"bugsOpened": { "type": "array", "items": { "type": "integer" } },
|
|
62
|
+
"durationSec": { "type": ["integer", "null"], "minimum": 0 }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"totals": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"required": ["pass", "fail", "blocked", "naoExecutado", "total"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"pass": { "type": "integer", "minimum": 0 },
|
|
71
|
+
"fail": { "type": "integer", "minimum": 0 },
|
|
72
|
+
"blocked": { "type": "integer", "minimum": 0 },
|
|
73
|
+
"naoExecutado": { "type": "integer", "minimum": 0 },
|
|
74
|
+
"total": { "type": "integer", "minimum": 0 }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"blockedByReason": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"propertyNames": {
|
|
80
|
+
"enum": [
|
|
81
|
+
"ambiente",
|
|
82
|
+
"setup-falhou",
|
|
83
|
+
"massa-de-dados",
|
|
84
|
+
"dependencia-nao-entregue",
|
|
85
|
+
"bloqueado-por-bug",
|
|
86
|
+
"credencial",
|
|
87
|
+
"outro"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"additionalProperties": { "type": "integer", "minimum": 0 }
|
|
91
|
+
},
|
|
92
|
+
"bugs": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"required": ["opened", "closedSincePreviousCycle", "openBlocking"],
|
|
95
|
+
"properties": {
|
|
96
|
+
"opened": { "type": "integer", "minimum": 0 },
|
|
97
|
+
"closedSincePreviousCycle": { "type": "integer", "minimum": 0 },
|
|
98
|
+
"openBlocking": { "type": "array", "items": { "type": "integer" } }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"delta": {
|
|
102
|
+
"type": ["object", "null"],
|
|
103
|
+
"properties": {
|
|
104
|
+
"pass": { "type": "integer" },
|
|
105
|
+
"blocked": { "type": "integer" }
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"verdict": { "enum": ["liberada", "nao-liberada", "ciclo-abortado"] },
|
|
109
|
+
"terminationReason": {
|
|
110
|
+
"enum": ["todas-verdes", "sem-mudanca-de-estado", "teto-de-ciclos", "preflight-falhou", null]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -282,7 +282,7 @@ export async function listProjectItems(token, projectId) {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
|
-
content { ... on Issue { number title state id } }
|
|
285
|
+
content { ... on Issue { number title state id milestone { number title } } }
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
}
|
|
@@ -304,6 +304,11 @@ export async function listProjectItems(token, projectId) {
|
|
|
304
304
|
state: issue.state,
|
|
305
305
|
nodeId: issue.id,
|
|
306
306
|
itemId: node.id,
|
|
307
|
+
// Habilita filtrar o snapshot por milestone sem nenhuma chamada extra
|
|
308
|
+
// (`order --milestone`, e o cache do board fica mais rico de graça).
|
|
309
|
+
milestone: issue.milestone
|
|
310
|
+
? { number: issue.milestone.number, title: issue.milestone.title }
|
|
311
|
+
: null,
|
|
307
312
|
fields,
|
|
308
313
|
});
|
|
309
314
|
}
|
package/src/api/github-rest.mjs
CHANGED
|
@@ -307,6 +307,27 @@ export async function listMilestones(token, owner, repo) {
|
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
// Milestone pelo número — o `qa-lead` lê a DESCRIÇÃO atual antes de atualizar
|
|
311
|
+
// o bloco de trilha (substituir o bloco exige conhecer o resto do texto).
|
|
312
|
+
export async function getMilestone(token, owner, repo, milestoneNumber) {
|
|
313
|
+
const octokit = makeOctokit(token);
|
|
314
|
+
const res = await octokit.rest.issues.getMilestone({
|
|
315
|
+
owner, repo, milestone_number: milestoneNumber,
|
|
316
|
+
});
|
|
317
|
+
return res.data;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Atualiza SÓ a descrição do milestone. O chamador é responsável por preservar
|
|
321
|
+
// o que já estava lá (Release Notes etc.) — ver upsertMilestoneBlock: o
|
|
322
|
+
// contrato é substituir um bloco delimitado, nunca a descrição inteira.
|
|
323
|
+
export async function updateMilestoneDescription(token, owner, repo, milestoneNumber, description) {
|
|
324
|
+
const octokit = makeOctokit(token);
|
|
325
|
+
const res = await octokit.rest.issues.updateMilestone({
|
|
326
|
+
owner, repo, milestone_number: milestoneNumber, description,
|
|
327
|
+
});
|
|
328
|
+
return res.data;
|
|
329
|
+
}
|
|
330
|
+
|
|
310
331
|
// Issues de uma milestone (pelo NÚMERO dela), abertas e fechadas.
|
|
311
332
|
//
|
|
312
333
|
// `state: 'all'` de propósito: uma Feature fechada continua contando para o
|
package/src/cli.mjs
CHANGED
|
@@ -151,7 +151,7 @@ export function buildProgram() {
|
|
|
151
151
|
.option('--dry-run', 'Decide e explica sem executar nada')
|
|
152
152
|
.option('--yes', 'Confirma o passo que exige confirmação')
|
|
153
153
|
.option('--apply', 'Autoriza especificamente o decompose-apply (erra se o passo pendente for outro)')
|
|
154
|
-
.option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug')
|
|
154
|
+
.option('--step <nome>', 'Força um passo: spec | plan | critique | validate | decompose | decompose-apply | bug | qa-plan')
|
|
155
155
|
.option('--max-steps <n>', 'Encadeia até N passos (padrão: 1)', '1')
|
|
156
156
|
.option('--force', 'Ignora o portão de label de gatilho pendente')
|
|
157
157
|
.option('--no-remote-check', 'Não consulta o remoto pelos documentos ausentes (offline)')
|
|
@@ -320,12 +320,101 @@ export function buildProgram() {
|
|
|
320
320
|
|
|
321
321
|
program
|
|
322
322
|
.command('qa')
|
|
323
|
-
.description('
|
|
324
|
-
.
|
|
323
|
+
.description('Executa o plano de QA de uma issue (Feature/Story/Bug) localmente; com --pr-number, move a Feature para QA (usado pelo GitHub Action)')
|
|
324
|
+
.argument('[issue]', 'Número da issue (Feature, Story ou Bug), ex.: 12 ou #12')
|
|
325
|
+
.option('--pr-number <n>', 'Modo Action: move a Feature/Bug do PR aprovado ou mergeado para 🧪 QA')
|
|
326
|
+
.option('--only <n[,m]>', 'Executa só o(s) cenário(s) indicado(s) — número POSICIONAL no plano')
|
|
327
|
+
.option('--severity <p>', 'Severidade dos Bugs abertos na reprova: P0, P1, P2 ou P3 (default: qa.defaultBugPriority ou P2)')
|
|
328
|
+
.option('--dry-run', 'Monta o contexto e imprime cenários e comando — ZERO escrita no GitHub')
|
|
329
|
+
.action(async (issueArg, options) => {
|
|
330
|
+
// Dois modos, mutuamente exclusivos: `--pr-number` é a transição Code
|
|
331
|
+
// Review → 🧪 QA (Action); o argumento posicional é a EXECUÇÃO do plano.
|
|
332
|
+
if (issueArg && options.prNumber) {
|
|
333
|
+
console.error('Use OU `qa <issue>` (executa o plano) OU `qa --pr-number <n>` (move o board) — nunca os dois.');
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (!issueArg && !options.prNumber) {
|
|
337
|
+
console.error('Informe a issue (`spec-wave qa <issue>`) ou o PR (`spec-wave qa --pr-number <n>`).');
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
if (options.prNumber) {
|
|
341
|
+
const { qa } = await import('./commands/qa.mjs');
|
|
342
|
+
await qa(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const { qaRun } = await import('./commands/qa-run.mjs');
|
|
346
|
+
await qaRun({ issue: issueArg, ...options })
|
|
347
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
348
|
+
})
|
|
349
|
+
.addHelpText('after', `
|
|
350
|
+
A fase de QA em duas metades (a ordem é obrigatória):
|
|
351
|
+
1. plano label spec-wave:qa na FEATURE → Action gera docs/features/<slug>/qa-plan.md
|
|
352
|
+
(valida + critica; limpo → label spec-wave:qa-ready)
|
|
353
|
+
2. executar qa <issue> — SEMPRE local, contra o checkout. Exige qa-ready na Feature:
|
|
354
|
+
o verde avança a Etapa sozinho, então o portão humano é a revisão do plano.
|
|
355
|
+
|
|
356
|
+
Alvos: qa <feature> (Stories ainda sem qa-approved) · qa <story> · qa <bug> (Teste de Regressão do bug.md).
|
|
357
|
+
|
|
358
|
+
Desfechos:
|
|
359
|
+
verde +qa-approved · Story → 📋 Homologação · Bug → 🚀 Deploy · Feature quando todas liberarem
|
|
360
|
+
(Bug filho aberto segura a Story mesmo verde)
|
|
361
|
+
vermelho 1 Bug por cenário reprovado (filho da Story, ✅ Ready, bug.md commitado) · exit 1
|
|
362
|
+
blocked sem fail = inconclusivo: nada move, nenhum Bug · exit 1
|
|
363
|
+
|
|
364
|
+
Executor: "qa": { "command": "..." } no .spec-wave.json (ou SPEC_WAVE_QA_CMD).
|
|
365
|
+
Placeholders: {contextFile} {qaPlanFile} {specFile} {issue} {type} {title}.
|
|
366
|
+
Re-teste após o fix: qa <story> --only <cenário> (não duplica Bug — comenta no existente).
|
|
367
|
+
|
|
368
|
+
Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
|
|
369
|
+
|
|
370
|
+
program
|
|
371
|
+
.command('qa-lead')
|
|
372
|
+
.description('Orquestra o QA de uma trilha (milestone): prepara os planos (plan), executa o ciclo em containers paralelos (run) e reimprime relatórios (report)')
|
|
373
|
+
.argument('<acao>', 'plan | run | report')
|
|
374
|
+
.argument('[milestone]', 'Milestone da trilha, por número ou título, ex.: 12 ou "v1.4"')
|
|
375
|
+
.option('--watch', 'No plan: aguarda os planos disparados resolverem (qa-ready/critique-failed), com teto qa.lead.planWaitTimeoutMin')
|
|
376
|
+
.option('--only <features>', 'No run: sub-trilha explícita, ex.: --only 318,320 (decisão humana declarada, não relaxamento do portão)')
|
|
377
|
+
.option('--max-cycles <n>', 'No run: teto de ciclos desta trilha (default: qa.lead.maxCycles, 3)')
|
|
378
|
+
.option('--cycle <n>', 'No report: qual ciclo imprimir (default: o último)')
|
|
379
|
+
.option('--dry-run', 'Classifica/planeja e imprime — nenhuma label, zero container, zero escrita')
|
|
380
|
+
.action(async (acao, milestone, options) => {
|
|
381
|
+
const { qaLead } = await import('./commands/qa-lead.mjs');
|
|
382
|
+
await qaLead({ action: acao, milestone, ...options })
|
|
383
|
+
.catch(err => { console.error(err.message); process.exit(1); });
|
|
384
|
+
})
|
|
385
|
+
.addHelpText('after', `
|
|
386
|
+
Trilha = milestone (D-QAL1), na ordem do \`spec-wave order\` sem argumento. Duas fases (D-QAL2):
|
|
387
|
+
plan classifica cada Feature (pronta / portão humano / em voo / sem spec-plan) e aplica
|
|
388
|
+
\`spec-wave:qa\` nas que precisam de plano. PARA no portão humano: revise os qa-plan.md.
|
|
389
|
+
run exige a trilha inteira com \`spec-wave:qa-ready\`. Preflight global → até
|
|
390
|
+
qa.lead.maxParallel containers (um por Feature, backend docker) → coleta os comentários
|
|
391
|
+
de veredito → grava docs/qa/<slug>/cycle-<n>/{report.md,report.json} e o bloco
|
|
392
|
+
delimitado na descrição do milestone. Ciclo N+1 só se um Bug fechou ou um bloqueio
|
|
393
|
+
caiu (D-QAL7); teto de 3 ciclos.
|
|
394
|
+
report reimprime um ciclo já gravado, sem executar nada.
|
|
395
|
+
|
|
396
|
+
O Lead NUNCA aplica label de estado, não move card e não abre issue — isso é do \`qa\` dentro
|
|
397
|
+
dos containers. Configuração: bloco "qa": { "lead": { ... } } no .spec-wave.json (§4.5 da spec);
|
|
398
|
+
\`container.image\` é obrigatória para a fase run.
|
|
399
|
+
|
|
400
|
+
Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
|
|
401
|
+
|
|
402
|
+
program
|
|
403
|
+
.command('generate-qa-plan')
|
|
404
|
+
.description('Gera (ou re-critica) o plano de QA de uma Feature em qa-plan.md — roda no GitHub Action ou localmente')
|
|
405
|
+
.requiredOption('--issue-number <n>', 'Número da issue no GitHub')
|
|
325
406
|
.action(async (options) => {
|
|
326
|
-
const {
|
|
327
|
-
await
|
|
328
|
-
})
|
|
407
|
+
const { generateQaPlan } = await import('./commands/generate-qa-plan.mjs');
|
|
408
|
+
await generateQaPlan(options).catch(err => { console.error(err.message); process.exit(1); });
|
|
409
|
+
})
|
|
410
|
+
.addHelpText('after', `
|
|
411
|
+
Só em FEATURE (o plano é um arquivo por Feature, com seções "## Cenário N — Story #X").
|
|
412
|
+
Arquivo ausente → gera via IA e publica em PR; presente → valida + critica COMO ESTÁ
|
|
413
|
+
(edições manuais preservadas — para regerar do zero, apague o arquivo e reaplique a label).
|
|
414
|
+
Requer spec.md e plan.md na base. Limpo → spec-wave:qa-ready (revise o plano ANTES de
|
|
415
|
+
rodar \`spec-wave qa <issue>\` — o veredito verde avança a Etapa sozinho).
|
|
416
|
+
|
|
417
|
+
Docs: https://astratech-net-br.github.io/spec-wave-cli/guia/qa/`);
|
|
329
418
|
|
|
330
419
|
program
|
|
331
420
|
.command('implement')
|
|
@@ -333,6 +422,7 @@ export function buildProgram() {
|
|
|
333
422
|
.argument('<issue>', 'Número da issue (Feature, Story ou Task), ex.: 12 ou #12')
|
|
334
423
|
.option('--feature-dir <path>', 'Caminho do docs/features/<slug> (sobrescreve a resolução automática)')
|
|
335
424
|
.option('--dry-run', 'Monta o contexto e imprime o comando sem executar o spec-kit')
|
|
425
|
+
.option('--refresh', 'Ignora o cache local (sub-issues/dependências) e reconsulta a API')
|
|
336
426
|
.action(async (issue, options) => {
|
|
337
427
|
const { implement } = await import('./commands/implement.mjs');
|
|
338
428
|
await implement({ issue, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
@@ -342,10 +432,21 @@ export function buildProgram() {
|
|
|
342
432
|
.command('order')
|
|
343
433
|
.description('Ordena as Stories pelas dependências (topológica). Sem argumento, o mapa de todas as Features com trabalho')
|
|
344
434
|
.argument('[feature]', 'Número da issue da Feature, ex.: 12 ou #12. Omitido: todas as Features abertas fora de 🎉 Done')
|
|
345
|
-
.
|
|
435
|
+
.option('--milestone <ref>', 'Só as Features desta milestone (número ou título) — no mapa sem argumento')
|
|
436
|
+
.option('--json', 'Saída em JSON estável (para agentes/scripts), sem ANSI')
|
|
437
|
+
.option('--remote', 'Une também o blocked_by da API (1 chamada por Story) — pega arestas criadas só pela UI')
|
|
438
|
+
.option('--refresh', 'Ignora o cache local e reconsulta a API')
|
|
439
|
+
.option('--sync', 'Grava as dependências VIVAS (body + blocked_by) de volta no decomposition.md e regenera o dependency-map.json')
|
|
440
|
+
.action(async (feature, options) => {
|
|
346
441
|
const { order } = await import('./commands/order.mjs');
|
|
347
|
-
await order({ feature }).catch(err => { console.error(err.message); process.exit(1); });
|
|
348
|
-
})
|
|
442
|
+
await order({ feature, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
443
|
+
})
|
|
444
|
+
.addHelpText('after', `
|
|
445
|
+
As ARESTAS vêm de fontes locais (dependency-map.json do decompose --apply, decomposition.md,
|
|
446
|
+
linhas "Depende de:" do corpo) — zero chamada por Story. A Etapa vem de UM snapshot do board,
|
|
447
|
+
cacheado por ${'`cache.ttlSec`'} (default 600s; SPEC_WAVE_CACHE_TTL na env; 0 desliga) com
|
|
448
|
+
staleness sempre visível. blocked_by criado SÓ pela UI não está nas fontes locais: use --remote
|
|
449
|
+
para incluí-lo na consulta, ou --sync para gravá-lo em definitivo nos artefatos.`);
|
|
349
450
|
|
|
350
451
|
program
|
|
351
452
|
.command('merge')
|
|
@@ -354,6 +455,8 @@ export function buildProgram() {
|
|
|
354
455
|
.option('--yes', 'Executa os merges (sem isso, só mostra o plano)')
|
|
355
456
|
.option('--dry-run', 'Mostra o plano e sai')
|
|
356
457
|
.option('--keep-branches', 'Não apaga as branches das Stories depois do merge')
|
|
458
|
+
.option('--remote', 'No plano: une também o blocked_by da API (a execução com --yes sempre o faz, fresco)')
|
|
459
|
+
.option('--refresh', 'Ignora o cache local e reconsulta a API')
|
|
357
460
|
.action(async (feature, options) => {
|
|
358
461
|
const { merge } = await import('./commands/merge.mjs');
|
|
359
462
|
await merge({ feature, ...options }).catch(err => { console.error(err.message); process.exit(1); });
|
|
@@ -441,6 +544,8 @@ Fluxo típico (cada tema, na ordem):
|
|
|
441
544
|
decompor run <issue> (decompose → --apply) → order <feature>
|
|
442
545
|
implementar implement <issue> · task start/done · story review
|
|
443
546
|
entregar merge <feature> (PRs empilhados, na ordem) · run --pr <n> (board até QA)
|
|
547
|
+
validar (QA) label spec-wave:qa (gera qa-plan.md) → qa <issue> [--only n] (executa e dá o veredito)
|
|
548
|
+
trilha inteira (milestone): qa-lead plan → [revisão humana] → qa-lead run · qa-lead report
|
|
444
549
|
acompanhar info · order · move · repair-stage
|
|
445
550
|
|
|
446
551
|
Docs: https://astratech-net-br.github.io/spec-wave-cli/ · spec-wave <comando> --help`);
|
|
@@ -38,6 +38,8 @@ import { detectIssueType } from '../lib/issue-type.mjs';
|
|
|
38
38
|
import { loadConfig } from '../lib/project-root.mjs';
|
|
39
39
|
import { resolveFlowContext } from '../lib/flow-run.mjs';
|
|
40
40
|
import { publishArtifact } from '../lib/artifact-publish.mjs';
|
|
41
|
+
import { buildDependencyMap, storiesFromAppliedDoc, DEPENDENCY_MAP_FILE } from '../lib/dependency-map.mjs';
|
|
42
|
+
import { invalidateCache } from '../lib/net-cache.mjs';
|
|
41
43
|
import { loadArtifact } from '../lib/doc-source.mjs';
|
|
42
44
|
import { awaitingMergeBlock } from '../lib/artifact-pr.mjs';
|
|
43
45
|
import { isAwaitingMerge } from '../lib/doc-source.mjs';
|
|
@@ -258,14 +260,14 @@ function formatItemsLintWarning(texts) {
|
|
|
258
260
|
* dezenas de itens). Sem o `await`, a promise rejeitada não é capturada por esse
|
|
259
261
|
* catch: o aviso some e vira unhandled rejection.
|
|
260
262
|
*/
|
|
261
|
-
async function publishFile(ctx, { doc, pathRel, content, nextLabel = null }) {
|
|
263
|
+
async function publishFile(ctx, { doc, pathRel, content, nextLabel = null, extraFiles = [] }) {
|
|
262
264
|
const { token, owner, repo, issue, issueNumber, base } = ctx;
|
|
263
265
|
const published = await publishArtifact({
|
|
264
266
|
token, owner, repo, doc,
|
|
265
267
|
issueNumber: parseInt(issueNumber, 10),
|
|
266
268
|
issueTitle: issue?.title || '',
|
|
267
269
|
issueUrl: issue?.html_url || '',
|
|
268
|
-
pathRel, content, base, nextLabel,
|
|
270
|
+
pathRel, content, base, nextLabel, extraFiles,
|
|
269
271
|
});
|
|
270
272
|
if (published.warning) console.warn(`⚠️ ${published.warning}`);
|
|
271
273
|
return published;
|
|
@@ -626,10 +628,30 @@ async function applyDecomposition(ctx) {
|
|
|
626
628
|
// da branch do rascunho: aquela já foi mergeada (é pré-condição do apply), e
|
|
627
629
|
// reabri-la produziria um PR reintroduzindo estado antigo.
|
|
628
630
|
try {
|
|
631
|
+
const appliedAt = new Date().toISOString();
|
|
632
|
+
// O dependency-map.json viaja no MESMO commit da anotação: o apply é o
|
|
633
|
+
// momento em que o grafo nasce pronto em memória — é ele que torna a ordem
|
|
634
|
+
// consultável por qualquer clone SEM pagar a API (lib/dependency-map.mjs).
|
|
635
|
+
const extraFiles = [];
|
|
636
|
+
if (doc.kind === 'stories') {
|
|
637
|
+
const converted = storiesFromAppliedDoc({ ...doc, appliedAt });
|
|
638
|
+
if (converted.ok) {
|
|
639
|
+
const mapRel = docRel.replace(/decomposition\.md$/, DEPENDENCY_MAP_FILE);
|
|
640
|
+
extraFiles.push({
|
|
641
|
+
path: mapRel,
|
|
642
|
+
content: `${JSON.stringify(buildDependencyMap({
|
|
643
|
+
featureNumber: number, stories: converted.stories, source: 'apply', generatedAt: appliedAt,
|
|
644
|
+
}), null, 2)}\n`,
|
|
645
|
+
});
|
|
646
|
+
} else {
|
|
647
|
+
console.warn(`⚠️ dependency-map.json não gerado: ${converted.reason}.`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
629
650
|
const anotado = await publishFile(ctx, {
|
|
630
651
|
doc: 'decomposition-apply',
|
|
631
652
|
pathRel: docRel,
|
|
632
|
-
content: renderDecompositionDoc({ ...doc, appliedAt
|
|
653
|
+
content: renderDecompositionDoc({ ...doc, appliedAt }),
|
|
654
|
+
extraFiles,
|
|
633
655
|
});
|
|
634
656
|
console.log(anotado.pr?.number
|
|
635
657
|
? `${docRel} anotado com as issues criadas — PR #${anotado.pr.number}.`
|
|
@@ -641,6 +663,10 @@ async function applyDecomposition(ctx) {
|
|
|
641
663
|
);
|
|
642
664
|
}
|
|
643
665
|
|
|
666
|
+
// As sub-issues da Feature mudaram — o cache local de quem consultar agora
|
|
667
|
+
// mentiria por até um TTL inteiro.
|
|
668
|
+
invalidateCache(ctx.root, `subissues-${number}`, 'board-items');
|
|
669
|
+
|
|
644
670
|
await addLabel(token, owner, repo, number, LABEL_DECOMPOSED)
|
|
645
671
|
.catch(err => console.warn(`Falha ao aplicar a label ${LABEL_DECOMPOSED}: ${err.message}`));
|
|
646
672
|
// Best-effort como as duas vizinhas: as issues já existem, e derrubar o run
|