@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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/api/github-graphql.mjs +37 -0
  3. package/src/api/github-rest.mjs +48 -0
  4. package/src/cli.mjs +51 -2
  5. package/src/commands/audit.mjs +280 -0
  6. package/src/commands/doctor.mjs +40 -16
  7. package/src/commands/implement.mjs +19 -2
  8. package/src/commands/install-skill.mjs +18 -8
  9. package/src/commands/merge.mjs +292 -0
  10. package/src/commands/move.mjs +26 -11
  11. package/src/commands/order.mjs +42 -0
  12. package/src/commands/preflight.mjs +322 -0
  13. package/src/commands/run.mjs +4 -3
  14. package/src/commands/update.mjs +143 -12
  15. package/src/lib/board.mjs +18 -2
  16. package/src/lib/critique.mjs +64 -8
  17. package/src/lib/pr-branch.mjs +96 -7
  18. package/src/lib/pr-step.mjs +12 -7
  19. package/src/lib/spec-audit.mjs +372 -0
  20. package/src/lib/tech-context.mjs +20 -14
  21. package/src/plugin/.claude-plugin/plugin.json +1 -1
  22. package/src/plugin/README.md +5 -0
  23. package/src/plugin/skills/audit/SKILL.md +34 -0
  24. package/src/plugin/skills/audit/model-prompt.critique.md +34 -0
  25. package/src/plugin/skills/merge/SKILL.md +34 -0
  26. package/src/plugin/skills/order/SKILL.md +1 -0
  27. package/src/plugin/skills/plan/model-prompt.md +1 -0
  28. package/src/plugin/skills/plan/reference/tech-context.md +6 -0
  29. package/src/plugin/skills/preparar-feature/SKILL.md +247 -0
  30. package/src/plugin/skills/preparar-feature/reference/critica.md +88 -0
  31. package/src/plugin/skills/preparar-specs/SKILL.md +191 -0
  32. package/src/plugin/skills/preparar-specs/reference/armadilhas.md +209 -0
  33. package/src/plugin/skills/preparar-specs/reference/revisao.md +110 -0
  34. package/src/plugin/skills/update/SKILL.md +10 -4
  35. package/src/plugin/skills/workflow/SKILL.md +6 -1
  36. package/src/templates/config/tech_context.yml +13 -0
  37. package/src/templates/skill/SKILL.md +36 -7
  38. 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
+ }
@@ -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
- // §4.1 Fonte de verdade estática. Ausência não é erro: segue com {} e avisa.
25
- function readStaticContext(cwd) {
24
+ // Leitura SILENCIOSA da fonte estática para quem consulta (o `audit`
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
- console.warn(`⚠️ Falha ao parsear ${TECH_CONTEXT_PATH}: ${err.message}. Ignorando.`);
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.27.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",
@@ -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`.