@spec-wave/cli 0.2.2 → 0.4.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/bin/spec-wave.mjs CHANGED
@@ -49,9 +49,9 @@ program
49
49
 
50
50
  program
51
51
  .command('issue')
52
- .description('Cria um work item (epic/feature/story/task...), opcionalmente como sub-issue, e adiciona ao board')
52
+ .description('Cria um work item (initiative/epic/feature/story/task...), opcionalmente como sub-issue, e adiciona ao board')
53
53
  .requiredOption('--title <title>', 'Título (sem o prefixo de tipo, ex.: [FEATURE])')
54
- .option('--type <type>', 'Tipo: epic, feature, story, task, bug, spike ou rfc', 'feature')
54
+ .option('--type <type>', 'Tipo: initiative, epic, feature, story, task, bug, spike ou rfc', 'feature')
55
55
  .option('--parent <n>', 'Número da issue pai (cria como sub-issue dela)')
56
56
  .option('--body <text>', 'Descrição')
57
57
  .option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3')
@@ -61,6 +61,18 @@ program
61
61
  await issue(options).catch(err => { console.error(err.message); process.exit(1); });
62
62
  });
63
63
 
64
+ program
65
+ .command('initiative')
66
+ .description('Atalho de `issue --type initiative` (nó raiz que agrupa Epics)')
67
+ .requiredOption('--title <title>', 'Título da initiative (sem o prefixo [INITIATIVE])')
68
+ .option('--body <text>', 'Descrição da initiative')
69
+ .option('--priority <p>', 'Prioridade: P0, P1, P2 ou P3 (adiciona label)')
70
+ .option('--area <area>', 'Área: Frontend, Backend, Mobile, Infra, DevOps ou Data')
71
+ .action(async (options) => {
72
+ const { initiative } = await import('../src/commands/initiative.mjs');
73
+ await initiative(options).catch(err => { console.error(err.message); process.exit(1); });
74
+ });
75
+
64
76
  program
65
77
  .command('feature')
66
78
  .description('Atalho de `issue --type feature`')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spec-wave/cli",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "Setup spec-driven GitHub workflow with Projects v2, labels, issue templates, and AI-powered Actions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "@octokit/graphql": "^9.0.1",
23
23
  "@octokit/rest": "^22.0.0",
24
24
  "chalk": "^5.4.1",
25
- "commander": "^13.1.0"
25
+ "commander": "^13.1.0",
26
+ "js-yaml": "^4.1.0"
26
27
  }
27
28
  }
@@ -1,24 +1,29 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
3
3
  import { resolveToken } from '../api/auth.mjs';
4
4
  import { getIssue, removeLabel, commentOnIssue } from '../api/github-rest.mjs';
5
5
  import { generateDocument } from '../lib/claude.mjs';
6
6
  import { slugify } from '../lib/slugify.mjs';
7
+ import { buildTechContext } from '../lib/tech-context.mjs';
7
8
 
8
- const SYSTEM_PROMPT = `Você é um Tech Lead experiente. Gere um plano técnico (plan.md) completo e detalhado para a Feature descrita pelo usuário.
9
+ const SYSTEM_PROMPT = `Você é um Tech Lead experiente. Gere um plano técnico (plan.md) completo e detalhado, baseado ESTRITAMENTE no spec.md fornecido.
9
10
 
10
- O plano deve conter exatamente estas seções em português:
11
- # Frontend
12
- # Backend
13
- # Banco de dados
14
- # Infraestrutura
15
- # Segurança
16
- # Testes
17
- # Estimativa (Story Points)
11
+ O plano deve conter EXATAMENTE estas seções em português, nesta ordem:
12
+ # Estratégia Técnica
13
+ - Abordagem Arquitetural, Decisões-Chave e uma Matriz de Rastreabilidade (tabela) ligando cada Critério de Aceite do spec a um componente técnico.
14
+ # Detalhamento da Implementação
15
+ - Subseções: ## Backend, ## Banco de Dados, ## Frontend, ## Infraestrutura.
16
+ # Segurança e Conformidade
17
+ # Estratégia de Testes
18
+ - Unitários, Integração e E2E.
19
+ # Rollback e Monitoramento
20
+ - Plano de Rollback, Métricas Observadas e Alertas.
18
21
 
19
- Para cada seção, forneça detalhes técnicos concretos e acionáveis baseados na descrição da Feature.
20
- A estimativa de Story Points deve usar a sequência de Fibonacci: 1, 2, 3, 5, 8, 13, 21.
21
- Responda APENAS com o conteúdo do plan.md, sem texto adicional.`;
22
+ Regras OBRIGATÓRIAS:
23
+ - 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).
24
+ - Use APENAS as tecnologias e serviços listados no tech_context fornecido. Não invente APIs ou serviços inexistentes.
25
+ - Forneça detalhes acionáveis: caminhos exatos de endpoints, nomes de DTOs, constraints de banco.
26
+ - Responda APENAS com o conteúdo do plan.md, sem texto adicional.`;
22
27
 
