@spec-wave/cli 0.11.1 → 0.12.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/README.md +14 -1
- package/package.json +1 -1
- package/src/commands/decompose.mjs +14 -9
- package/src/commands/generate-plan.mjs +18 -9
- package/src/commands/generate-spec.mjs +13 -7
- package/src/commands/implement.mjs +9 -7
- package/src/commands/validate.mjs +15 -12
- package/src/lib/feature-docs.mjs +89 -0
- package/src/templates/skill/SKILL.md +32 -5
package/README.md
CHANGED
|
@@ -91,7 +91,20 @@ Ferramenta Node.js que configura e opera o fluxo via linha de comando.
|
|
|
91
91
|
gh issue edit 12 --add-label "spec-wave:force" --add-label "spec-wave:decompose"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
No `decompose` ela **fecha as sub-issues da decomposição anterior** (Stories e suas Tasks) antes de gerar as novas — operação destrutiva, com aviso no comentário da issue quando alguma já saiu de `✅ Ready`. Em `generate-spec`/`generate-plan`
|
|
94
|
+
No `decompose` ela **fecha as sub-issues da decomposição anterior** (Stories e suas Tasks) antes de gerar as novas — operação destrutiva, com aviso no comentário da issue quando alguma já saiu de `✅ Ready`. Em `generate-spec`/`generate-plan` ela **versiona** o documento em vez de sobrescrever (veja abaixo). A flag equivalente para execução local é `--force`.
|
|
95
|
+
|
|
96
|
+
### Documentos versionados
|
|
97
|
+
|
|
98
|
+
A primeira geração escreve `spec.md`/`plan.md` (v1). Cada regeração **forçada** grava a próxima versão ao lado, preservando as anteriores:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
docs/features/<slug>/
|
|
102
|
+
spec.md ← v1
|
|
103
|
+
spec-v2.md ← regeração forçada ⬅ spec atual
|
|
104
|
+
plan.md ⬅ plano atual (spec e plan versionam independente)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A **maior versão é o documento atual**: é ela que o `generate-plan` usa como contexto, que o `validate` valida, e que o `decompose` e o `implement` leem. Uma regeração **sem** force sobrescreve essa versão atual.
|
|
95
108
|
|
|
96
109
|
### Skill (`src/templates/skill/SKILL.md`)
|
|
97
110
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { resolveToken } from '../api/auth.mjs';
|
|
3
3
|
import { getIssue, createIssue, removeLabel, addLabel, commentOnIssue, addBlockedBy, closeIssue } from '../api/github-rest.mjs';
|
|
4
4
|
import { addSubIssue, listSubIssues, addProjectItem, getItemSingleSelectValue } from '../api/github-graphql.mjs';
|
|
5
5
|
import { loadProjectConfig, resolveField, advanceToStage } from '../lib/board.mjs';
|
|
6
6
|
import { isForced, consumeForceLabel } from '../lib/force.mjs';
|
|
7
|
+
import { resolveDoc } from '../lib/feature-docs.mjs';
|
|
7
8
|
import { generateDocument } from '../lib/claude.mjs';
|
|
8
9
|
import { runCritique } from '../lib/critique.mjs';
|
|
9
10
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
@@ -285,14 +286,18 @@ async function decomposeFeature(ctx) {
|
|
|
285
286
|
const slug = slugify(issue.title);
|
|
286
287
|
const featureDir = `docs/features/${slug}`;
|
|
287
288
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
// Documentos ATUAIS (maior versão): com regerações forçadas o vigente é
|
|
290
|
+
// spec-v2.md/plan-v3.md…, não o arquivo original.
|
|
291
|
+
const spec = resolveDoc(featureDir, 'spec');
|
|
292
|
+
const plan = resolveDoc(featureDir, 'plan');
|
|
293
|
+
const specContent = spec ? readFileSync(spec.path, 'utf-8') : '(spec.md não encontrado)';
|
|
294
|
+
const planContent = plan ? readFileSync(plan.path, 'utf-8') : '(plan.md não encontrado)';
|
|
294
295
|
|
|
295
296
|
console.log(`Decompondo Feature: ${issue.title}`);
|
|
297
|
+
if (spec || plan) {
|
|
298
|
+
console.log(`Documentos: ${spec ? `${spec.path} (v${spec.version})` : 'spec ausente'} · ` +
|
|
299
|
+
`${plan ? `${plan.path} (v${plan.version})` : 'plano ausente'}`);
|
|
300
|
+
}
|
|
296
301
|
const userContent = [
|
|
297
302
|
`Feature: ${issue.title}`,
|
|
298
303
|
`Issue #${issueNumber}`,
|
|
@@ -308,8 +313,8 @@ async function decomposeFeature(ctx) {
|
|
|
308
313
|
try {
|
|
309
314
|
critique = await runCritique({
|
|
310
315
|
kind: 'stories',
|
|
311
|
-
spec:
|
|
312
|
-
plan:
|
|
316
|
+
spec: spec ? specContent : null,
|
|
317
|
+
plan: plan ? planContent : null,
|
|
313
318
|
stories: decomposition.stories,
|
|
314
319
|
usage,
|
|
315
320
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, writeFileSync, readFileSync
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
4
|
import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
5
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
@@ -9,6 +9,7 @@ import { runCritique } from '../lib/critique.mjs';
|
|
|
9
9
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
10
10
|
import { slugify } from '../lib/slugify.mjs';
|
|
11
11
|
import { isForced, consumeForceLabel } from '../lib/force.mjs';
|
|
12
|
+
import { resolveDoc, resolveWritePath } from '../lib/feature-docs.mjs';
|
|
12
13
|
import { buildTechContext } from '../lib/tech-context.mjs';
|
|
13
14
|
|
|
14
15
|
// Aviso anexado ao comentário quando o lint de idioma ainda reprova após o
|
|
@@ -70,18 +71,22 @@ export async function generatePlan({ issueNumber, force = false }) {
|
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
+
// Force: grava a PRÓXIMA versão (plan-v2.md…) preservando as anteriores; sem
|
|
75
|
+
// force, sobrescreve a versão vigente. Ver lib/feature-docs.mjs.
|
|
74
76
|
const forced = isForced({ labels: issue.labels || [], flag: force });
|
|
75
77
|
await consumeForceLabel(token, owner, repo, parseInt(issueNumber, 10));
|
|
76
|
-
if (forced) console.log('Modo forçado ativo — plan.md será regerado (sobrescreve o existente).');
|
|
77
78
|
|
|
78
79
|
const slug = slugify(issue.title);
|
|
79
80
|
const featureDir = `docs/features/${slug}`;
|
|
80
|
-
const
|
|
81
|
+
const target = resolveWritePath(featureDir, 'plan', { force: forced });
|
|
82
|
+
const filePath = target.path;
|
|
83
|
+
if (forced) console.log(`Modo forçado ativo — gerando ${filePath} (v${target.version}), sem tocar nas versões anteriores.`);
|
|
81
84
|
|
|
82
|
-
//
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
+
// Spec ATUAL (maior versão) como contexto — a spec é gerada antes do plano.
|
|
86
|
+
const spec = resolveDoc(featureDir, 'spec');
|
|
87
|
+
const specPath = spec?.path || `${featureDir}/spec.md`;
|
|
88
|
+
const specContent = spec ? readFileSync(spec.path, 'utf-8') : null;
|
|
89
|
+
if (spec) console.log(`Usando ${spec.path} (spec v${spec.version}) como contexto.`);
|
|
85
90
|
|
|
86
91
|
// Tech context (RFC-002 §4): estático + dinâmico + override do corpo da issue.
|
|
87
92
|
const tech = buildTechContext({ issueBody: issue.body || '' });
|
|
@@ -119,7 +124,7 @@ export async function generatePlan({ issueNumber, force = false }) {
|
|
|
119
124
|
git(`git config user.email "spec-wave[bot]@github.com"`);
|
|
120
125
|
git(`git config user.name "spec-wave[bot]"`);
|
|
121
126
|
git(`git add "${filePath}"`);
|
|
122
|
-
git(`git commit -m "docs: generate plan.
|
|
127
|
+
git(`git commit -m "docs: generate plan v${target.version} for ${slug} [spec-wave]"`);
|
|
123
128
|
git('git pull --rebase');
|
|
124
129
|
git('git push');
|
|
125
130
|
|
|
@@ -129,8 +134,12 @@ export async function generatePlan({ issueNumber, force = false }) {
|
|
|
129
134
|
// Comment on issue
|
|
130
135
|
await commentOnIssue(
|
|
131
136
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
132
|
-
`📋 **
|
|
137
|
+
`📋 **plano técnico gerado automaticamente** (v${target.version})\n\n` +
|
|
133
138
|
`📄 Arquivo: [\`${filePath}\`](https://github.com/${owner}/${repo}/blob/main/${filePath})\n\n` +
|
|
139
|
+
(target.isNewVersion
|
|
140
|
+
? `♻️ Regeração forçada: as versões anteriores foram **preservadas**; ` +
|
|
141
|
+
`**v${target.version} passa a ser o plano atual** — é ele que a validação e a decomposição vão ler.\n\n`
|
|
142
|
+
: '') +
|
|
134
143
|
`Revise o plano e, quando estiver pronto, valide a Feature: mova o card para **✅ Ready** ou use:\n` +
|
|
135
144
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:ready"\n\`\`\`` +
|
|
136
145
|
formatLintWarning(lintFindings)
|
|
@@ -6,6 +6,7 @@ import { generateDocument } from '../lib/claude.mjs';
|
|
|
6
6
|
import { recordUsage } from '../lib/usage-report.mjs';
|
|
7
7
|
import { slugify } from '../lib/slugify.mjs';
|
|
8
8
|
import { isForced, consumeForceLabel } from '../lib/force.mjs';
|
|
9
|
+
import { resolveWritePath } from '../lib/feature-docs.mjs';
|
|
9
10
|
import { detectIssueType } from '../lib/issue-type.mjs';
|
|
10
11
|
import { allowsSpecPlan, SPEC_PLAN_EXCLUDED_TYPES, TARGET_LANGUAGE } from '../config.mjs';
|
|
11
12
|
|
|
@@ -71,16 +72,17 @@ export async function generateSpec({ issueNumber, force = false }) {
|
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// Force (flag --force ou label spec-wave:force): em vez de sobrescrever,
|
|
76
|
+
// grava a PRÓXIMA versão (spec-v2.md, spec-v3.md…) e ela passa a ser o
|
|
77
|
+
// documento atual. Sem force, sobrescreve a versão vigente.
|
|
77
78
|
const forced = isForced({ labels: issue.labels || [], flag: force });
|
|
78
79
|
await consumeForceLabel(token, owner, repo, parseInt(issueNumber, 10));
|
|
79
|
-
if (forced) console.log('Modo forçado ativo — spec.md será regerado (sobrescreve o existente).');
|
|
80
80
|
|
|
81
81
|
const slug = slugify(issue.title);
|
|
82
82
|
const featureDir = `docs/features/${slug}`;
|
|
83
|
-
const
|
|
83
|
+
const target = resolveWritePath(featureDir, 'spec', { force: forced });
|
|
84
|
+
const filePath = target.path;
|
|
85
|
+
if (forced) console.log(`Modo forçado ativo — gerando ${filePath} (v${target.version}), sem tocar nas versões anteriores.`);
|
|
84
86
|
|
|
85
87
|
// Payload estruturado (RFC-002 §5.1): metadata + entrada de negócio.
|
|
86
88
|
const payload = {
|
|
@@ -115,7 +117,7 @@ export async function generateSpec({ issueNumber, force = false }) {
|
|
|
115
117
|
git(`git config user.email "spec-wave[bot]@github.com"`);
|
|
116
118
|
git(`git config user.name "spec-wave[bot]"`);
|
|
117
119
|
git(`git add "${filePath}"`);
|
|
118
|
-
git(`git commit -m "docs: generate spec.
|
|
120
|
+
git(`git commit -m "docs: generate spec v${target.version} for ${slug} [spec-wave]"`);
|
|
119
121
|
git('git pull --rebase');
|
|
120
122
|
git('git push');
|
|
121
123
|
|
|
@@ -125,8 +127,12 @@ export async function generateSpec({ issueNumber, force = false }) {
|
|
|
125
127
|
// Comment on issue
|
|
126
128
|
await commentOnIssue(
|
|
127
129
|
token, owner, repo, parseInt(issueNumber, 10),
|
|
128
|
-
`📋 **spec
|
|
130
|
+
`📋 **spec gerada automaticamente** (v${target.version})\n\n` +
|
|
129
131
|
`📄 Arquivo: [\`${filePath}\`](https://github.com/${owner}/${repo}/blob/main/${filePath})\n\n` +
|
|
132
|
+
(target.isNewVersion
|
|
133
|
+
? `♻️ Regeração forçada: as versões anteriores foram **preservadas**; ` +
|
|
134
|
+
`**v${target.version} passa a ser a spec atual** — é ela que o plano, a validação e a decomposição vão ler.\n\n`
|
|
135
|
+
: '') +
|
|
130
136
|
`Revise a especificação e, quando estiver pronto, gere o plano técnico: mova o card para **📋 Plan** ou use:\n` +
|
|
131
137
|
`\`\`\`\ngh issue edit ${issueNumber} --add-label "spec-wave:plan"\n\`\`\`` +
|
|
132
138
|
formatLintWarning(lintFindings)
|
|
@@ -14,6 +14,7 @@ import { detectIssueType } from '../lib/issue-type.mjs';
|
|
|
14
14
|
import { slugify } from '../lib/slugify.mjs';
|
|
15
15
|
import { parseDependencies, orderStories, formatDependencyLine } from '../lib/dependencies.mjs';
|
|
16
16
|
import { loadProjectConfig, resolveField } from '../lib/board.mjs';
|
|
17
|
+
import { resolveDoc } from '../lib/feature-docs.mjs';
|
|
17
18
|
import { planBoardMoves, applyBoardMoves } from '../lib/implement-board.mjs';
|
|
18
19
|
import { extractPathsFromPlan, buildCodeDigest } from '../lib/code-digest.mjs';
|
|
19
20
|
|
|
@@ -41,15 +42,16 @@ async function resolveFeature(token, startNodeId) {
|
|
|
41
42
|
return null;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
// Lê
|
|
45
|
+
// Lê os documentos ATUAIS (maior versão) de um docs/features/<slug>: com
|
|
46
|
+
// regerações forçadas o vigente é spec-v2.md/plan-v3.md…, não o original.
|
|
45
47
|
function readSpecPlan(featureDir) {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
+
const spec = resolveDoc(featureDir, 'spec');
|
|
49
|
+
const plan = resolveDoc(featureDir, 'plan');
|
|
48
50
|
return {
|
|
49
|
-
specPath,
|
|
50
|
-
planPath,
|
|
51
|
-
spec:
|
|
52
|
-
plan:
|
|
51
|
+
specPath: spec?.path || path.join(featureDir, 'spec.md'),
|
|
52
|
+
planPath: plan?.path || path.join(featureDir, 'plan.md'),
|
|
53
|
+
spec: spec ? readFileSync(spec.path, 'utf-8') : null,
|
|
54
|
+
plan: plan ? readFileSync(plan.path, 'utf-8') : null,
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { resolveToken } from '../api/auth.mjs';
|
|
4
4
|
import { getIssue, removeLabel, addLabel, commentOnIssue } from '../api/github-rest.mjs';
|
|
5
5
|
import { slugify } from '../lib/slugify.mjs';
|
|
6
|
+
import { resolveDoc } from '../lib/feature-docs.mjs';
|
|
6
7
|
import { CONFIG_FILE, LABEL_CRITIQUE_FAILED, REQUIRED_PLAN_SECTIONS, REQUIRED_SPEC_SECTIONS } from '../config.mjs';
|
|
7
8
|
import { findIncompleteDocSigns } from '../lib/doc-completeness.mjs';
|
|
8
9
|
|
|
@@ -39,37 +40,39 @@ export async function validate({ issueNumber }) {
|
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Valida sempre o documento ATUAL de cada tipo — com regerações forçadas a
|
|
44
|
+
// versão vigente é a maior (plan-v2.md, plan-v3.md…), não o plan.md original.
|
|
45
|
+
const plan = resolveDoc(featureDir, 'plan');
|
|
46
|
+
const planPath = plan?.path || `${featureDir}/plan.md`;
|
|
47
|
+
if (!plan) {
|
|
45
48
|
errors.push('❌ `plan.md` não encontrado em `' + planPath + '`');
|
|
46
49
|
} else {
|
|
47
|
-
const planContent = readFileSync(
|
|
50
|
+
const planContent = readFileSync(plan.path, 'utf-8');
|
|
48
51
|
for (const section of REQUIRED_PLAN_SECTIONS) {
|
|
49
52
|
if (!planContent.includes(`# ${section}`)) {
|
|
50
|
-
errors.push(`❌ Seção obrigatória ausente no plan.
|
|
53
|
+
errors.push(`❌ Seção obrigatória ausente no plano (v${plan.version}): **${section}**`);
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
for (const problem of findIncompleteDocSigns(planContent)) {
|
|
54
|
-
errors.push(`❌ \`
|
|
57
|
+
errors.push(`❌ \`${planPath}\` parece incompleto: ${problem}`);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
const specPath = `${featureDir}/spec.md`;
|
|
60
|
-
if (!
|
|
61
|
+
const spec = resolveDoc(featureDir, 'spec');
|
|
62
|
+
const specPath = spec?.path || `${featureDir}/spec.md`;
|
|
63
|
+
if (!spec) {
|
|
61
64
|
errors.push('❌ `spec.md` não encontrado em `' + specPath + '`');
|
|
62
65
|
} else {
|
|
63
|
-
const specContent = readFileSync(
|
|
66
|
+
const specContent = readFileSync(spec.path, 'utf-8');
|
|
64
67
|
for (const section of REQUIRED_SPEC_SECTIONS) {
|
|
65
68
|
if (!specContent.includes(`# ${section}`)) {
|
|
66
|
-
errors.push(`❌ Seção obrigatória ausente
|
|
69
|
+
errors.push(`❌ Seção obrigatória ausente na spec (v${spec.version}): **${section}**`);
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
// Seções presentes não garantem documento completo: um corte dentro da
|
|
70
73
|
// última seção passa na checagem acima (foi o caso da EP2-F13).
|
|
71
74
|
for (const problem of findIncompleteDocSigns(specContent)) {
|
|
72
|
-
errors.push(`❌ \`
|
|
75
|
+
errors.push(`❌ \`${specPath}\` parece incompleto: ${problem}`);
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Resolução dos documentos versionados de uma Feature (docs/features/<slug>/).
|
|
2
|
+
//
|
|
3
|
+
// A primeira geração escreve `spec.md`/`plan.md` (= v1). Cada regeração
|
|
4
|
+
// FORÇADA (flag --force ou label spec-wave:force) cria a próxima versão em vez
|
|
5
|
+
// de sobrescrever: `spec-v2.md`, `spec-v3.md`, … O documento ATUAL é sempre a
|
|
6
|
+
// MAIOR versão — é ele que validate/generate-plan/decompose/implement leem, e
|
|
7
|
+
// é ele que uma regeração sem force sobrescreve.
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Versão de um arquivo de documento (função PURA).
|
|
13
|
+
* `spec.md` → 1; `spec-v2.md` → 2. Outro nome → null.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} filename nome do arquivo (sem diretório)
|
|
16
|
+
* @param {string} base 'spec' ou 'plan'
|
|
17
|
+
* @returns {number|null}
|
|
18
|
+
*/
|
|
19
|
+
export function parseDocVersion(filename, base) {
|
|
20
|
+
const match = new RegExp(`^${base}(?:-v(\\d+))?\\.md$`).exec(String(filename ?? ''));
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
return match[1] ? parseInt(match[1], 10) : 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Documento ATUAL entre os arquivos dados: o de maior versão (função PURA).
|
|
27
|
+
* Empate (raro: `spec.md` e `spec-v1.md` coexistindo) resolve pelo sufixado,
|
|
28
|
+
* para o resultado ser determinístico.
|
|
29
|
+
*
|
|
30
|
+
* @param {string[]} filenames
|
|
31
|
+
* @param {string} base 'spec' ou 'plan'
|
|
32
|
+
* @returns {{ file: string, version: number }|null}
|
|
33
|
+
*/
|
|
34
|
+
export function pickLatestDoc(filenames, base) {
|
|
35
|
+
const found = (filenames || [])
|
|
36
|
+
.map(file => ({ file, version: parseDocVersion(file, base), suffixed: /-v\d+\.md$/.test(file) }))
|
|
37
|
+
.filter(entry => entry.version !== null)
|
|
38
|
+
.sort((a, b) => (a.version - b.version) || (Number(a.suffixed) - Number(b.suffixed)));
|
|
39
|
+
if (found.length === 0) return null;
|
|
40
|
+
const { file, version } = found[found.length - 1];
|
|
41
|
+
return { file, version };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Nome do arquivo da PRÓXIMA versão (função PURA): `spec.md` quando ainda não
|
|
46
|
+
* há nenhum, `spec-v<N+1>.md` a partir da maior versão existente.
|
|
47
|
+
*
|
|
48
|
+
* @param {string[]} filenames
|
|
49
|
+
* @param {string} base 'spec' ou 'plan'
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function nextDocName(filenames, base) {
|
|
53
|
+
const latest = pickLatestDoc(filenames, base);
|
|
54
|
+
return latest ? `${base}-v${latest.version + 1}.md` : `${base}.md`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Nomes de arquivo do diretório da feature ([] se ele ainda não existe).
|
|
58
|
+
function readDir(featureDir) {
|
|
59
|
+
return featureDir && existsSync(featureDir) ? readdirSync(featureDir) : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Caminho do documento ATUAL (maior versão) — null se nenhum existe.
|
|
64
|
+
*
|
|
65
|
+
* @returns {{ path: string, version: number }|null}
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDoc(featureDir, base) {
|
|
68
|
+
const latest = pickLatestDoc(readDir(featureDir), base);
|
|
69
|
+
return latest ? { path: path.join(featureDir, latest.file), version: latest.version } : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Caminho onde a geração deve escrever.
|
|
74
|
+
* - `force: true` → próxima versão (nunca sobrescreve nada);
|
|
75
|
+
* - `force: false` → o documento atual (sobrescreve), ou `<base>.md` no 1º run.
|
|
76
|
+
*
|
|
77
|
+
* @returns {{ path: string, version: number, isNewVersion: boolean }}
|
|
78
|
+
*/
|
|
79
|
+
export function resolveWritePath(featureDir, base, { force = false } = {}) {
|
|
80
|
+
const files = readDir(featureDir);
|
|
81
|
+
if (!force) {
|
|
82
|
+
const latest = pickLatestDoc(files, base);
|
|
83
|
+
return latest
|
|
84
|
+
? { path: path.join(featureDir, latest.file), version: latest.version, isNewVersion: false }
|
|
85
|
+
: { path: path.join(featureDir, `${base}.md`), version: 1, isNewVersion: false };
|
|
86
|
+
}
|
|
87
|
+
const name = nextDocName(files, base);
|
|
88
|
+
return { path: path.join(featureDir, name), version: parseDocVersion(name, base), isNewVersion: true };
|
|
89
|
+
}
|
|
@@ -109,7 +109,11 @@ Label **modificadora** — `spec-wave:force`: sozinha **não dispara nada**; adi
|
|
|
109
109
|
```bash
|
|
110
110
|
gh issue edit <n> --add-label "spec-wave:force" --add-label "spec-wave:decompose"
|
|
111
111
|
```
|
|
112
|
-
Efeito por comando:
|
|
112
|
+
Efeito por comando:
|
|
113
|
+
- **spec** e **plan** → geram a **próxima versão** em vez de sobrescrever: `spec.md` (v1) → `spec-v2.md` → `spec-v3.md`… As versões anteriores ficam intactas e a **maior versão passa a ser o documento atual** (veja *Documentos versionados*).
|
|
114
|
+
- **decompose** → ignora o guard e **fecha as sub-issues da decomposição anterior** antes de gerar as novas (veja *Guard de idempotência*).
|
|
115
|
+
|
|
116
|
+
Sem a label, `spec`/`plan` **sobrescrevem o documento atual** (comportamento de sempre).
|
|
113
117
|
|
|
114
118
|
A etapa **🚧 Desenvolvimento** é coberta pelo comando **local** `npx @spec-wave/cli@latest implement <número>` (não é uma label/Action): lê uma **Feature** (todas as Stories pendentes, em ordem de dependência), uma Story ou uma Task e aciona o spec-kit para implementar. Veja `/spec-wave implement`.
|
|
115
119
|
|
|
@@ -246,6 +250,22 @@ Após o `generate-plan` e **antes** da criação de issues no `decompose`, um se
|
|
|
246
250
|
- **Fluxo de resolução:** (1) leia o comentário 🔎 na issue; (2) corrija `spec.md`/`plan.md` (regenere com as labels ou edite e commite); (3) remova a label: `gh issue edit <n> --remove-label "spec-wave:critique-failed"`; (4) re-aplique `spec-wave:ready` para validar de novo.
|
|
247
251
|
- Findings leves não bloqueiam — trate-os como revisão de qualidade.
|
|
248
252
|
|
|
253
|
+
### Documentos versionados (`spec-v2.md`, `plan-v3.md`…)
|
|
254
|
+
|
|
255
|
+
A primeira geração escreve `spec.md` e `plan.md` — a **v1**. Cada regeração **forçada** (label `spec-wave:force`) grava a **próxima versão** ao lado, sem tocar nas anteriores:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
docs/features/<slug>/
|
|
259
|
+
spec.md ← v1
|
|
260
|
+
spec-v2.md ← 1ª regeração forçada
|
|
261
|
+
spec-v3.md ← 2ª regeração forçada ⬅ spec ATUAL
|
|
262
|
+
plan.md ⬅ plano ATUAL (spec e plan versionam independente)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**A maior versão é sempre o documento atual** — é ela que o `generate-plan` usa como contexto, que o `validate` valida, que o `decompose` lê e que o `implement` anexa. Uma regeração **sem** force sobrescreve essa versão atual (não volta para o `spec.md` original).
|
|
266
|
+
|
|
267
|
+
Ao mostrar os documentos ao usuário, use a versão atual; ofereça as anteriores só se ele quiser comparar. Nunca renomeie nem apague versões antigas por conta própria — elas são o histórico da Feature.
|
|
268
|
+
|
|
249
269
|
### Guard de idempotência do decompose (`spec-wave:decomposed`)
|
|
250
270
|
|
|
251
271
|
O evento `labeled` pode redisparar (re-add da label, retry de runner). Para não duplicar Stories/Tasks, o `decompose` **pula** quando a issue já tem a label **`spec-wave:decomposed`** ou já tem sub-issues do tipo-alvo (`[STORY]` para Feature, `[TASK]` para RFC). Ao concluir com sucesso, o Action grava a label. Os workflows ainda usam `concurrency` por issue para serializar runs simultâneos.
|
|
@@ -416,7 +436,10 @@ Inicia a geração da **especificação funcional** para uma Feature. É o **pri
|
|
|
416
436
|
```
|
|
417
437
|
3. Informe: "Label `spec-wave:spec` adicionada. O GitHub Action `generate-spec.yml` irá gerar o `spec.md` automaticamente."
|
|
418
438
|
4. Após a conclusão, ofereça revisar o spec.md gerado em `docs/features/<slug>/spec.md`.
|
|
419
|
-
5. **Para regerar** (a spec já existe e o usuário quer outra versão):
|
|
439
|
+
5. **Para regerar** (a spec já existe e o usuário quer outra versão): adicione `spec-wave:force` junto com o gatilho — o Action grava a **próxima versão** (`spec-v2.md`, `spec-v3.md`…) e preserva as anteriores; a nova passa a ser a spec atual. Sem o force, a versão atual é **sobrescrita** (edições manuais se perdem) — avise antes de acionar assim.
|
|
440
|
+
```bash
|
|
441
|
+
gh issue edit <número> --add-label "spec-wave:force" --add-label "spec-wave:spec"
|
|
442
|
+
```
|
|
420
443
|
6. Próximo passo: gerar o plano técnico — mova para **📋 Plan** e use `/spec-wave plan <número>`.
|
|
421
444
|
|
|
422
445
|
---
|
|
@@ -436,7 +459,7 @@ O plano técnico segue o schema do RFC-002 §3.2: **Estratégia Técnica** (com
|
|
|
436
459
|
```
|
|
437
460
|
4. Informe: "Label `spec-wave:plan` adicionada. O GitHub Action `generate-plan.yml` irá gerar o `plan.md` automaticamente. Acompanhe em: Actions → Generate Plan."
|
|
438
461
|
5. Após a conclusão (cheque comentários na issue ou aguarde confirmação do usuário), ofereça revisar o plan.md gerado em `docs/features/<slug>/plan.md`.
|
|
439
|
-
6. **Para regerar**:
|
|
462
|
+
6. **Para regerar**: adicione `spec-wave:force` junto com `spec-wave:plan` — grava `plan-v2.md`, `plan-v3.md`… preservando as anteriores. Sem o force, o plano atual é sobrescrito (confirme antes).
|
|
440
463
|
7. Próximo passo: validar a Feature — mova para **✅ Ready** e use `/spec-wave ready <número>`.
|
|
441
464
|
|
|
442
465
|
---
|
|
@@ -695,8 +718,12 @@ Audita um Pull Request e corrige automaticamente os problemas encontrados — se
|
|
|
695
718
|
docs/
|
|
696
719
|
features/
|
|
697
720
|
<slug-da-feature>/
|
|
698
|
-
spec.md
|
|
699
|
-
|
|
721
|
+
spec.md ← gerado quando spec-wave:spec é adicionado (1º) = v1
|
|
722
|
+
spec-v2.md ← regeração forçada (spec-wave:force); v1 preservada
|
|
723
|
+
plan.md ← gerado quando spec-wave:plan é adicionado (2º, usa a spec ATUAL) = v1
|
|
724
|
+
plan-v2.md ← regeração forçada do plano
|
|
700
725
|
```
|
|
701
726
|
|
|
727
|
+
A **maior versão de cada tipo é o documento atual** (aqui: `spec-v2.md` e `plan-v2.md`) — veja *Documentos versionados*.
|
|
728
|
+
|
|
702
729
|
O slug é gerado a partir do título da issue: `[FEATURE] Cadastro de Pedidos com PIX` → `cadastro-de-pedidos-com-pix`
|