@spec-wave/cli 0.15.0 → 0.16.1
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/README.md +1 -0
- package/bin/spec-wave.mjs +44 -5
- package/package.json +8 -2
- package/src/agent/anthropic-agent.mjs +337 -0
- package/src/agent/errors.mjs +33 -0
- package/src/agent/index.mjs +108 -0
- package/src/agent/openrouter-agent.mjs +378 -0
- package/src/agent/run-types.mjs +59 -0
- package/src/agent/telemetry.mjs +54 -0
- package/src/agent/tools.mjs +452 -0
- package/src/agent/tracing.mjs +106 -0
- package/src/api/github-graphql.mjs +23 -1
- package/src/api/github-rest.mjs +8 -0
- package/src/commands/bug.mjs +8 -0
- package/src/commands/code-review.mjs +45 -4
- package/src/commands/decompose.mjs +22 -72
- package/src/commands/dev-agent.mjs +3 -3
- package/src/commands/doctor.mjs +77 -6
- package/src/commands/generate-bug.mjs +195 -0
- package/src/commands/generate-plan.mjs +19 -44
- package/src/commands/generate-spec.mjs +18 -46
- package/src/commands/implement.mjs +105 -2
- package/src/commands/init.mjs +3 -3
- package/src/commands/install-skill.mjs +72 -16
- package/src/commands/issue.mjs +9 -7
- package/src/commands/move.mjs +11 -1
- package/src/commands/qa.mjs +23 -2
- package/src/commands/refresh.mjs +171 -5
- package/src/commands/triage.mjs +174 -0
- package/src/commands/update.mjs +16 -3
- package/src/commands/validate.mjs +82 -10
- package/src/config.mjs +159 -1
- package/src/lib/bug-context.mjs +160 -0
- package/src/lib/bug-doc.mjs +51 -0
- package/src/lib/bug-triage.mjs +81 -0
- package/src/lib/claude.mjs +71 -254
- package/src/lib/critique.mjs +43 -30
- package/src/lib/flow-run.mjs +145 -0
- package/src/lib/implement-board.mjs +12 -1
- package/src/lib/plugin-skills.mjs +122 -0
- package/src/lib/project-root.mjs +9 -2
- package/src/lib/prompt-loader.mjs +257 -0
- package/src/lib/skill-file.mjs +35 -0
- package/src/plugin/.claude-plugin/plugin.json +20 -0
- package/src/plugin/README.md +73 -0
- package/src/plugin/skills/bug/SKILL.md +60 -0
- package/src/plugin/skills/bug/model-prompt.critique.md +48 -0
- package/src/plugin/skills/bug/model-prompt.md +74 -0
- package/src/plugin/skills/decompose/SKILL.md +117 -0
- package/src/plugin/skills/decompose/model-prompt.critique.md +46 -0
- package/src/plugin/skills/decompose/model-prompt.feature.md +69 -0
- package/src/plugin/skills/decompose/model-prompt.rfc.md +52 -0
- package/src/plugin/skills/doctor/SKILL.md +51 -0
- package/src/plugin/skills/fix-pr/SKILL.md +130 -0
- package/src/plugin/skills/implement/SKILL.md +102 -0
- package/src/plugin/skills/info/SKILL.md +40 -0
- package/src/plugin/skills/issue/SKILL.md +63 -0
- package/src/plugin/skills/move/SKILL.md +52 -0
- package/src/plugin/skills/order/SKILL.md +36 -0
- package/src/plugin/skills/plan/SKILL.md +58 -0
- package/src/plugin/skills/plan/model-prompt.critique.md +44 -0
- package/src/plugin/skills/plan/model-prompt.md +59 -0
- package/src/plugin/skills/plan/reference/tech-context.md +56 -0
- package/src/plugin/skills/ready/SKILL.md +44 -0
- package/src/plugin/skills/rfc/SKILL.md +47 -0
- package/src/plugin/skills/setup/SKILL.md +67 -0
- package/src/plugin/skills/spec/SKILL.md +55 -0
- package/src/plugin/skills/spec/model-prompt.md +61 -0
- package/src/plugin/skills/story/SKILL.md +49 -0
- package/src/plugin/skills/task/SKILL.md +41 -0
- package/src/plugin/skills/triage/SKILL.md +52 -0
- package/src/plugin/skills/uninstall/SKILL.md +43 -0
- package/src/plugin/skills/update/SKILL.md +51 -0
- package/src/plugin/skills/workflow/SKILL.md +158 -0
- package/src/templates/skill/SKILL.md +54 -4
- package/src/templates/workflows/generate-bug.yml +36 -0
- package/src/templates/workflows/validate.yml +2 -1
- package/src/ui/wizard.mjs +5 -2
package/src/config.mjs
CHANGED
|
@@ -35,7 +35,7 @@ export const DEFAULT_PROVIDER = 'anthropic';
|
|
|
35
35
|
// Ações de IA que podem ter modelo próprio no .spec-wave.json (bloco
|
|
36
36
|
// `ai.models`, ex.: { "critique": "claude-opus-4-1" }). Resolvidas em runtime
|
|
37
37
|
// por resolveAiConfig() em src/lib/claude.mjs.
|
|
38
|
-
export const AI_ACTIONS = ['spec', 'plan', 'decompose', 'critique'];
|
|
38
|
+
export const AI_ACTIONS = ['spec', 'plan', 'decompose', 'critique', 'bug'];
|
|
39
39
|
|
|
40
40
|
// Override de modelo POR EXECUÇÃO: a label `spec-wave:model:<apelido>` na issue
|
|
41
41
|
// aponta para uma entrada de `ai.modelAliases` do .spec-wave.json. Serve para
|
|
@@ -59,6 +59,12 @@ export function getProvider(value) {
|
|
|
59
59
|
|
|
60
60
|
export const STATUS_OPTIONS = [
|
|
61
61
|
{ name: '📥 Backlog', color: 'GRAY' },
|
|
62
|
+
// 🐞 Triagem é a porta de entrada do Bug reportado (RFC-004 §4). Entra AQUI, e
|
|
63
|
+
// não no fim da lista, porque shouldAdvanceStage compara índices RELATIVOS em
|
|
64
|
+
// STAGE_ORDER: inserir no meio preserva a validade de todo par (atual, destino)
|
|
65
|
+
// que já funcionava, enquanto pôr no fim tornaria "Triagem → qualquer coisa"
|
|
66
|
+
// um retrocesso e travaria o fluxo inteiro do Bug.
|
|
67
|
+
{ name: '🐞 Triagem', color: 'RED' },
|
|
62
68
|
{ name: '🎯 Priorizado', color: 'BLUE' },
|
|
63
69
|
{ name: '📋 Spec', color: 'YELLOW' },
|
|
64
70
|
{ name: '📋 Plan', color: 'YELLOW' },
|
|
@@ -77,14 +83,74 @@ export const STATUS_OPTIONS = [
|
|
|
77
83
|
// • "Status" (campo nativo: Todo/In Progress/Done): o PROGRESSO dentro da etapa
|
|
78
84
|
// atual. Ao avançar de etapa, o Status reinicia em "Todo".
|
|
79
85
|
|
|
86
|
+
// Etapas que JÁ FORAM canônicas e saíram do fluxo. Um board antigo ainda as
|
|
87
|
+
// tem como opção do campo Etapa, e o `.spec-wave.json` gerado na época ainda
|
|
88
|
+
// guarda o id delas. São reportadas pelo doctor à parte das colunas criadas à
|
|
89
|
+
// mão, porque a orientação é outra: aqui a coluna não deve ser adaptada, deve
|
|
90
|
+
// ser esvaziada e removida.
|
|
91
|
+
export const RETIRED_STAGES = [
|
|
92
|
+
{ name: '📋 Backlog Técnico', removedIn: '0.10.0', replacedBy: '✅ Ready' },
|
|
93
|
+
];
|
|
94
|
+
|
|
80
95
|
// Etapas (campo Etapa) referenciadas pelo fluxo de implementação.
|
|
96
|
+
export const STAGE_TRIAGE = STATUS_OPTIONS.find(s => s.name.includes('Triagem')).name;
|
|
81
97
|
export const STAGE_READY = STATUS_OPTIONS.find(s => s.name.includes('Ready')).name;
|
|
82
98
|
export const STAGE_DEVELOPMENT = STATUS_OPTIONS.find(s => s.name.includes('Desenvolvimento')).name;
|
|
83
99
|
export const STAGE_CODE_REVIEW = STATUS_OPTIONS.find(s => s.name.includes('Code Review')).name;
|
|
100
|
+
export const STAGE_QA = STATUS_OPTIONS.find(s => s.name.includes('QA')).name;
|
|
101
|
+
export const STAGE_UAT = STATUS_OPTIONS.find(s => s.name.includes('Homologação')).name;
|
|
102
|
+
export const STAGE_DEPLOY = STATUS_OPTIONS.find(s => s.name.includes('Deploy')).name;
|
|
84
103
|
export const STAGE_DONE = STATUS_OPTIONS.find(s => s.name.includes('Done')).name;
|
|
85
104
|
// Ordem canônica das etapas — usada para garantir que uma issue só AVANÇA.
|
|
86
105
|
export const STAGE_ORDER = STATUS_OPTIONS.map(s => s.name);
|
|
87
106
|
|
|
107
|
+
// Trilha de cada tipo de work item: as etapas que ele DE FATO percorre, na
|
|
108
|
+
// ordem (RFC-001 §4, RFC-004 §4). É documentação executável, não regra dura — a
|
|
109
|
+
// única regra dura do board continua sendo shouldAdvanceStage ("só avança").
|
|
110
|
+
// isStageInTrack serve para AVISAR quem move um item para fora da trilha dele
|
|
111
|
+
// (ex.: um Bug para 📋 Homologação), não para bloquear: bloquear exigiria que
|
|
112
|
+
// todo chamador de advanceToStage soubesse o tipo do item, e dois deles não
|
|
113
|
+
// sabem. Um tipo ausente daqui não tem trilha declarada e nunca gera aviso.
|
|
114
|
+
export const STAGE_TRACKS = {
|
|
115
|
+
Feature: STAGE_ORDER.filter(s => s !== STAGE_TRIAGE),
|
|
116
|
+
Story: [STAGE_READY, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_QA, STAGE_UAT, STAGE_DONE],
|
|
117
|
+
Task: [STAGE_READY, STAGE_DEVELOPMENT, STAGE_DONE],
|
|
118
|
+
Bug: [STAGE_TRIAGE, STAGE_READY, STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, STAGE_QA, STAGE_DEPLOY, STAGE_DONE],
|
|
119
|
+
RFC: [STATUS_OPTIONS[0].name, STAGE_READY, STAGE_DEVELOPMENT, STAGE_DONE],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A etapa em que um item NASCE (função PURA).
|
|
124
|
+
*
|
|
125
|
+
* Quase todo tipo nasce em 📥 Backlog. O Bug é a exceção: nasce em 🐞 Triagem,
|
|
126
|
+
* porque um defeito reportado precisa ser confirmado antes de virar fila. A
|
|
127
|
+
* exceção da exceção é o Bug P0 — a urgência inverte a ordem, ele nasce em
|
|
128
|
+
* ✅ Ready e a triagem é confirmada depois (RFC-004 §4.3).
|
|
129
|
+
*
|
|
130
|
+
* @param {string} type tipo do work item ('Feature', 'Bug', …)
|
|
131
|
+
* @param {string|null} [severity] prioridade, quando já conhecida ('P0'…'P3')
|
|
132
|
+
* @returns {string} nome da etapa inicial
|
|
133
|
+
*/
|
|
134
|
+
export function initialStageForType(type, severity = null) {
|
|
135
|
+
if (type !== 'Bug') return STATUS_OPTIONS[0].name;
|
|
136
|
+
return severity === 'P0' ? STAGE_READY : STAGE_TRIAGE;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A etapa pertence à trilha declarada do tipo? (função PURA)
|
|
141
|
+
*
|
|
142
|
+
* Tipo sem trilha declarada devolve `true` — a ausência de trilha não é motivo
|
|
143
|
+
* para avisar nada.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} type tipo do work item
|
|
146
|
+
* @param {string} stage nome da etapa
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
export function isStageInTrack(type, stage) {
|
|
150
|
+
const track = STAGE_TRACKS[type];
|
|
151
|
+
return track ? track.includes(stage) : true;
|
|
152
|
+
}
|
|
153
|
+
|
|
88
154
|
// Valores do campo nativo "Status" (progresso dentro da etapa).
|
|
89
155
|
export const PROGRESS_TODO = 'Todo';
|
|
90
156
|
export const PROGRESS_IN_PROGRESS = 'In Progress';
|
|
@@ -199,6 +265,70 @@ export const PRIORITY_LABELS = [
|
|
|
199
265
|
export const LABEL_DECOMPOSE = 'spec-wave:decompose';
|
|
200
266
|
export const LABEL_DECOMPOSE_APPLY = 'spec-wave:decompose-apply';
|
|
201
267
|
|
|
268
|
+
// Label de gatilho da fila do dev-agent (o daemon `spec-wave-agent`): aplicá-la
|
|
269
|
+
// numa issue coloca a issue na fila que o agente consulta com `gh issue list`.
|
|
270
|
+
// Precisa estar registrada aqui porque `update` remove TODA label `spec-wave:*`
|
|
271
|
+
// que não esteja em ALL_LABELS — sem esta entrada, um `spec-wave update` de
|
|
272
|
+
// rotina apagava a label da fila e desligava o agente sem aviso.
|
|
273
|
+
export const LABEL_DEV_AGENT = 'spec-wave:dev-agent';
|
|
274
|
+
|
|
275
|
+
// Gatilho e estado do bug.md (RFC-004 §5). O bug.md é o artefato do Bug — leve
|
|
276
|
+
// por decisão: um defeito não gera spec.md + plan.md.
|
|
277
|
+
export const LABEL_BUG = 'spec-wave:bug';
|
|
278
|
+
export const LABEL_BUG_APPROVED = 'spec-wave:bug-approved';
|
|
279
|
+
|
|
280
|
+
// Desfecho da triagem do PM (RFC-004 §4.1). São mutuamente exclusivas: um bug
|
|
281
|
+
// triado foi aceito, rejeitado ou marcado como duplicata.
|
|
282
|
+
export const LABEL_TRIAGED = 'spec-wave:triaged';
|
|
283
|
+
export const LABEL_DUPLICATE = 'spec-wave:duplicate';
|
|
284
|
+
export const LABEL_WONT_FIX = 'spec-wave:wont-fix';
|
|
285
|
+
|
|
286
|
+
// Regressão pós-deploy (origem d): defeito aberto contra uma release entregue.
|
|
287
|
+
export const LABEL_REGRESSION = 'spec-wave:regression';
|
|
288
|
+
|
|
289
|
+
// Origem do defeito — as quatro portas de entrada do RFC-004 §4.2, detalhadas
|
|
290
|
+
// em seis rótulos. É o que torna possível medir DE ONDE vêm os bugs, e portanto
|
|
291
|
+
// qual portão do processo está deixando passar.
|
|
292
|
+
//
|
|
293
|
+
// Vive em label, não em campo do Projects v2, porque SnapshotItem.labels já
|
|
294
|
+
// chega ao client sem mudança nenhuma de contrato — um campo novo exigiria
|
|
295
|
+
// mexer em toSnapshotItem, no ProjectConfig e no refresh de todo repo.
|
|
296
|
+
export const BUG_ORIGINS = ['qa', 'uat', 'support', 'dev', 'review', 'regression'];
|
|
297
|
+
export const BUG_ORIGIN_PREFIX = 'spec-wave:origin:';
|
|
298
|
+
|
|
299
|
+
const BUG_ORIGIN_DESCRIPTIONS = {
|
|
300
|
+
qa: 'Bug encontrado na reprovação de QA',
|
|
301
|
+
uat: 'Bug encontrado na reprovação da Homologação',
|
|
302
|
+
support: 'Bug reportado por suporte ou usuário em produção',
|
|
303
|
+
dev: 'Bug encontrado pelo dev durante o desenvolvimento',
|
|
304
|
+
review: 'Bug encontrado no Code Review',
|
|
305
|
+
regression: 'Regressão detectada após o deploy',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Label de origem a partir do identificador (função PURA).
|
|
310
|
+
*
|
|
311
|
+
* @param {string} origin um de BUG_ORIGINS
|
|
312
|
+
* @returns {string|null} a label, ou null se a origem não existir
|
|
313
|
+
*/
|
|
314
|
+
export function bugOriginLabel(origin) {
|
|
315
|
+
return BUG_ORIGINS.includes(origin) ? `${BUG_ORIGIN_PREFIX}${origin}` : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Origem a partir das labels de uma issue (função PURA).
|
|
320
|
+
*
|
|
321
|
+
* @param {object|Array} issueOrLabels issue ou array de labels
|
|
322
|
+
* @returns {string|null} a origem, ou null quando não há
|
|
323
|
+
*/
|
|
324
|
+
export function bugOriginFromLabels(issueOrLabels) {
|
|
325
|
+
const found = labelNames(issueOrLabels)
|
|
326
|
+
.find(n => n.startsWith(BUG_ORIGIN_PREFIX));
|
|
327
|
+
if (!found) return null;
|
|
328
|
+
const origin = found.slice(BUG_ORIGIN_PREFIX.length);
|
|
329
|
+
return BUG_ORIGINS.includes(origin) ? origin : null;
|
|
330
|
+
}
|
|
331
|
+
|
|
202
332
|
// Labels de estado gravadas pelas automações (não são gatilhos do usuário).
|
|
203
333
|
export const LABEL_CRITIQUE_FAILED = 'spec-wave:critique-failed';
|
|
204
334
|
export const LABEL_DECOMPOSED = 'spec-wave:decomposed';
|
|
@@ -212,6 +342,18 @@ export const TRIGGER_LABELS = [
|
|
|
212
342
|
{ name: 'spec-wave:plan-approved', color: '0E8A16', description: 'Spec+plan validados com sucesso' },
|
|
213
343
|
{ name: LABEL_DECOMPOSE, color: 'BFD4F2', description: 'Gerar/re-criticar o rascunho da decomposição (decomposition.md)' },
|
|
214
344
|
{ name: LABEL_DECOMPOSE_APPLY, color: 'BFD4F2', description: 'Aplicar o decomposition.md revisado: criar Stories e Tasks' },
|
|
345
|
+
{ name: LABEL_DEV_AGENT, color: '5319E7', description: 'Enfileira a issue para o dev-agent autônomo' },
|
|
346
|
+
{ name: LABEL_BUG, color: 'BFD4F2', description: 'Gerar bug.md via GitHub Action' },
|
|
347
|
+
{ name: LABEL_BUG_APPROVED, color: '0E8A16', description: 'bug.md validado (reprodução, causa raiz e teste de regressão)' },
|
|
348
|
+
{ name: LABEL_TRIAGED, color: '0E8A16', description: 'Bug triado pelo PM (severidade, origem e pai definidos)' },
|
|
349
|
+
{ name: LABEL_DUPLICATE, color: 'EDEDED', description: 'Duplicata de outra issue (o corpo aponta qual)' },
|
|
350
|
+
{ name: LABEL_WONT_FIX, color: 'EDEDED', description: 'Rejeitado na triagem — não será corrigido' },
|
|
351
|
+
{ name: LABEL_REGRESSION, color: 'B60205', description: 'Regressão detectada após o deploy' },
|
|
352
|
+
...BUG_ORIGINS.map(o => ({
|
|
353
|
+
name: `${BUG_ORIGIN_PREFIX}${o}`,
|
|
354
|
+
color: 'D93F0B',
|
|
355
|
+
description: BUG_ORIGIN_DESCRIPTIONS[o],
|
|
356
|
+
})),
|
|
215
357
|
{ name: LABEL_DECOMPOSE_READY, color: '0E8A16', description: 'Rascunho de decomposição pronto para revisão humana' },
|
|
216
358
|
{ name: LABEL_CRITIQUE_FAILED, color: 'B60205', description: 'Crítica adversarial apontou contradições graves' },
|
|
217
359
|
{ name: LABEL_NEEDS_HUMAN, color: 'B60205', description: 'Crítica reprovou N vezes seguidas — precisa de revisão humana' },
|
|
@@ -236,6 +378,7 @@ export function labelNames(issueOrLabels) {
|
|
|
236
378
|
}
|
|
237
379
|
|
|
238
380
|
export const WORKFLOW_FILES = [
|
|
381
|
+
'generate-bug.yml',
|
|
239
382
|
'generate-plan.yml',
|
|
240
383
|
'generate-spec.yml',
|
|
241
384
|
'validate.yml',
|
|
@@ -255,6 +398,21 @@ export const REQUIRED_SPEC_SECTIONS = [
|
|
|
255
398
|
'Requisitos Não-Funcionais',
|
|
256
399
|
];
|
|
257
400
|
|
|
401
|
+
// Seções obrigatórias do bug.md (RFC-004 §5). Deliberadamente seis, e leves: o
|
|
402
|
+
// que o corretor precisa saber para reproduzir, achar a causa e provar o fix.
|
|
403
|
+
//
|
|
404
|
+
// ⚠️ validate compara com `content.includes('# ' + secao)` — byte a byte. Nada
|
|
405
|
+
// de caractere exótico aqui (ex.: '×' U+00D7), e o prompt do generate-bug lê
|
|
406
|
+
// ESTA constante para emitir exatamente estes títulos.
|
|
407
|
+
export const REQUIRED_BUG_SECTIONS = [
|
|
408
|
+
'Reprodução',
|
|
409
|
+
'Esperado e Obtido',
|
|
410
|
+
'Impacto e Severidade',
|
|
411
|
+
'Causa Raiz',
|
|
412
|
+
'Escopo do Fix',
|
|
413
|
+
'Teste de Regressão',
|
|
414
|
+
];
|
|
415
|
+
|
|
258
416
|
export const REQUIRED_PLAN_SECTIONS = [
|
|
259
417
|
'Estratégia Técnica',
|
|
260
418
|
'Detalhamento da Implementação',
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Contexto de implementação de um Bug (RFC-004 §7.1) — puro, testável.
|
|
2
|
+
//
|
|
3
|
+
// A diferença para o contexto de Story não é o formato, é a ORDEM DO TRABALHO.
|
|
4
|
+
// Uma Story tem tasks a executar; um Bug tem um defeito a entender antes de
|
|
5
|
+
// tocar em qualquer coisa. Por isso o contexto impõe quatro fases —
|
|
6
|
+
// reproduzir → causa raiz → fix mínimo → teste de regressão — e a primeira
|
|
7
|
+
// entrega é um teste que FALHA.
|
|
8
|
+
//
|
|
9
|
+
// A ordem existe porque a alternativa é o modo de falha clássico da correção
|
|
10
|
+
// assistida: o executor lê o sintoma, encontra o lugar onde ele se manifesta,
|
|
11
|
+
// remenda ali, e o defeito reaparece na próxima entrada.
|
|
12
|
+
|
|
13
|
+
import { STAGE_DEVELOPMENT, STAGE_CODE_REVIEW, PROGRESS_IN_PROGRESS } from '../config.mjs';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Monta o markdown do contexto de um Bug (função PURA).
|
|
17
|
+
*
|
|
18
|
+
* @param {object} params
|
|
19
|
+
* @param {{number:number,title:string,body?:string}} params.bug
|
|
20
|
+
* @param {string|null} [params.bugDoc] conteúdo de docs/bugs/<slug>/bug.md
|
|
21
|
+
* @param {{number:number,title:string,kind:string}|null} [params.parent]
|
|
22
|
+
* @param {Array} [params.comments] grupos de comentários (mesmo shape do implement)
|
|
23
|
+
* @param {string|null} [params.codeDigest]
|
|
24
|
+
* @param {string[]} [params.blockedByWarnings]
|
|
25
|
+
* @param {string|null} [params.severity] P0–P3
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function buildBugContext({
|
|
29
|
+
bug, bugDoc = null, parent = null, comments = [], codeDigest = null,
|
|
30
|
+
blockedByWarnings = [], severity = null,
|
|
31
|
+
}) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
lines.push(`# Contexto de correção — Bug #${bug.number}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push(`**Bug:** ${bug.title}`);
|
|
36
|
+
if (severity) lines.push(`**Severidade:** ${severity}`);
|
|
37
|
+
if (parent) {
|
|
38
|
+
lines.push(`**Item afetado:** ${parent.kind} #${parent.number} — ${parent.title}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (bug.body && bug.body.trim()) {
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('## Relato original');
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push(bug.body.trim());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (blockedByWarnings.length > 0) {
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('## ⚠️ Dependências declaradas');
|
|
51
|
+
for (const w of blockedByWarnings) lines.push(`- ${w}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
lines.push('');
|
|
55
|
+
lines.push('## Como corrigir (quatro fases, nesta ordem)');
|
|
56
|
+
lines.push('');
|
|
57
|
+
lines.push(
|
|
58
|
+
'> **Não comece pelo fix.** O modo de falha desta tarefa é encontrar o lugar onde o ' +
|
|
59
|
+
'sintoma aparece, remendar ali, e o defeito voltar na próxima entrada. As fases 1 e 2 ' +
|
|
60
|
+
'existem para impedir isso.'
|
|
61
|
+
);
|
|
62
|
+
lines.push('');
|
|
63
|
+
lines.push('### 1. Reproduzir');
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(
|
|
66
|
+
'Escreva um teste que **falha** por causa deste defeito, seguindo os passos de reprodução. ' +
|
|
67
|
+
'Rode-o e confirme que falha **pelo motivo certo** — um teste que falha por erro de ' +
|
|
68
|
+
'digitação no próprio teste não reproduz nada. Se não conseguir reproduzir, **pare e ' +
|
|
69
|
+
'reporte**: sem reprodução não há como provar que a correção funcionou.'
|
|
70
|
+
);
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push('### 2. Causa raiz');
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(
|
|
75
|
+
'Investigue até a **origem**, não até o lugar onde o erro se manifesta. Um valor nulo ' +
|
|
76
|
+
'que estoura numa função raramente nasceu ali. Cite arquivo e função. Se o `bug.md` já ' +
|
|
77
|
+
'traz uma causa raiz, **confirme-a contra o código** antes de aceitar — ela foi escrita ' +
|
|
78
|
+
'por outro modelo, sem executar nada.'
|
|
79
|
+
);
|
|
80
|
+
lines.push('');
|
|
81
|
+
lines.push('### 3. Fix mínimo');
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push(
|
|
84
|
+
'Corrija a causa raiz e **apenas ela**. Um bug é o convite mais comum para refatoração ' +
|
|
85
|
+
'oportunista: se você vir outros problemas no caminho, **anote-os no comentário final ' +
|
|
86
|
+
'em vez de corrigi-los** — cada mudança extra aumenta o risco de uma correção que ' +
|
|
87
|
+
'precisava ser cirúrgica.'
|
|
88
|
+
);
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push('### 4. Teste de regressão');
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(
|
|
93
|
+
'O teste da fase 1 agora **passa**. Garanta que ele fica no repositório e que **falharia ' +
|
|
94
|
+
'de novo** se o fix fosse revertido — essa é a única prova de que ele testa o defeito, e ' +
|
|
95
|
+
'não outra coisa. Rode a suíte inteira: um fix que quebra outro teste não está pronto.'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('## Ao terminar');
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push(
|
|
102
|
+
`1. Commit com a **causa raiz** na mensagem: \`fix: <o que estava errado> (#${bug.number})\`, ` +
|
|
103
|
+
'e o corpo explicando a origem — não o sintoma.'
|
|
104
|
+
);
|
|
105
|
+
lines.push(`2. Abra o Pull Request com \`Fixes #${bug.number}\` no corpo.`);
|
|
106
|
+
lines.push(
|
|
107
|
+
`3. O board move sozinho: o Bug sai de **${STAGE_DEVELOPMENT}** (${PROGRESS_IN_PROGRESS}) ` +
|
|
108
|
+
`para **${STAGE_CODE_REVIEW}** quando o PR abre. Não mova à mão.`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (bugDoc && bugDoc.trim()) {
|
|
112
|
+
lines.push('');
|
|
113
|
+
lines.push('---');
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('## bug.md — investigação já registrada');
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push(
|
|
118
|
+
'> Escrito por IA **sem executar código**. Trate a causa raiz como hipótese a confirmar, ' +
|
|
119
|
+
'não como fato.'
|
|
120
|
+
);
|
|
121
|
+
lines.push('');
|
|
122
|
+
lines.push(bugDoc.trim());
|
|
123
|
+
} else {
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push('---');
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push(
|
|
128
|
+
'> **Sem `bug.md`.** A investigação inteira é sua: o relato acima e os comentários são ' +
|
|
129
|
+
'tudo o que existe.'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (comments && comments.length > 0) {
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('## Comentários da issue');
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(
|
|
138
|
+
'> É onde costuma estar o que faltava no relato original — passos extras, ambiente, ' +
|
|
139
|
+
'e o retorno de quem reportou. Em conflito, o comentário mais recente prevalece.'
|
|
140
|
+
);
|
|
141
|
+
for (const group of comments) {
|
|
142
|
+
for (const c of group.items || []) {
|
|
143
|
+
lines.push('');
|
|
144
|
+
lines.push(`**${c.author || 'desconhecido'}** (${c.createdAt}):`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(String(c.body || '').trim());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (codeDigest) {
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push('## Digest do código');
|
|
154
|
+
lines.push('');
|
|
155
|
+
lines.push(codeDigest);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lines.push('');
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Caminhos e checagem estrutural do bug.md — o artefato do Bug (RFC-004 §5).
|
|
2
|
+
//
|
|
3
|
+
// Fica fora de docs/features/ de propósito: um Bug NÃO é uma Feature e não gera
|
|
4
|
+
// spec.md/plan.md. Manter os dois na mesma pasta faria o `validate` e o
|
|
5
|
+
// `resolveFeaturePaths` da UI tropeçarem num diretório sem os arquivos que
|
|
6
|
+
// esperam.
|
|
7
|
+
//
|
|
8
|
+
// Decisão em função pura, I/O no comando — nada aqui toca o filesystem.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { slugify } from './slugify.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Caminhos do bug.md a partir do título da issue (função PURA).
|
|
14
|
+
*
|
|
15
|
+
* Devolve o relativo (links, mensagem de commit) e o absoluto ancorado na raiz
|
|
16
|
+
* do repositório (fs) — o config é procurado subindo na árvore, e os documentos
|
|
17
|
+
* moram junto dele.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} title título da issue (com ou sem o prefixo [BUG])
|
|
20
|
+
* @param {string} [root] raiz do repositório; ausente = process.cwd()
|
|
21
|
+
* @returns {{ slug: string, dirRel: string, fileRel: string, dirAbs: string, fileAbs: string }}
|
|
22
|
+
*/
|
|
23
|
+
export function bugDocPaths(title, root = null) {
|
|
24
|
+
const slug = slugify(title);
|
|
25
|
+
const dirRel = `docs/bugs/${slug}`;
|
|
26
|
+
const fileRel = `${dirRel}/bug.md`;
|
|
27
|
+
const base = root || process.cwd();
|
|
28
|
+
return {
|
|
29
|
+
slug,
|
|
30
|
+
dirRel,
|
|
31
|
+
fileRel,
|
|
32
|
+
dirAbs: path.resolve(base, dirRel),
|
|
33
|
+
fileAbs: path.resolve(base, fileRel),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Seções obrigatórias ausentes de um documento (função PURA).
|
|
39
|
+
*
|
|
40
|
+
* Extraída da duplicação que existia em validate.mjs (um laço idêntico para
|
|
41
|
+
* spec.md e outro para plan.md). Casa por `# <seção>`, o que aceita qualquer
|
|
42
|
+
* nível de heading (`##`, `###`) — a checagem é de PRESENÇA, não de hierarquia.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} content conteúdo do documento
|
|
45
|
+
* @param {string[]} sections títulos obrigatórios
|
|
46
|
+
* @returns {string[]} os que faltam, na ordem declarada
|
|
47
|
+
*/
|
|
48
|
+
export function findMissingSections(content, sections) {
|
|
49
|
+
const text = String(content || '');
|
|
50
|
+
return (sections || []).filter(section => !text.includes(`# ${section}`));
|
|
51
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Decisões da triagem de Bug (RFC-004 §4.1) — puras, testáveis, sem I/O.
|
|
2
|
+
//
|
|
3
|
+
// O portão que estas funções guardam: P2/P3 só entram na fila técnica com o
|
|
4
|
+
// bug.md aprovado. A razão não é burocracia — é que um bug sem causa raiz
|
|
5
|
+
// investigada consome o tempo do dev na investigação que a triagem deveria ter
|
|
6
|
+
// feito, e é aí que "corrigir o sintoma" acontece. P0/P1 são exceção porque
|
|
7
|
+
// esperar o documento custa mais que investigar durante a correção.
|
|
8
|
+
|
|
9
|
+
import { LABEL_BUG_APPROVED, LABEL_NEEDS_HUMAN, LABEL_CRITIQUE_FAILED } from '../config.mjs';
|
|
10
|
+
|
|
11
|
+
export const TRIAGE_ACTIONS = ['accept', 'reject', 'duplicate'];
|
|
12
|
+
|
|
13
|
+
// Severidades que dispensam o bug.md antes da fila.
|
|
14
|
+
const SEVERITY_WITHOUT_DOC = ['P0', 'P1'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a ação de triagem informada (função PURA).
|
|
18
|
+
*
|
|
19
|
+
* Mesmo contrato de resolveStageName/resolveProgressName: devolve
|
|
20
|
+
* `{action, error}` em vez de lançar.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} input
|
|
23
|
+
* @returns {{ action: string|null, error: string|null }}
|
|
24
|
+
*/
|
|
25
|
+
export function resolveTriageAction(input) {
|
|
26
|
+
const key = String(input ?? '').trim().toLowerCase();
|
|
27
|
+
if (!key) {
|
|
28
|
+
return { action: null, error: `Informe a ação: ${TRIAGE_ACTIONS.join(', ')}.` };
|
|
29
|
+
}
|
|
30
|
+
const match = TRIAGE_ACTIONS.find(a => a === key);
|
|
31
|
+
return match
|
|
32
|
+
? { action: match, error: null }
|
|
33
|
+
: {
|
|
34
|
+
action: null,
|
|
35
|
+
error: `Ação "${input}" não existe. Use uma de: ${TRIAGE_ACTIONS.join(', ')}.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* O bug pode ser aceito na fila técnica? (função PURA)
|
|
41
|
+
*
|
|
42
|
+
* @param {object} params
|
|
43
|
+
* @param {string|null} params.severity P0–P3
|
|
44
|
+
* @param {string[]} params.labels labels da issue
|
|
45
|
+
* @returns {{ ok: boolean, error: string|null }}
|
|
46
|
+
*/
|
|
47
|
+
export function canAcceptBug({ severity = null, labels = [] } = {}) {
|
|
48
|
+
const names = labels || [];
|
|
49
|
+
|
|
50
|
+
// Portões humanos da crítica valem para qualquer severidade: aceitar um bug
|
|
51
|
+
// cujo documento foi reprovado é justamente o que o portão existe para evitar.
|
|
52
|
+
if (names.includes(LABEL_NEEDS_HUMAN)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error:
|
|
56
|
+
`A crítica reprovou repetidas vezes e \`${LABEL_NEEDS_HUMAN}\` está aplicada. ` +
|
|
57
|
+
'Revise o bug.md e remova a label antes de aceitar.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (names.includes(LABEL_CRITIQUE_FAILED)) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error:
|
|
64
|
+
`A crítica adversarial apontou problemas graves (\`${LABEL_CRITIQUE_FAILED}\`). ` +
|
|
65
|
+
'Corrija o bug.md e remova a label antes de aceitar.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (SEVERITY_WITHOUT_DOC.includes(severity)) return { ok: true, error: null };
|
|
70
|
+
|
|
71
|
+
if (!names.includes(LABEL_BUG_APPROVED)) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error:
|
|
75
|
+
`Bug ${severity || 'sem severidade'} exige o bug.md validado antes da fila técnica. ` +
|
|
76
|
+
'Aplique `spec-wave:bug` para gerá-lo e `spec-wave:ready` para validá-lo — ' +
|
|
77
|
+
'ou reclassifique a severidade com --severity se for urgente.',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, error: null };
|
|
81
|
+
}
|