23
28
  export async function generatePlan({ issueNumber }) {
24
29
  const token = await resolveToken();
@@ -38,11 +43,28 @@ export async function generatePlan({ issueNumber }) {
38
43
  const featureDir = `docs/features/${slug}`;
39
44
  const filePath = `${featureDir}/plan.md`;
40
45
 
46
+ // Read existing spec.md if available (spec é gerada antes do plano)
47
+ const specPath = `${featureDir}/spec.md`;
48
+ const specContent = existsSync(specPath) ? readFileSync(specPath, 'utf-8') : null;
49
+
50
+ // Tech context (RFC-002 §4): estático + dinâmico + override do corpo da issue.
51
+ const tech = buildTechContext({ issueBody: issue.body || '' });
52
+
53
+ // Payload estruturado (RFC-002 §5.2): spec_content + tech_context.
54
+ const payload = {
55
+ spec_content: specContent || '(spec.md ainda não gerado — baseie-se na descrição da Feature)',
56
+ feature_title: issue.title,
57
+ feature_description: issue.body || '(sem descrição)',
58
+ tech_context: {
59
+ static: tech.merged,
60
+ dynamic: tech.dynamic,
61
+ overrides: tech.overrides,
62
+ },
63
+ };
64
+ const userContent = `Gere o plan.md a partir deste payload JSON:\n\n${JSON.stringify(payload, null, 2)}`;
65
+
41
66
  console.log(`Gerando plan.md para: ${issue.title}`);
42
- const content = await generateDocument(
43
- SYSTEM_PROMPT,
44
- `Feature: ${issue.title}\n\nDescrição:\n${issue.body || '(sem descrição)'}`
45
- );
67
+ const content = await generateDocument(SYSTEM_PROMPT, userContent);
46
68
 
47
69
  mkdirSync(featureDir, { recursive: true });
48
70
  writeFileSync(filePath, content, 'utf-8');
@@ -63,8 +85,8 @@ export async function generatePlan({ issueNumber }) {
63
85
  token, owner, repo, parseInt(issueNumber, 10),
64
86
  `📋 **plan.md gerado automaticamente!**\n\n` +
65
87
  `📄 Arquivo: [\`${filePath}\`](${filePath})\n\n` +
66
- `Revise o plano e, quando estiver pronto, mova o card para a coluna **📋 Spec** ou use:\n` +
67
- `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:spec"\n\`\`\``
88
+ `Revise o plano e, quando estiver pronto, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
89
+ `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\``
68
90
  );
69
91
 
70
92
  console.log(`plan.md criado em: ${filePath}`);
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { resolveToken } from '../api/auth.mjs';
4
4
  import { getIssue, removeLabel, commentOnIssue } from '../api/github-rest.mjs';
5
5
  import { generateDocument } from '../lib/claude.mjs';
@@ -7,17 +7,23 @@ import { slugify } from '../lib/slugify.mjs';
7
7
 
8
8
  const SYSTEM_PROMPT = `Você é um Product Manager experiente. Gere uma especificação funcional (spec.md) completa para a Feature descrita pelo usuário.
9
9
 
10
- O spec deve conter exatamente estas seções em português:
11
- # Objetivo
10
+ O spec deve conter EXATAMENTE estas seções em português, nesta ordem:
11
+ # Visão Geral
12
+ - Objetivo, Personas e Critérios de Sucesso como bullets.
12
13
  # Regras de Negócio
13
14
  # Fluxos
15
+ - Subseções: ## Fluxo Principal (Happy Path), ## Fluxos Alternativos, ## Cenários de Erro.
14
16
  # Critérios de Aceite
15
- # Casos de Erro
17
+ - OBRIGATORIAMENTE no formato Gherkin, dentro de um bloco \`\`\`gherkin com Given/When/Then. Um cenário por critério.
16
18
  # Dependências
19
+ - Subdivida em Internas e Externas.
20
+ # Requisitos Não-Funcionais
21
+ - Performance, Segurança e Usabilidade.
17
22
 
18
- Para cada seção, seja específico e detalhado. Os Critérios de Aceite devem estar no formato de checklist markdown (- [ ] item).
19
- Se um plano técnico (plan.md) for fornecido, use-o para enriquecer os detalhes técnicos relevantes.
20
- Responda APENAS com o conteúdo do spec.md, sem texto adicional.`;
23
+ Regras:
24
+ - NÃO invente regras de negócio. Se faltar informação, marque explicitamente com "[TODO: requer esclarecimento do PO]".
25
+ - Seja específico e detalhado em cada seção.
26
+ - Responda APENAS com o conteúdo do spec.md, sem texto adicional.`;
21
27
 
22
28
  export async function generateSpec({ issueNumber }) {
23
29
  const token = await resolveToken();
@@ -37,15 +43,18 @@ export async function generateSpec({ issueNumber }) {
37
43
  const featureDir = `docs/features/${slug}`;
38
44
  const filePath = `${featureDir}/spec.md`;
39
45
 
40
- // Read existing plan.md if available
41
- const planPath = `${featureDir}/plan.md`;
42
- const planContent = existsSync(planPath) ? readFileSync(planPath, 'utf-8') : null;
43
-
44
- const userContent = [
45
- `Feature: ${issue.title}`,
46
- `\nDescrição:\n${issue.body || '(sem descrição)'}`,
47
- planContent ? `\nPlano Técnico (plan.md):\n${planContent}` : '',
48
- ].join('');
46
+ // Payload estruturado (RFC-002 §5.1): metadata + entrada de negócio.
47
+ const payload = {
48
+ metadata: {
49
+ feature_title: issue.title,
50
+ feature_description: issue.body || '(sem descrição)',
51
+ labels: (issue.labels || []).map((l) => (typeof l === 'string' ? l : l.name)),
52
+ },
53
+ business_input: {
54
+ raw: issue.body || '(sem descrição)',
55
+ },
56
+ };
57
+ const userContent = `Gere o spec.md a partir deste payload JSON:\n\n${JSON.stringify(payload, null, 2)}`;
49
58
 
50
59
  console.log(`Gerando spec.md para: ${issue.title}`);
51
60
  const content = await generateDocument(SYSTEM_PROMPT, userContent);
@@ -69,8 +78,8 @@ export async function generateSpec({ issueNumber }) {
69
78
  token, owner, repo, parseInt(issueNumber, 10),
70
79
  `📋 **spec.md gerado automaticamente!**\n\n` +
71
80
  `📄 Arquivo: [\`${filePath}\`](${filePath})\n\n` +
72
- `Revise a especificação e, quando estiver pronto, mova o card para **✅ Ready** ou use:\n` +
73
- `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\``
81
+ `Revise a especificação e, quando estiver pronto, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
82
+ `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\``
74
83
  );
75
84
 
76
85
  console.log(`spec.md criado em: ${filePath}`);
@@ -122,7 +122,7 @@ export async function init(options) {
122
122
  labelSpinner.start('Criando labels...');
123
123
  try {
124
124
  await setupLabels(token, owner, repo, labelSpinner);
125
- labelSpinner.stop('Labels criadas (15 labels)');
125
+ labelSpinner.stop('Labels criadas (16 labels)');
126
126
  } catch (err) {
127
127
  labelSpinner.stop('');
128
128
  p.log.error(`Erro ao criar labels: ${err.message}`);
@@ -0,0 +1,8 @@
1
+ import { issue } from './issue.mjs';
2
+
3
+ // `initiative` é um atalho de `issue --type initiative`. A Initiative é o nó raiz
4
+ // da hierarquia (Initiative → Epic → Feature → Story → Task) e agrupa Epics.
5
+ // Toda a lógica vive em issue.mjs.
6
+ export async function initiative(options) {
7
+ return issue({ ...options, type: 'initiative' });
8
+ }
@@ -179,8 +179,13 @@ export async function issue(options) {
179
179
  }
180
180
 
181
181
  const parentLine = parent ? ` (sub-issue de #${parent.number})` : '';
182
+ const hints = {
183
+ Feature: `Próximo: \`/spec-wave plan ${created.number}\` para o planejamento técnico.`,
184
+ Initiative: `Próximo: crie Epics sob esta Initiative com \`spec-wave issue --type epic --parent ${created.number} --title "..."\`.`,
185
+ Epic: `Próximo: crie Features sob este Epic com \`spec-wave feature --parent ${created.number} --title "..."\`.`,
186
+ };
182
187
  p.outro(
183
188
  `${chalk.green('✓')} ${type} #${created.number} criado em "${INITIAL_STAGE}"${parentLine}.\n` +
184
- ` ${type === 'Feature' ? `Próximo: \`/spec-wave plan ${created.number}\` para o planejamento técnico.` : ''}`
189
+ ` ${hints[type] || ''}`
185
190
  );
186
191
  }
@@ -67,8 +67,8 @@ export async function validate({ issueNumber }) {
67
67
  await commentOnIssue(
68
68
  token, owner, repo, parseInt(issueNumber, 10),
69
69
  `✅ **Validação concluída com sucesso!**\n\n` +
70
- `- [\`${planPath}\`](${planPath}) ✓\n` +
71
- `- [\`${specPath}\`](${specPath}) ✓\n\n` +
70
+ `- [\`${specPath}\`](${specPath}) ✓\n` +
71
+ `- [\`${planPath}\`](${planPath}) ✓\n\n` +
72
72
  `A Feature está pronta para decomposição. Mova o card para **📋 Backlog Técnico** ou use:\n` +
73
73
  `\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:decompose"\n\`\`\``
74
74
  );
package/src/config.mjs CHANGED
@@ -36,8 +36,8 @@ export function getProvider(value) {
36
36
  export const STATUS_OPTIONS = [
37
37
  { name: '📥 Backlog', color: 'GRAY' },
38
38
  { name: '🎯 Priorizado', color: 'BLUE' },
39
- { name: '📋 Plan', color: 'YELLOW' },
40
39
  { name: '📋 Spec', color: 'YELLOW' },
40
+ { name: '📋 Plan', color: 'YELLOW' },
41
41
  { name: '✅ Ready', color: 'GREEN' },
42
42
  { name: '📋 Backlog Técnico', color: 'BLUE' },
43
43
  { name: '🚧 Desenvolvimento', color: 'ORANGE' },
@@ -53,6 +53,7 @@ export const CUSTOM_FIELDS = [
53
53
  name: 'Work Item Type',
54
54
  dataType: 'SINGLE_SELECT',
55
55
  options: [
56
+ { name: 'Initiative', color: 'PINK', description: 'Agrupamento estratégico de Epics' },
56
57
  { name: 'Epic', color: 'PURPLE', description: 'Objetivo estratégico' },
57
58
  { name: 'Feature', color: 'BLUE', description: 'Capacidade funcional' },
58
59
  { name: 'Story', color: 'GREEN', description: 'Necessidade do usuário' },
@@ -110,6 +111,7 @@ export const WORK_ITEM_TYPES = CUSTOM_FIELDS
110
111
  .options.map(o => o.name);
111
112
 
112
113
  export const TYPE_LABELS = [
114
+ { name: '[INITIATIVE]', color: 'C5DEF5', description: 'Agrupamento estratégico de Epics' },
113
115
  { name: '[EPIC]', color: '7B61FF', description: 'Objetivo estratégico' },
114
116
  { name: '[FEATURE]', color: '0075CA', description: 'Capacidade funcional' },
115
117
  { name: '[STORY]', color: '0E8A16', description: 'Necessidade do usuário' },
@@ -127,8 +129,8 @@ export const PRIORITY_LABELS = [
127
129
  ];
128
130
 
129
131
  export const TRIGGER_LABELS = [
130
- { name: 'spec-wave:plan', color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
131
132
  { name: 'spec-wave:spec', color: 'BFD4F2', description: 'Gerar spec.md via GitHub Action' },
133
+ { name: 'spec-wave:plan', color: 'BFD4F2', description: 'Gerar plan.md via GitHub Action' },
132
134
  { name: 'spec-wave:ready', color: '0E8A16', description: 'Validar spec+plan e mover para Ready' },
133
135
  { name: 'spec-wave:decompose', color: 'BFD4F2', description: 'Decompor em Stories e Tasks' },
134
136
  ];
@@ -147,14 +149,24 @@ export const ISSUE_TEMPLATE_FILES = [
147
149
  'spec-template.md',
148
150
  ];
149
151
 
152
+ export const REQUIRED_SPEC_SECTIONS = [
153
+ 'Visão Geral',
154
+ 'Critérios de Aceite',
155
+ 'Requisitos Não-Funcionais',
156
+ ];
157
+
150
158
  export const REQUIRED_PLAN_SECTIONS = [
151
- 'Frontend',
152
- 'Backend',
153
- 'Banco de dados',
154
- 'Testes',
159
+ 'Estratégia Técnica',
160
+ 'Detalhamento da Implementação',
161
+ 'Segurança e Conformidade',
162
+ 'Estratégia de Testes',
163
+ 'Rollback e Monitoramento',
155
164
  ];
156
165
 
157
- export const REQUIRED_SPEC_SECTIONS = [
158
- 'Objetivo',
159
- 'Critérios de Aceite',
166
+ // Arquivos de configuração versionados gerados pelo `init` em .github/config/.
167
+ // O tech_context.yml (RFC-002 §4) é a fonte de verdade estática que o
168
+ // generate-plan lê para embasar o plano técnico. O scaffold em setup/files.mjs
169
+ // só cria o arquivo se ainda não existir, para não sobrescrever ajustes manuais.
170
+ export const CONFIG_FILES = [
171
+ 'tech_context.yml',
160
172
  ];
@@ -23,17 +23,40 @@ function resolveAi() {
23
23
  return { provider: meta.value, model, secret: meta.secret };
24
24
  }
25
25
 
26
- export async function generateDocument(systemPrompt, userContent) {
26
+ // temperature padrão 0.2 (RFC-002 §5): "Determinism over Creativity". Pode ser
27
+ // sobrescrita por chamada via opts, mas o default cobre spec/plan/decompose.
28
+ export async function generateDocument(systemPrompt, userContent, opts = {}) {
27
29
  const ai = resolveAi();
28
- console.log(`Provider de IA: ${ai.provider} · modelo: ${ai.model}`);
30
+ const temperature = opts.temperature ?? 0.2;
31
+ // Modelos de reasoning (ex.: deepseek-r1) consomem tokens "pensando" antes da
32
+ // resposta, então o teto precisa ser maior para o plano não vir truncado.
33
+ const maxTokens = opts.maxTokens ?? 8192;
34
+ console.log(`Provider de IA: ${ai.provider} · modelo: ${ai.model} · temperature: ${temperature} · max_tokens: ${maxTokens}`);
29
35
 
30
- if (ai.provider === 'openrouter') {
31
- return generateWithOpenRouter(systemPrompt, userContent, ai);
32
- }
33
- return generateWithAnthropic(systemPrompt, userContent, ai);
36
+ const raw = ai.provider === 'openrouter'
37
+ ? await generateWithOpenRouter(systemPrompt, userContent, ai, temperature, maxTokens)
38
+ : await generateWithAnthropic(systemPrompt, userContent, ai, temperature, maxTokens);
39
+
40
+ return stripOuterFence(raw);
41
+ }
42
+
43
+ // Remove blocos de raciocínio que alguns modelos (ex.: deepseek-r1) embutem no
44
+ // content. A OpenRouter normalmente separa em `reasoning`, mas isto é uma rede
45
+ // de segurança caso o <think> venha junto do conteúdo final.
46
+ function stripReasoning(text) {
47
+ return text.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
48
+ }
49
+
50
+ // Alguns modelos (ex.: deepseek-r1) envolvem o documento inteiro numa fence
51
+ // ```markdown ... ```. Removemos só o wrapper externo (a primeira e a última
52
+ // linha de cerca), preservando fences internas (gherkin, typescript, etc.).
53
+ function stripOuterFence(text) {
54
+ const t = (text || '').trim();
55
+ const m = t.match(/^```[a-zA-Z]*\n([\s\S]*)\n```$/);
56
+ return m ? m[1].trim() : t;
34
57
  }
35
58
 
36
- async function generateWithAnthropic(systemPrompt, userContent, ai) {
59
+ async function generateWithAnthropic(systemPrompt, userContent, ai, temperature, maxTokens) {
37
60
  const apiKey = process.env.ANTHROPIC_API_KEY;
38
61
  if (!apiKey) {
39
62
  throw new Error(
@@ -45,7 +68,8 @@ async function generateWithAnthropic(systemPrompt, userContent, ai) {
45
68
  const client = new Anthropic({ apiKey });
46
69
  const message = await client.messages.create({
47
70
  model: ai.model,
48
- max_tokens: 4096,
71
+ max_tokens: maxTokens,
72
+ temperature,
49
73
  messages: [{ role: 'user', content: userContent }],
50
74
  system: systemPrompt,
51
75
  });
@@ -53,7 +77,7 @@ async function generateWithAnthropic(systemPrompt, userContent, ai) {
53
77
  return message.content[0].text;
54
78
  }
55
79
 
56
- async function generateWithOpenRouter(systemPrompt, userContent, ai) {
80
+ async function generateWithOpenRouter(systemPrompt, userContent, ai, temperature, maxTokens) {
57
81
  const apiKey = process.env.OPENROUTER_API_KEY;
58
82
  if (!apiKey) {
59
83
  throw new Error(
@@ -72,7 +96,8 @@ async function generateWithOpenRouter(systemPrompt, userContent, ai) {
72
96
  },
73
97
  body: JSON.stringify({
74
98
  model: ai.model,
75
- max_tokens: 4096,
99
+ max_tokens: maxTokens,
100
+ temperature,
76
101
  messages: [
77
102
  { role: 'system', content: systemPrompt },
78
103
  { role: 'user', content: userContent },
@@ -86,7 +111,7 @@ async function generateWithOpenRouter(systemPrompt, userContent, ai) {
86
111
  }
87
112
 
88
113
  const data = await res.json();
89
- const content = data?.choices?.[0]?.message?.content;
114
+ const content = stripReasoning(data?.choices?.[0]?.message?.content || '');
90
115
  if (!content) {
91
116
  throw new Error(`OpenRouter retornou resposta vazia: ${JSON.stringify(data)}`);
92
117
  }
@@ -0,0 +1,142 @@
1
+ import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+
5
+ // Subsistema de Tech Context (RFC-002 §4).
6
+ //
7
+ // Monta o contexto técnico que o `generate-plan` injeta na chamada de IA, no
8
+ // formato do payload RFC §5.2: { static, dynamic, overrides }. Roda no checkout
9
+ // do GitHub Action, então lê tudo do filesystem local (cwd = raiz do repo-alvo).
10
+
11
+ const TECH_CONTEXT_PATH = '.github/config/tech_context.yml';
12
+
13
+ // Diretórios onde migrations costumam viver (várias stacks).
14
+ const MIGRATION_DIRS = [
15
+ 'migrations',
16
+ 'db/migrations',
17
+ 'src/migrations',
18
+ 'prisma/migrations',
19
+ 'database/migrations',
20
+ ];
21
+
22
+ const MAX_MIGRATIONS = 10;
23
+
24
+ // §4.1 — Fonte de verdade estática. Ausência não é erro: segue com {} e avisa.
25
+ function readStaticContext(cwd) {
26
+ 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
+ }
35
+ try {
36
+ return yaml.load(readFileSync(filePath, 'utf-8')) || {};
37
+ } catch (err) {
38
+ console.warn(`⚠️ Falha ao parsear ${TECH_CONTEXT_PATH}: ${err.message}. Ignorando.`);
39
+ return {};
40
+ }
41
+ }
42
+
43
+ // §4.2 — Augmentação dinâmica: migrations recentes + versões exatas de pacotes.
44
+ function readDynamicContext(cwd) {
45
+ return {
46
+ recent_migrations: listRecentMigrations(cwd),
47
+ current_packages: readPackageVersions(cwd),
48
+ };
49
+ }
50
+
51
+ function listRecentMigrations(cwd) {
52
+ for (const dir of MIGRATION_DIRS) {
53
+ const abs = path.join(cwd, dir);
54
+ if (!existsSync(abs)) continue;
55
+ try {
56
+ const entries = readdirSync(abs)
57
+ .map((name) => {
58
+ const full = path.join(abs, name);
59
+ return { name, mtime: statSync(full).mtimeMs };
60
+ })
61
+ .sort((a, b) => b.mtime - a.mtime)
62
+ .slice(0, MAX_MIGRATIONS)
63
+ .map((e) => `${dir}/${e.name}`);
64
+ if (entries.length) return entries;
65
+ } catch {
66
+ // diretório ilegível → tenta o próximo
67
+ }
68
+ }
69
+ return [];
70
+ }
71
+
72
+ function readPackageVersions(cwd) {
73
+ const pkgPath = path.join(cwd, 'package.json');
74
+ if (!existsSync(pkgPath)) return {};
75
+ try {
76
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
77
+ return { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
78
+ } catch {
79
+ return {};
80
+ }
81
+ }
82
+
83
+ // §4.3 — Override do corpo da issue. Extrai a seção markdown exatamente titulada
84
+ // `## Tech Override` e parseia o bloco como YAML. Retorna {} se ausente.
85
+ export function parseTechOverride(issueBody) {
86
+ if (!issueBody) return {};
87
+ const lines = issueBody.split(/\r?\n/);
88
+ const start = lines.findIndex((l) => l.trim() === '## Tech Override');
89
+ if (start === -1) return {};
90
+
91
+ // Captura até o próximo heading de mesmo nível (ou superior) ou fim do corpo.
92
+ const body = [];
93
+ for (let i = start + 1; i < lines.length; i++) {
94
+ if (/^#{1,2}\s/.test(lines[i])) break;
95
+ body.push(lines[i]);
96
+ }
97
+
98
+ // Tolera o YAML vir dentro de uma fence ```yaml ... ```.
99
+ let raw = body.join('\n').trim();
100
+ const fence = raw.match(/```(?:ya?ml)?\s*\n([\s\S]*?)```/);
101
+ if (fence) raw = fence[1];
102
+ if (!raw.trim()) return {};
103
+
104
+ try {
105
+ return yaml.load(raw) || {};
106
+ } catch (err) {
107
+ console.warn(`⚠️ Falha ao parsear a seção '## Tech Override': ${err.message}. Ignorando.`);
108
+ return {};
109
+ }
110
+ }
111
+
112
+ // Deep-merge: override vence; objetos são mesclados recursivamente, escalares e
113
+ // arrays são substituídos.
114
+ function deepMerge(base, override) {
115
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
116
+ const out = { ...base };
117
+ for (const key of Object.keys(override)) {
118
+ out[key] = key in base ? deepMerge(base[key], override[key]) : override[key];
119
+ }
120
+ return out;
121
+ }
122
+
123
+ function isPlainObject(v) {
124
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
125
+ }
126
+
127
+ // Monta o tech context completo. `cwd` default = process.cwd() (checkout do Action).
128
+ export function buildTechContext({ issueBody = '', cwd = process.cwd() } = {}) {
129
+ const staticContext = readStaticContext(cwd);
130
+ const dynamic = readDynamicContext(cwd);
131
+ const overrides = parseTechOverride(issueBody);
132
+ const merged = deepMerge(staticContext, overrides);
133
+
134
+ return {
135
+ static: staticContext,
136
+ dynamic,
137
+ overrides,
138
+ merged,
139
+ // String pronta para injetar no prompt: contexto efetivo + efêmeros.
140
+ yaml: yaml.dump({ ...merged, _dynamic: dynamic }, { lineWidth: 120 }),
141
+ };
142
+ }
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import path from 'node:path';
4
- import { upsertFile, isRepoInitialized } from '../api/github-rest.mjs';
4
+ import { upsertFile, getFileContent, isRepoInitialized } from '../api/github-rest.mjs';
5
5
 
6
6
  const __dir = path.dirname(fileURLToPath(import.meta.url));
7
7
  const TEMPLATES_DIR = path.join(__dir, '..', 'templates');
@@ -60,4 +60,24 @@ export async function setupFiles(token, owner, repo, spinner) {
60
60
  spinner.message(`Criando arquivo ${i + 1}/${filesToCreate.length}: ${file.path}`);
61
61
  await upsertFile(token, owner, repo, file.path, file.content, file.message);
62
62
  }
63
+
64
+ // Arquivos de config criados apenas se ainda não existirem, para não
65
+ // sobrescrever ajustes manuais (tech_context.yml é editado pelo time).
66
+ const configFiles = [
67
+ {
68
+ path: '.github/config/tech_context.yml',
69
+ content: readTemplate('config', 'tech_context.yml'),
70
+ message: 'chore: scaffold tech_context.yml [spec-wave]',
71
+ },
72
+ ];
73
+
74
+ for (const file of configFiles) {
75
+ const existing = await getFileContent(token, owner, repo, file.path);
76
+ if (existing !== null) {
77
+ spinner.message(`Mantendo ${file.path} existente (não sobrescrito)`);
78
+ continue;
79
+ }
80
+ spinner.message(`Criando arquivo de config: ${file.path}`);
81
+ await upsertFile(token, owner, repo, file.path, file.content, file.message);
82
+ }
63
83
  }
@@ -0,0 +1,42 @@
1
+ # tech_context.yml — Fonte de verdade estática do sistema (RFC-002 §4)
2
+ #
3
+ # O comando `generate-plan` lê este arquivo para embasar o plano técnico, usando
4
+ # APENAS as tecnologias e serviços aqui declarados. Mantenha-o atualizado.
5
+ #
6
+ # Augmentação dinâmica (automática, não precisa editar): o agente também lê as
7
+ # migrations recentes e as versões exatas de pacotes (package.json) em tempo de
8
+ # execução. Para desvios pontuais, adicione uma seção `## Tech Override` no corpo
9
+ # da issue da Feature — ela é mesclada (deep-merge) sobre esta config.
10
+
11
+ system_info:
12
+ name: "Order Management System"
13
+ stack:
14
+ backend: "Node.js (NestJS v10)"
15
+ frontend: "React 18 with Vite"
16
+ database: "PostgreSQL 15"
17
+ cache: "Redis 7"
18
+ messaging: "RabbitMQ"
19
+ infra: "Kubernetes (EKS) + Helm"
20
+ architecture: "Microservices via API Gateway"
21
+
22
+ security:
23
+ auth_protocol: "JWT (Access/Refresh)"
24
+ rbac_roles: ["ADMIN", "MANAGER", "GARCOM", "COZINHA"]
25
+
26
+ database_schemas:
27
+ - table: "users"
28
+ columns: "id, name, email, role, password_hash"
29
+ - table: "tables"
30
+ columns: "id, number, status, capacity"
31
+ - table: "products"
32
+ columns: "id, name, category, price, stock"
33
+
34
+ existing_services:
35
+ - name: "Inventory API"
36
+ endpoint: "/api/inventory/{productId}/availability"
37
+ auth: "mTLS"
38
+ docs: "https://docs.internal/inventory"
39
+
40
+ internal_libraries:
41
+ - "shared-logger"
42
+ - "db-client (TypeORM)"
@@ -6,30 +6,46 @@ labels: "[FEATURE]"
6
6
  assignees: ""
7
7
  ---
8
8
 
9
- # Frontend
9
+ # Estratégia Técnica
10
10
 
11
- <!-- Componentes, rotas, estados, integrações de UI -->
11
+ - **Abordagem Arquitetural:** <!-- ex.: CQRS, Event Sourcing, REST -->
12
+ - **Decisões-Chave:** <!-- Justificativa das tecnologias/padrões escolhidos -->
13
+ - **Matriz de Rastreabilidade:** <!-- Cada Critério de Aceite do spec mapeado para um componente técnico -->
12
14
 
13
- # Backend
15
+ | Critério de Aceite | Componente Técnico |
16
+ |--------------------|--------------------|
17
+ | | |
14
18
 
15
- <!-- Endpoints, serviços, regras de negócio -->
19
+ # Detalhamento da Implementação
16
20
 
17
- # Banco de dados
21
+ ## Backend
18
22
 
19
- <!-- Migrations, tabelas, índices, queries relevantes -->
23
+ <!-- Endpoints, DTOs, controllers, serviços, casos de uso, jobs/filas -->
20
24
 
21
- # Infraestrutura
25
+ ## Banco de Dados
22
26
 
23
- <!-- Configurações de ambiente, variáveis, serviços externos -->
27
+ <!-- Novas tabelas, migrations, índices -->
24
28
 
25
- # Segurança
29
+ ## Frontend
26
30
 
27
- <!-- Autenticação, autorização, validações críticas -->
31
+ <!-- Componentes/telas, gerenciamento de estado, rotas e guards -->
28
32
 
29
- # Testes
33
+ ## Infraestrutura
30
34
 
31
- <!-- Estratégia de testes: unitários, integração, E2E -->
35
+ <!-- ConfigMaps/Secrets, pipeline CI/CD, feature flags e estratégia de rollout -->
32
36
 
33
- # Estimativa (Story Points)
37
+ # Segurança e Conformidade
34
38
 
35
- <!-- 1 / 2 / 3 / 5 / 8 / 13 / 21 -->
39
+ <!-- Autenticação/autorização (quais papéis acessam?), criptografia (em repouso/trânsito), logging e auditoria -->
40
+
41
+ # Estratégia de Testes
42
+
43
+ - **Unitários:** <!-- Escopo e frameworks -->
44
+ - **Integração:** <!-- Escopo e mocks -->
45
+ - **E2E:** <!-- Caminhos críticos -->
46
+
47
+ # Rollback e Monitoramento
48
+
49
+ - **Plano de Rollback:** <!-- Rollback de banco, revert de código -->
50
+ - **Métricas Observadas:** <!-- Dashboards (New Relic/Datadog) -->
51
+ - **Alertas:** <!-- Thresholds e caminhos de escalonamento -->
@@ -6,9 +6,11 @@ labels: "[FEATURE]"
6
6
  assignees: ""
7
7
  ---
8
8
 
9
- # Objetivo
9
+ # Visão Geral
10
10
 
11
- <!-- O que esta Feature entrega e qual problema resolve? -->
11
+ - **Objetivo:** <!-- O que esta Feature entrega e qual problema resolve? -->
12
+ - **Personas:** <!-- Quais usuários são afetados? (ex.: Garçom, Cozinha) -->
13
+ - **Critérios de Sucesso:** <!-- Resultados mensuráveis -->
12
14
 
13
15
  # Regras de Negócio
14
16
 
@@ -16,17 +18,37 @@ assignees: ""
16
18
 
17
19
  # Fluxos
18
20
 
19
- <!-- Descreva o fluxo principal e os fluxos alternativos -->
21
+ ## Fluxo Principal (Happy Path)
20
22
 
21
- # Critérios de Aceite
23
+ <!-- Passo a passo do caminho feliz -->
24
+
25
+ ## Fluxos Alternativos
22
26
 
23
- - [ ]
24
- - [ ]
27
+ <!-- Variações do fluxo principal -->
25
28
 
26
- # Casos de Erro
29
+ ## Cenários de Erro
27
30
 
28
31
  <!-- O que acontece quando algo dá errado? Como o sistema se comporta? -->
29
32
 
33
+ # Critérios de Aceite
34
+
35
+ <!-- Use o formato Gherkin (Given/When/Then). Um cenário por critério. -->
36
+
37
+ ```gherkin
38
+ Feature: [Nome da Feature]
39
+ Scenario: [Título do cenário]
40
+ Given [pré-condição]
41
+ When [ação]
42
+ Then [resultado esperado]
43
+ ```
44
+
30
45
  # Dependências
31
46
 
32
- <!-- Outras Features, integrações, APIs externas ou pré-requisitos -->
47
+ - **Internas:** <!-- Serviços/APIs dentro do sistema -->
48
+ - **Externas:** <!-- Sistemas de terceiros -->
49
+
50
+ # Requisitos Não-Funcionais
51
+
52
+ - **Performance:** <!-- ex.: tempo de resposta < 200ms -->
53
+ - **Segurança:** <!-- ex.: RBAC obrigatório -->
54
+ - **Usabilidade:** <!-- ex.: responsivo em mobile -->