@spec-wave/cli 0.27.0 → 0.29.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 +1 -1
- package/src/api/github-graphql.mjs +37 -0
- package/src/api/github-rest.mjs +48 -0
- package/src/cli.mjs +51 -2
- package/src/commands/audit.mjs +280 -0
- package/src/commands/doctor.mjs +40 -16
- package/src/commands/implement.mjs +19 -2
- package/src/commands/install-skill.mjs +18 -8
- package/src/commands/merge.mjs +292 -0
- package/src/commands/move.mjs +26 -11
- package/src/commands/order.mjs +42 -0
- package/src/commands/preflight.mjs +322 -0
- package/src/commands/run.mjs +4 -3
- package/src/commands/update.mjs +143 -12
- package/src/lib/board.mjs +18 -2
- package/src/lib/critique.mjs +64 -8
- package/src/lib/pr-branch.mjs +96 -7
- package/src/lib/pr-step.mjs +12 -7
- package/src/lib/spec-audit.mjs +372 -0
- package/src/lib/tech-context.mjs +20 -14
- package/src/plugin/.claude-plugin/plugin.json +1 -1
- package/src/plugin/README.md +5 -0
- package/src/plugin/skills/audit/SKILL.md +34 -0
- package/src/plugin/skills/audit/model-prompt.critique.md +34 -0
- package/src/plugin/skills/merge/SKILL.md +34 -0
- package/src/plugin/skills/order/SKILL.md +1 -0
- package/src/plugin/skills/plan/model-prompt.md +1 -0
- package/src/plugin/skills/plan/reference/tech-context.md +6 -0
- package/src/plugin/skills/preparar-feature/SKILL.md +247 -0
- package/src/plugin/skills/preparar-feature/reference/critica.md +88 -0
- package/src/plugin/skills/preparar-specs/SKILL.md +191 -0
- package/src/plugin/skills/preparar-specs/reference/armadilhas.md +209 -0
- package/src/plugin/skills/preparar-specs/reference/revisao.md +110 -0
- package/src/plugin/skills/update/SKILL.md +10 -4
- package/src/plugin/skills/workflow/SKILL.md +6 -1
- package/src/templates/config/tech_context.yml +13 -0
- package/src/templates/skill/SKILL.md +36 -7
- package/src/templates/workflows/qa.yml +9 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Auditoria de dependências de uma milestone (módulo puro — sem I/O).
|
|
2
|
+
//
|
|
3
|
+
// A crítica adversarial olha cada documento contra os insumos da MESMA Feature.
|
|
4
|
+
// O que ela estruturalmente não enxerga é o PAR: duas specs declarando depender
|
|
5
|
+
// uma da outra, bloqueante vivendo em milestone posterior à de quem depende
|
|
6
|
+
// dela, dependência que nenhuma Feature do repositório cria. Cada spec, sozinha,
|
|
7
|
+
// está certa — o defeito só existe no conjunto, e `reference/revisao.md` da
|
|
8
|
+
// preparar-specs pedia que um humano lesse as dez specs juntas para achá-lo.
|
|
9
|
+
//
|
|
10
|
+
// Este módulo automatiza a parte DETERMINÍSTICA dessa leitura. O que exige
|
|
11
|
+
// julgamento (a mesma regra contada de dois jeitos) continua fora — é a crítica
|
|
12
|
+
// de conjunto, uma chamada de modelo, não um grep.
|
|
13
|
+
//
|
|
14
|
+
// A seção `# Dependências` da spec é prosa (o template pede só "Internas e
|
|
15
|
+
// Externas"), então a extração é best-effort por três vias: referência `#N`,
|
|
16
|
+
// slug da Feature e título normalizado. O destino é um RELATÓRIO lido por gente
|
|
17
|
+
// (ou por agente) na Fase 3.5 — falso positivo custa uma conferência; falso
|
|
18
|
+
// negativo custa descobrir na implementação.
|
|
19
|
+
|
|
20
|
+
import { slugify } from './slugify.mjs';
|
|
21
|
+
import { orderStories } from './dependencies.mjs';
|
|
22
|
+
|
|
23
|
+
// Minúsculas e sem acento: "Gestão de Crédito" casa com "gestao de credito".
|
|
24
|
+
const norm = (s) => String(s || '')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.normalize('NFD')
|
|
27
|
+
.replace(/[̀-ͯ]/g, '');
|
|
28
|
+
|
|
29
|
+
// Título sem o prefixo [FEATURE]/[BUG]/etc., para casar com a prosa.
|
|
30
|
+
const bareTitle = (title) => String(title || '').replace(/^\s*\[.*?\]\s*/, '').trim();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* O texto da seção `# Dependências` de uma spec (PURA).
|
|
34
|
+
*
|
|
35
|
+
* Do H1 `# Dependências` até o próximo H1 — os `## Internas`/`## Externas`
|
|
36
|
+
* ficam dentro. `null` quando a seção não existe (spec fora do template).
|
|
37
|
+
*
|
|
38
|
+
* @param {string} markdown
|
|
39
|
+
* @returns {string|null}
|
|
40
|
+
*/
|
|
41
|
+
export function dependencySection(markdown = '') {
|
|
42
|
+
const lines = String(markdown).split('\n');
|
|
43
|
+
const start = lines.findIndex(l => /^#\s+depend[êe]ncias\s*$/i.test(norm(l.trim())));
|
|
44
|
+
if (start === -1) return null;
|
|
45
|
+
let end = lines.length;
|
|
46
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
47
|
+
if (/^#\s+\S/.test(lines[i])) { end = i; break; }
|
|
48
|
+
}
|
|
49
|
+
return lines.slice(start + 1, end).join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Todas as referências `#N` de um texto (PURA).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} text
|
|
56
|
+
* @returns {number[]} sem duplicatas, na ordem de aparição
|
|
57
|
+
*/
|
|
58
|
+
export function issueRefs(text = '') {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const m of String(text).matchAll(/#(\d+)\b/g)) {
|
|
61
|
+
const n = parseInt(m[1], 10);
|
|
62
|
+
if (Number.isInteger(n) && n > 0 && !out.includes(n)) out.push(n);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Quais issues do catálogo um texto menciona (PURA).
|
|
69
|
+
*
|
|
70
|
+
* Três vias, da mais precisa à mais frouxa: `#N`, slug, título normalizado.
|
|
71
|
+
* A frouxa existe porque a prosa da spec raramente cita o número — ela diz
|
|
72
|
+
* "depende do wizard de Cadastro de Pedidos".
|
|
73
|
+
*
|
|
74
|
+
* @param {string} text
|
|
75
|
+
* @param {Array<{number:number, title:string, slug?:string}>} catalog
|
|
76
|
+
* @param {number} [selfNumber] a própria Feature nunca é dependência dela mesma
|
|
77
|
+
* @returns {number[]} números do catálogo mencionados
|
|
78
|
+
*/
|
|
79
|
+
export function mentionedIssues(text = '', catalog = [], selfNumber = null) {
|
|
80
|
+
const refs = issueRefs(text);
|
|
81
|
+
const corpo = norm(text);
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const f of catalog) {
|
|
84
|
+
if (!f || f.number === selfNumber) continue;
|
|
85
|
+
const slug = f.slug || slugify(f.title || '');
|
|
86
|
+
const titulo = norm(bareTitle(f.title));
|
|
87
|
+
// As vias frouxas (substring) exigem comprimento: "PIX" ou "x" como título
|
|
88
|
+
// casaria com meia prosa e transformaria tudo em dependência.
|
|
89
|
+
const citada = refs.includes(f.number)
|
|
90
|
+
|| (slug.length >= 4 && corpo.includes(slug))
|
|
91
|
+
|| (titulo.length >= 4 && corpo.includes(titulo));
|
|
92
|
+
if (citada && !out.includes(f.number)) out.push(f.number);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Bullets de dependência que não casam com NADA conhecido (PURA).
|
|
99
|
+
*
|
|
100
|
+
* É o sinal barato do recurso órfão: quatro specs declarando depender de
|
|
101
|
+
* "edição de custo_canal" enquanto nenhuma Feature cria essa edição — cada
|
|
102
|
+
* bullet está certo, e o conjunto não fecha. Sem um registro de recursos
|
|
103
|
+
* (o `criada_por:` do tech_context) não dá para PROVAR a órfã; dá para listar
|
|
104
|
+
* o que não rastreia a Feature nenhuma e deixar a conferência barata.
|
|
105
|
+
*
|
|
106
|
+
* Só a subseção **Internas** entra: dependência Externa (gateway, API de
|
|
107
|
+
* terceiro) não rastreia a Feature nenhuma POR DEFINIÇÃO — flagá-la seria
|
|
108
|
+
* ensinar a ignorar o aviso. Sem subseções, a seção inteira é o fallback.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} section texto da seção Dependências
|
|
111
|
+
* @param {Array<{number:number, title:string, slug?:string}>} catalog
|
|
112
|
+
* @returns {string[]} texto dos bullets sem alvo, aparados
|
|
113
|
+
*/
|
|
114
|
+
export function untraceableBullets(section = '', catalog = []) {
|
|
115
|
+
const lines = String(section).split('\n');
|
|
116
|
+
const começo = lines.findIndex(l => /^#{2,}\s+.*internas/i.test(norm(l)));
|
|
117
|
+
let alvo = lines;
|
|
118
|
+
if (começo !== -1) {
|
|
119
|
+
let fim = lines.length;
|
|
120
|
+
for (let i = começo + 1; i < lines.length; i++) {
|
|
121
|
+
if (/^#{2,}\s+\S/.test(lines[i])) { fim = i; break; }
|
|
122
|
+
}
|
|
123
|
+
alvo = lines.slice(começo + 1, fim);
|
|
124
|
+
}
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const line of alvo) {
|
|
127
|
+
const m = line.match(/^\s*[-*]\s+(.*\S)\s*$/);
|
|
128
|
+
if (!m) continue;
|
|
129
|
+
const bullet = m[1];
|
|
130
|
+
// "Nenhuma"/"N/A" é resposta válida do gerador, não dependência sem alvo.
|
|
131
|
+
if (/^(nenhuma|n\/a|—|-)\.?$/i.test(norm(bullet).trim())) continue;
|
|
132
|
+
if (issueRefs(bullet).length > 0) continue;
|
|
133
|
+
if (mentionedIssues(bullet, catalog).length > 0) continue;
|
|
134
|
+
out.push(bullet.length > 120 ? `${bullet.slice(0, 119)}…` : bullet);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `a` vence DEPOIS de `b`? (PURA)
|
|
141
|
+
*
|
|
142
|
+
* Por data de vencimento quando as duas têm; senão pelo número — milestones
|
|
143
|
+
* nascem na ordem do planejamento, e é o único critério que sobra quando
|
|
144
|
+
* ninguém preencheu as datas.
|
|
145
|
+
*
|
|
146
|
+
* @param {{number?:number, due_on?:string|null}|null} a
|
|
147
|
+
* @param {{number?:number, due_on?:string|null}|null} b
|
|
148
|
+
*/
|
|
149
|
+
export function milestoneAfter(a, b) {
|
|
150
|
+
if (a?.due_on && b?.due_on) return new Date(a.due_on) > new Date(b.due_on);
|
|
151
|
+
return (a?.number ?? 0) > (b?.number ?? 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Palavras do slug que não dizem nada sozinhas.
|
|
155
|
+
const STOPWORDS = new Set([
|
|
156
|
+
'de', 'da', 'do', 'das', 'dos', 'com', 'para', 'por', 'em', 'no', 'na',
|
|
157
|
+
'nos', 'nas', 'e', 'o', 'a', 'os', 'as', 'um', 'uma', 'ao', 'sem', 'sobre',
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Palavras significativas de um slug (PURA). Curtas e vazias ficam de fora.
|
|
162
|
+
*/
|
|
163
|
+
export function slugKeywords(slug = '') {
|
|
164
|
+
return String(slug).split('-').filter(w => w.length >= 4 && !STOPWORDS.has(w));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Arquivos de código cujo caminho casa com o slug da Feature (PURA).
|
|
169
|
+
*
|
|
170
|
+
* A heurística do "isso já não existe?": spec descrevendo o que outra Feature
|
|
171
|
+
* já implementou é retrabalho ou comportamento duplicado em produção. Exige
|
|
172
|
+
* DUAS palavras do slug no mesmo caminho (uma só casa com meio repositório);
|
|
173
|
+
* com o slug de uma palavra útil, uma basta.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} slug
|
|
176
|
+
* @param {string[]} files caminhos relativos do repositório
|
|
177
|
+
* @returns {Array<{path:string, palavras:string[]}>} os 5 mais específicos
|
|
178
|
+
*/
|
|
179
|
+
export function codeOverlap(slug, files = []) {
|
|
180
|
+
const kws = slugKeywords(slug);
|
|
181
|
+
if (kws.length === 0) return [];
|
|
182
|
+
const minimo = Math.min(2, kws.length);
|
|
183
|
+
const hits = [];
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
const nf = norm(f);
|
|
186
|
+
const palavras = kws.filter(k => nf.includes(k));
|
|
187
|
+
if (palavras.length >= minimo) hits.push({ path: f, palavras });
|
|
188
|
+
}
|
|
189
|
+
return hits.sort((x, y) => y.palavras.length - x.palavras.length).slice(0, 5);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Decisões de modelagem sem dono no tech_context (PURA).
|
|
194
|
+
*
|
|
195
|
+
* O tech_context registrava "não existe feature para editar esses valores" e a
|
|
196
|
+
* frase ficou meses lá sem ninguém agir — constatação não vira pendência sem um
|
|
197
|
+
* campo que cobre resposta. O contrato: cada entrada de `decisoes_de_modelagem`
|
|
198
|
+
* declara `criada_por:` com a Feature que cria o recurso, ou `SEM DONO`
|
|
199
|
+
* enquanto ninguém o criar. Este check lista o que está sem resposta; a seção
|
|
200
|
+
* ausente não é achado — a adoção é opcional por repositório.
|
|
201
|
+
*
|
|
202
|
+
* Tolerante ao formato (o YAML é livre): aceita lista de objetos ou mapa
|
|
203
|
+
* recurso→objeto; o rótulo sai de `recurso`/`nome`/`tabela`, da chave do mapa
|
|
204
|
+
* ou da posição.
|
|
205
|
+
*
|
|
206
|
+
* @param {object} staticContext o YAML do tech_context, parseado
|
|
207
|
+
* @returns {Array<{recurso:string, motivo:'sem-campo'|'sem-dono'}>}
|
|
208
|
+
*/
|
|
209
|
+
export function unownedDecisions(staticContext = {}) {
|
|
210
|
+
const secao = staticContext?.decisoes_de_modelagem;
|
|
211
|
+
if (!secao || typeof secao !== 'object') return [];
|
|
212
|
+
const entradas = Array.isArray(secao)
|
|
213
|
+
? secao.map((v, i) => [null, v, i])
|
|
214
|
+
: Object.entries(secao).map(([k, v], i) => [k, v, i]);
|
|
215
|
+
|
|
216
|
+
const out = [];
|
|
217
|
+
for (const [chave, entrada, i] of entradas) {
|
|
218
|
+
if (!entrada || typeof entrada !== 'object') continue;
|
|
219
|
+
const recurso = entrada.recurso || entrada.nome || entrada.tabela || chave || `item ${i + 1}`;
|
|
220
|
+
const dono = String(entrada.criada_por ?? '').trim();
|
|
221
|
+
if (!dono) out.push({ recurso, motivo: 'sem-campo' });
|
|
222
|
+
else if (norm(dono) === 'sem dono') out.push({ recurso, motivo: 'sem-dono' });
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A auditoria inteira (PURA — todo o I/O fica no comando).
|
|
229
|
+
*
|
|
230
|
+
* @param {object} params
|
|
231
|
+
* @param {Array<{number:number, title:string, slug?:string, spec?:string|null,
|
|
232
|
+
* milestone?:{number?:number, title?:string, due_on?:string|null}|null}>}
|
|
233
|
+
* params.features Features ABERTAS da milestone-alvo, com o conteúdo da spec
|
|
234
|
+
* @param {Array<{number:number, title:string, slug?:string, closed?:boolean,
|
|
235
|
+
* milestone?:object|null}>} params.catalog
|
|
236
|
+
* tudo que uma spec pode citar: as Features de TODAS as milestones (e as
|
|
237
|
+
* issues avulsas que o comando resolveu via API)
|
|
238
|
+
* @param {number[]} [params.missingRefs] referências `#N` que a API disse não existir
|
|
239
|
+
* @param {string[]} [params.files] caminhos do repositório para a heurística de código
|
|
240
|
+
* @param {object|null} [params.techContext] o tech_context estático, para as decisões sem dono
|
|
241
|
+
* @returns {{
|
|
242
|
+
* semSpec: number[], semSecao: number[],
|
|
243
|
+
* grafo: Array<{number:number, dependsOn:number[]}>,
|
|
244
|
+
* ciclos: number[],
|
|
245
|
+
* inversoes: Array<{feature:number, dep:number, featureMilestone:string, depMilestone:string}>,
|
|
246
|
+
* bloqueantesSemMilestone: Array<{feature:number, dep:number}>,
|
|
247
|
+
* inexistentes: Array<{feature:number, refs:number[]}>,
|
|
248
|
+
* naoRastreaveis: Array<{feature:number, bullets:string[]}>,
|
|
249
|
+
* sobreposicoes: Array<{feature:number, slug:string, hits:Array<{path:string, palavras:string[]}>}>,
|
|
250
|
+
* }}
|
|
251
|
+
*/
|
|
252
|
+
export function auditMilestone({
|
|
253
|
+
features = [], catalog = [], missingRefs = [], files = [], techContext = null,
|
|
254
|
+
} = {}) {
|
|
255
|
+
const byNumber = new Map(catalog.map(f => [f.number, f]));
|
|
256
|
+
const semSpec = [];
|
|
257
|
+
const semSecao = [];
|
|
258
|
+
const grafo = [];
|
|
259
|
+
const inversoes = [];
|
|
260
|
+
const bloqueantesSemMilestone = [];
|
|
261
|
+
const inexistentes = [];
|
|
262
|
+
const naoRastreaveis = [];
|
|
263
|
+
const sobreposicoes = [];
|
|
264
|
+
|
|
265
|
+
for (const f of features) {
|
|
266
|
+
const slug = f.slug || slugify(f.title || '');
|
|
267
|
+
|
|
268
|
+
const hits = codeOverlap(slug, files);
|
|
269
|
+
if (hits.length > 0) sobreposicoes.push({ feature: f.number, slug, hits });
|
|
270
|
+
|
|
271
|
+
if (!f.spec) { semSpec.push(f.number); continue; }
|
|
272
|
+
const section = dependencySection(f.spec);
|
|
273
|
+
if (section == null) { semSecao.push(f.number); continue; }
|
|
274
|
+
|
|
275
|
+
const deps = mentionedIssues(section, catalog, f.number);
|
|
276
|
+
grafo.push({ number: f.number, dependsOn: deps });
|
|
277
|
+
|
|
278
|
+
const mortas = issueRefs(section).filter(n => missingRefs.includes(n));
|
|
279
|
+
if (mortas.length > 0) inexistentes.push({ feature: f.number, refs: mortas });
|
|
280
|
+
|
|
281
|
+
const bullets = untraceableBullets(section, catalog);
|
|
282
|
+
if (bullets.length > 0) naoRastreaveis.push({ feature: f.number, bullets });
|
|
283
|
+
|
|
284
|
+
for (const d of deps) {
|
|
285
|
+
const dep = byNumber.get(d);
|
|
286
|
+
if (!dep || dep.closed) continue; // entregue: a dependência está satisfeita
|
|
287
|
+
if (!dep.milestone) {
|
|
288
|
+
bloqueantesSemMilestone.push({ feature: f.number, dep: d });
|
|
289
|
+
} else if (milestoneAfter(dep.milestone, f.milestone)) {
|
|
290
|
+
inversoes.push({
|
|
291
|
+
feature: f.number,
|
|
292
|
+
dep: d,
|
|
293
|
+
featureMilestone: f.milestone?.title || `#${f.milestone?.number ?? '?'}`,
|
|
294
|
+
depMilestone: dep.milestone.title || `#${dep.milestone.number}`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Ciclo A↔B entre Features: mesmo Kahn das Stories — dependência para fora do
|
|
301
|
+
// conjunto (outra milestone) vira `external` lá e não entra no ciclo.
|
|
302
|
+
const { cycle } = orderStories(grafo);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
semSpec, semSecao, grafo, ciclos: cycle, inversoes,
|
|
306
|
+
bloqueantesSemMilestone, inexistentes, naoRastreaveis, sobreposicoes,
|
|
307
|
+
semDono: unownedDecisions(techContext || {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* O veredito da auditoria (PURA) — o que bloqueia e o que só avisa.
|
|
313
|
+
*
|
|
314
|
+
* Material é o que muda o desenho ou o sequenciamento se ninguém decidir:
|
|
315
|
+
* ciclo, inversão de milestone, referência a issue que não existe. O resto é
|
|
316
|
+
* conferência barata — a heurística de código e os bullets sem alvo têm falso
|
|
317
|
+
* positivo por construção, e bloquear por eles ensinaria a ignorar o comando.
|
|
318
|
+
*
|
|
319
|
+
* @returns {{ status: 'ok'|'aviso'|'problema', materiais: string[], avisos: string[] }}
|
|
320
|
+
*/
|
|
321
|
+
export function auditVerdict(audit) {
|
|
322
|
+
const materiais = [];
|
|
323
|
+
const avisos = [];
|
|
324
|
+
const lista = (arr) => arr.map(n => `#${n}`).join(', ');
|
|
325
|
+
|
|
326
|
+
if (audit.ciclos.length > 0) {
|
|
327
|
+
materiais.push(`Ciclo de dependência entre Features: ${lista(audit.ciclos)} — nenhuma pode começar primeiro.`);
|
|
328
|
+
}
|
|
329
|
+
for (const i of audit.inversoes) {
|
|
330
|
+
materiais.push(
|
|
331
|
+
`#${i.feature} (${i.featureMilestone}) depende de #${i.dep}, que está em "${i.depMilestone}" — ` +
|
|
332
|
+
'a bloqueante vence DEPOIS de quem depende dela.'
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
for (const i of audit.inexistentes) {
|
|
336
|
+
materiais.push(`#${i.feature} referencia issue(s) inexistente(s): ${lista(i.refs)}.`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (audit.semSpec.length > 0) {
|
|
340
|
+
avisos.push(`Sem spec para auditar: ${lista(audit.semSpec)}.`);
|
|
341
|
+
}
|
|
342
|
+
if (audit.semSecao.length > 0) {
|
|
343
|
+
avisos.push(`Spec sem seção "# Dependências" (fora do template): ${lista(audit.semSecao)}.`);
|
|
344
|
+
}
|
|
345
|
+
for (const b of audit.bloqueantesSemMilestone) {
|
|
346
|
+
avisos.push(`#${b.feature} depende de #${b.dep}, que não está em milestone nenhuma — sem data, não há como sequenciar.`);
|
|
347
|
+
}
|
|
348
|
+
for (const n of audit.naoRastreaveis) {
|
|
349
|
+
avisos.push(
|
|
350
|
+
`#${n.feature} declara ${n.bullets.length} dependência(s) que não rastreiam a nenhuma Feature — ` +
|
|
351
|
+
'possível recurso sem dono (quem cria?).'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
for (const s of audit.sobreposicoes) {
|
|
355
|
+
avisos.push(
|
|
356
|
+
`#${s.feature} ("${s.slug}") tem caminho correspondente no código — confira se já não existe: ` +
|
|
357
|
+
s.hits.map(h => h.path).join(', ')
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
for (const d of audit.semDono || []) {
|
|
361
|
+
avisos.push(
|
|
362
|
+
`tech_context: decisão de modelagem "${d.recurso}" ` +
|
|
363
|
+
(d.motivo === 'sem-dono'
|
|
364
|
+
? 'declarada SEM DONO — decida quem cria antes de planejar a milestone.'
|
|
365
|
+
: 'sem o campo `criada_por:` — declare a Feature que cria o recurso, ou `SEM DONO`.')
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (materiais.length) return { status: 'problema', materiais, avisos };
|
|
370
|
+
if (avisos.length) return { status: 'aviso', materiais, avisos };
|
|
371
|
+
return { status: 'ok', materiais, avisos };
|
|
372
|
+
}
|
package/src/lib/tech-context.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import yaml from 'js-yaml';
|
|
|
8
8
|
// formato do payload RFC §5.2: { static, dynamic, overrides }. Roda no checkout
|
|
9
9
|
// do GitHub Action, então lê tudo do filesystem local (cwd = raiz do repo-alvo).
|
|
10
10
|
|
|
11
|
-
const TECH_CONTEXT_PATH = '.github/config/tech_context.yml';
|
|
11
|
+
export const TECH_CONTEXT_PATH = '.github/config/tech_context.yml';
|
|
12
12
|
|
|
13
13
|
// Diretórios onde migrations costumam viver (várias stacks).
|
|
14
14
|
const MIGRATION_DIRS = [
|
|
@@ -21,25 +21,31 @@ const MIGRATION_DIRS = [
|
|
|
21
21
|
|
|
22
22
|
const MAX_MIGRATIONS = 10;
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
24
|
+
// Leitura SILENCIOSA da fonte estática — para quem só consulta (o `audit` lê
|
|
25
|
+
// as decisões de modelagem e ausência não é problema dele). `context: null`
|
|
26
|
+
// distingue "não existe" de "existe vazio"; `error` só em falha de parse.
|
|
27
|
+
export function loadStaticTechContext(cwd = process.cwd()) {
|
|
26
28
|
const filePath = path.join(cwd, TECH_CONTEXT_PATH);
|
|
27
|
-
if (!existsSync(filePath)) {
|
|
28
|
-
console.warn(
|
|
29
|
-
`⚠️ ${TECH_CONTEXT_PATH} não encontrado. ` +
|
|
30
|
-
`O plano será gerado sem contexto técnico estático. ` +
|
|
31
|
-
`Rode \`spec-wave init\` para gerar o scaffold.`
|
|
32
|
-
);
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
29
|
+
if (!existsSync(filePath)) return { context: null, error: null };
|
|
35
30
|
try {
|
|
36
|
-
return yaml.load(readFileSync(filePath, 'utf-8')) || {};
|
|
31
|
+
return { context: yaml.load(readFileSync(filePath, 'utf-8')) || {}, error: null };
|
|
37
32
|
} catch (err) {
|
|
38
|
-
|
|
39
|
-
return {};
|
|
33
|
+
return { context: null, error: err.message };
|
|
40
34
|
}
|
|
41
35
|
}
|
|
42
36
|
|
|
37
|
+
// §4.1 — Fonte de verdade estática. Ausência não é erro: segue com {} e avisa.
|
|
38
|
+
function readStaticContext(cwd) {
|
|
39
|
+
const { context, error } = loadStaticTechContext(cwd);
|
|
40
|
+
if (context) return context;
|
|
41
|
+
console.warn(error
|
|
42
|
+
? `⚠️ Falha ao parsear ${TECH_CONTEXT_PATH}: ${error}. Ignorando.`
|
|
43
|
+
: `⚠️ ${TECH_CONTEXT_PATH} não encontrado. ` +
|
|
44
|
+
`O plano será gerado sem contexto técnico estático. ` +
|
|
45
|
+
`Rode \`spec-wave init\` para gerar o scaffold.`);
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
// §4.2 — Augmentação dinâmica: migrations recentes + versões exatas de pacotes.
|
|
44
50
|
function readDynamicContext(cwd) {
|
|
45
51
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-wave",
|
|
3
3
|
"displayName": "Spec Wave",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.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",
|
package/src/plugin/README.md
CHANGED
|
@@ -18,6 +18,11 @@ carrega só a sua instrução.
|
|
|
18
18
|
| `plan` | Gera `plan.md` (2º documento) + `tech_context.yml` |
|
|
19
19
|
| `ready` | Valida spec + plan |
|
|
20
20
|
| `decompose` | Rascunho revisável → Stories/Tasks |
|
|
21
|
+
| `preparar-feature` | Orquestra: da spec até ✅ Ready, com Stories e Tasks criadas |
|
|
22
|
+
| `preparar-specs` | Orquestra: as specs de uma milestone inteira |
|
|
23
|
+
| `run` | Executa o próximo passo localmente (`run`/`mode`) |
|
|
24
|
+
| `bug` | Fluxo do Bug: `bug.md`, triagem, correção |
|
|
25
|
+
| `triage` | Tria um Bug: accept / reject / duplicate |
|
|
21
26
|
| `order` | Ordem topológica das Stories |
|
|
22
27
|
| `implement` | Etapa 🚧 Desenvolvimento |
|
|
23
28
|
| `task` | `start` / `done` de uma Task |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-wave-audit
|
|
3
|
+
description: "Use para auditar as specs de uma milestone COMO CONJUNTO, depois de geradas: dependência que ninguém cria, bloqueante em milestone posterior, ciclo entre Features, sobreposição com código já existente — e, com --critique, contradições semânticas entre as specs. Gatilhos: 'audita as specs da v06', 'as dependências da milestone fecham?', 'tem contradição entre as specs?', 'roda a auditoria antes dos planos'. NÃO use para criticar um documento de uma Feature — isso é o passo critique do fluxo normal."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash(npx @spec-wave/cli@latest *)
|
|
6
|
+
- Bash(gh issue *)
|
|
7
|
+
- Read
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# spec-wave audit — o olhar de conjunto da milestone
|
|
11
|
+
|
|
12
|
+
Comando **local**:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @spec-wave/cli@latest audit --milestone <nome> # determinístico
|
|
16
|
+
npx @spec-wave/cli@latest audit --milestone <nome> --critique # + crítica de conjunto (paga UMA chamada de modelo)
|
|
17
|
+
npx @spec-wave/cli@latest audit --milestone <nome> --json # o relatório para ramificar
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A crítica do fluxo normal audita cada documento contra os insumos da **mesma** Feature; este comando cruza as specs da milestone **entre si** — é onde vivem os defeitos que cada spec, sozinha, não mostra.
|
|
21
|
+
|
|
22
|
+
## O que a saída traz
|
|
23
|
+
|
|
24
|
+
- O **grafo de dependências entre Features**, extraído das seções `# Dependências` (por `#N`, slug e título — best-effort sobre prosa)
|
|
25
|
+
- **Materiais** (exit 1): ciclo entre Features, bloqueante em milestone **posterior** à de quem depende dela, referência a issue inexistente
|
|
26
|
+
- **Avisos**: dependência que não rastreia a Feature nenhuma (recurso sem dono — quem cria?), sobreposição do slug com caminhos do código, decisão de modelagem do `tech_context.yml` com `criada_por: SEM DONO`, spec ausente ou fora do template
|
|
27
|
+
- Com `--critique`: os **findings da crítica de conjunto** — uma chamada de modelo sobre todas as specs juntas, procurando a mesma regra contada de dois jeitos. Cada finding cita as Features envolvidas (`#412 × #415`); grave conta como material
|
|
28
|
+
|
|
29
|
+
## Passos
|
|
30
|
+
|
|
31
|
+
1. Rode depois que as specs da milestone existirem (geradas ou em PR — as quatro camadas entram), tipicamente como Fase 3.5 da skill **preparar-specs**.
|
|
32
|
+
2. **Confira os avisos heurísticos antes de reportar** — a extração é sobre prosa livre; falso positivo custa uma leitura, não uma decisão.
|
|
33
|
+
3. Achado material é decisão de PO: comente nas issues **dos dois lados**, apontando uma para a outra, e resolva antes de gerar os planos.
|
|
34
|
+
4. `--critique` paga modelo — em rodadas repetidas, rode o determinístico primeiro e a crítica só quando o conjunto estabilizar.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: critique-conjunto
|
|
3
|
+
action: critique
|
|
4
|
+
description: Critério da crítica adversarial do CONJUNTO de specs de uma milestone — contradição entre documentos, não dentro de um.
|
|
5
|
+
lenses:
|
|
6
|
+
- "regra compartilhada — a mesma regra (teto, prioridade, janela de tempo) contada de formas que não fecham em duas specs?"
|
|
7
|
+
- "dependência — as duas pontas concordam com a direção e o escopo do que uma declara depender da outra?"
|
|
8
|
+
- "afirmação sobre o mundo — uma spec afirma algo ('tal feature não existe', 'isso é feito por X') que outra spec do conjunto desmente?"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Crítica adversarial do conjunto de specs
|
|
12
|
+
|
|
13
|
+
Você audita TODAS as specs de uma milestone, **umas contra as outras**. Cada documento já passou por crítica individual — não repita esse trabalho. Seu papel é achar o que só existe **no par**: cada spec, sozinha, parece certa, e o erro vira dois comportamentos incompatíveis em produção, descobertos na integração.
|
|
14
|
+
|
|
15
|
+
Os checks determinísticos (ciclo de dependência, inversão de milestone, referência inexistente) já rodaram ANTES de você. Não os repita: seu valor é o julgamento semântico que um grep não faz.
|
|
16
|
+
|
|
17
|
+
## O que caracteriza um achado
|
|
18
|
+
|
|
19
|
+
- **Regra compartilhada que não fecha** — um limite, uma ordem de prioridade ou uma janela de tempo que aparece em mais de uma spec com valores, contagens ou marcos de início diferentes.
|
|
20
|
+
- **Dependência em que as pontas discordam** — a spec A declara que seu componente vive na feature B, e a spec de B não menciona esse componente; ou A e B declaram depender uma da outra com escopos que não se encaixam.
|
|
21
|
+
- **Afirmação desmentida pelo conjunto** — uma spec afirma "tal recurso ainda não existe" ou "isso é responsabilidade de X" e outra spec do conjunto mostra o contrário.
|
|
22
|
+
- **Comportamento duplicado** — duas specs descrevendo, com palavras diferentes, a mesma funcionalidade; implementadas as duas, o sistema terá dois caminhos para a mesma coisa.
|
|
23
|
+
|
|
24
|
+
## O que NÃO é achado
|
|
25
|
+
|
|
26
|
+
- Problema interno de uma spec só (regra ambígua, seção fraca, TODO) — isso é da crítica individual, que já rodou.
|
|
27
|
+
- Estilo, redação, nível de detalhe diferente entre specs.
|
|
28
|
+
- Lacuna paramétrica (falta um número, um prazo) — a menos que duas specs declarem **valores conflitantes** para o mesmo parâmetro.
|
|
29
|
+
|
|
30
|
+
## Barra de rigor
|
|
31
|
+
|
|
32
|
+
NÃO invente problemas. Um conjunto consistente é um resultado legítimo e frequente — specs geradas da mesma fonte normativa costumam concordar.
|
|
33
|
+
|
|
34
|
+
Antes de registrar um achado entre duas specs, releia AS DUAS passagens: se elas são compatíveis lidas com atenção (uma detalha, a outra resume), não há achado. Só é "grave" o que, implementado como está escrito, produz comportamentos incompatíveis ou trabalho duplicado.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spec-wave-merge
|
|
3
|
+
description: "Use para mergear os PRs empilhados das Stories de uma Feature do spec-wave, na ordem das dependências, movendo o board até 🧪 QA. Gatilhos: 'mergeia os PRs da feature 12', 'integra as stories', 'os PRs estão revisados, pode mergear', 'qual a ordem de merge?'. Use DEPOIS da revisão humana (PRs marcados como prontos). NÃO mergeie PRs de pilha à mão com --delete-branch — é o que fecha o PR dependente."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash(npx @spec-wave/cli@latest *)
|
|
6
|
+
- Bash(gh pr *)
|
|
7
|
+
- Read
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# spec-wave merge — o passo final, na ordem certa
|
|
11
|
+
|
|
12
|
+
Comando **local** (dentro do Actions o merge é decisão humana):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @spec-wave/cli@latest merge <feature> # mostra o plano e para
|
|
16
|
+
npx @spec-wave/cli@latest merge <feature> --yes # executa
|
|
17
|
+
npx @spec-wave/cli@latest merge <feature> --keep-branches
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
O `implement` empilha os PRs de propósito (cada Story revisável sozinha, diff limpo), mas a pilha torna o merge **ordem-dependente e frágil**: `--delete-branch` no primeiro PR fecha o segundo sem volta. Este comando encapsula a sequência segura — para cada PR na ordem topológica: reaponta a base para a default, mergeia com **merge commit**, atualiza o board (`code-review` + `qa` — merge move até 🧪 QA) e **só no fim** apaga as branches.
|
|
21
|
+
|
|
22
|
+
## O que saber antes de rodar
|
|
23
|
+
|
|
24
|
+
- **Sem `--yes` nada é mergeado** — a saída é o plano: a fila na ordem, qual base será reapontada, o que já está mergeado.
|
|
25
|
+
- **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
|
+
- **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
|
+
- 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.
|
|
28
|
+
|
|
29
|
+
## Passos
|
|
30
|
+
|
|
31
|
+
1. Confirme que os PRs da Feature foram revisados e marcados como prontos (`gh pr ready <n>` se preciso).
|
|
32
|
+
2. Rode sem `--yes` e apresente o plano ao usuário — ordem, retargets, avisos.
|
|
33
|
+
3. Com o ok, rode com `--yes`.
|
|
34
|
+
4. Confira o desfecho na saída: N PRs mergeados, board em 🧪 QA (o próprio comando confirma as escritas do board; se algum falhar, ele indica o `run --pr` de reparo).
|
|
@@ -29,6 +29,7 @@ npx @spec-wave/cli@latest order # o mapa de todas as Features com tr
|
|
|
29
29
|
- A **Etapa atual** de cada Story no board
|
|
30
30
|
- Avisos de **ciclo de dependência** — essas Stories ficam **fora da ordem**; corrija as linhas `Depende de:`
|
|
31
31
|
- 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)
|
|
32
|
+
- Avisos de **milestone divergente** (modo de uma Feature) — Story sem milestone, ou em milestone diferente do da Feature. É a outra órfã do pós-apply, além de `Etapa: —`: a Story some de toda visão de release e ninguém percebe. Sem milestone na Feature, nada é comparado
|
|
32
33
|
- **Bloqueadas por fora desta Feature** (modo de uma Feature) — dependências `#N` que não entram na ordenação porque a Story bloqueadora não está no conjunto, com o estado de cada uma. No modo sem argumento essa seção lista só o que ficou fora do board (Story concluída, Feature em Done, outro board)
|
|
33
34
|
|
|
34
35
|
## Passos
|
|
@@ -54,6 +54,7 @@ O plano deve conter EXATAMENTE estas seções em português, nesta ordem:
|
|
|
54
54
|
|
|
55
55
|
- TODA mudança de banco, endpoint de API ou componente de UI DEVE referenciar um Critério de Aceite específico do spec.md (rastreabilidade).
|
|
56
56
|
- Use APENAS as tecnologias e serviços listados no tech_context fornecido. Não invente APIs ou serviços inexistentes.
|
|
57
|
+
- Se o plano depender de um recurso que `decisoes_de_modelagem` declara com `criada_por: SEM DONO`, registre `[TODO: requer esclarecimento do PO]` apontando que o recurso não tem Feature criadora — não presuma que ele existirá.
|
|
57
58
|
- Forneça detalhes acionáveis: caminhos exatos de endpoints, nomes de DTOs, constraints de banco.
|
|
58
59
|
- Escreva em português (pt-BR). Não use caracteres de outros alfabetos (CJK, cirílico, árabe, tailandês).
|
|
59
60
|
- O arquivo deve conter APENAS o conteúdo do plan.md — nada de preâmbulo, comentário sobre o processo ou resumo do que você fez.
|
|
@@ -43,8 +43,14 @@ O `npx @spec-wave/cli@latest init` gera um **scaffold de exemplo** que **deve se
|
|
|
43
43
|
auth: "<ex.: JWT, mTLS>"
|
|
44
44
|
internal_libraries:
|
|
45
45
|
- "<lib interna>"
|
|
46
|
+
decisoes_de_modelagem: # opcional — recursos COMPARTILHADOS entre features
|
|
47
|
+
- recurso: "<nome>"
|
|
48
|
+
decisao: "<a modelagem decidida>"
|
|
49
|
+
criada_por: "<FT-xx.y | #N | SEM DONO>"
|
|
46
50
|
```
|
|
47
51
|
|
|
52
|
+
Em `decisoes_de_modelagem`, o campo `criada_por:` é **obrigatório por entrada**: é ele que transforma "não existe feature que crie isso" de nota perdida em pendência — `SEM DONO` aparece no `spec-wave audit` como item de checklist antes de planejar a milestone.
|
|
53
|
+
|
|
48
54
|
4. **Mostre o rascunho ao usuário e peça confirmação/ajustes** antes de gravar — ele conhece serviços internos e roles que o código pode não revelar.
|
|
49
55
|
|
|
50
56
|
5. **Grave** com Write em `.github/config/tech_context.yml`.
|