trackfw 1.0.4 → 2.0.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 +17 -8
- package/package.json +1 -1
- package/src/commands/adr.js +9 -8
- package/src/commands/discover.js +359 -0
- package/src/commands/index.js +1 -0
- package/src/commands/init.js +50 -19
- package/src/commands/log.js +5 -4
- package/src/commands/plugins.js +11 -10
- package/src/commands/req.js +11 -10
- package/src/commands/roadmap.js +6 -5
- package/src/commands/status.js +2 -1
- package/src/commands/validate.js +5 -4
- package/src/config/index.js +92 -0
- package/src/generators/adr.js +6 -5
- package/src/generators/init.js +66 -0
- package/src/generators/req.js +3 -2
- package/src/generators/roadmap.js +150 -57
- package/src/i18n/index.js +53 -0
- package/src/i18n/locales/en-US.json +122 -0
- package/src/i18n/locales/es-ES.json +122 -0
- package/src/i18n/locales/pt-BR.json +122 -0
- package/src/validator/index.js +196 -97
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"init": {
|
|
3
|
+
"description": "Inicializa a governança trackfw no projeto atual",
|
|
4
|
+
"prompt": {
|
|
5
|
+
"projectName": "Nome do projeto?",
|
|
6
|
+
"projectType": "Tipo de projeto?",
|
|
7
|
+
"frontendStack": "Stack de frontend?",
|
|
8
|
+
"pkgManager": "Gerenciador de pacotes?",
|
|
9
|
+
"backendLang": "Linguagem de backend?",
|
|
10
|
+
"backendFramework": "Framework de backend?",
|
|
11
|
+
"gitHooks": "Git hooks?",
|
|
12
|
+
"ci": "Sistema de CI?",
|
|
13
|
+
"aiTools": "Quais assistentes de IA você usa?",
|
|
14
|
+
"projectType_fullstack": "Full-stack (frontend + backend)",
|
|
15
|
+
"projectType_frontend": "Somente frontend",
|
|
16
|
+
"projectType_backend": "Somente backend",
|
|
17
|
+
"projectType_governance": "Somente governança (sem stack de build)"
|
|
18
|
+
},
|
|
19
|
+
"success": "✓ trackfw inicializado — execute 'trackfw status' para ver o estado de governança."
|
|
20
|
+
},
|
|
21
|
+
"adr": {
|
|
22
|
+
"description": "Gerenciar Architecture Decision Records",
|
|
23
|
+
"new": {
|
|
24
|
+
"description": "Criar um novo Architecture Decision Record",
|
|
25
|
+
"prompt": {
|
|
26
|
+
"title": "Título do ADR?",
|
|
27
|
+
"status": "Status inicial?",
|
|
28
|
+
"context": "Contexto (o que motiva esta decisão)?",
|
|
29
|
+
"decision": "Decisão (o que foi decidido)?",
|
|
30
|
+
"consequences": "Consequências (positivas e negativas)?",
|
|
31
|
+
"alternatives": "Alternativas consideradas?"
|
|
32
|
+
},
|
|
33
|
+
"created": "✓ ADR criado: {{path}}"
|
|
34
|
+
},
|
|
35
|
+
"list": {
|
|
36
|
+
"description": "Listar todos os ADRs com status",
|
|
37
|
+
"empty": "Nenhum ADR encontrado em docs/adr/"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"req": {
|
|
41
|
+
"description": "Gerenciar Requisitos",
|
|
42
|
+
"new": {
|
|
43
|
+
"description": "Criar um novo requisito",
|
|
44
|
+
"prompt": {
|
|
45
|
+
"title": "Requisito do projeto",
|
|
46
|
+
"motivation": "Motivação (por que isso é necessário)?",
|
|
47
|
+
"criteria": "Critérios de aceite (um por linha)",
|
|
48
|
+
"domainQuestion_authentication": "Como os usuários serão autenticados?",
|
|
49
|
+
"domainQuestion_ui": "Existe um framework de UI ou design system já escolhido?",
|
|
50
|
+
"domainQuestion_persistence": "Qual engine de banco de dados será usada?",
|
|
51
|
+
"domainQuestion_api": "Qual protocolo de API será usado?",
|
|
52
|
+
"domainQuestion_deploy": "Qual é o destino de deploy?",
|
|
53
|
+
"domainQuestion_events": "Qual message broker será usado?"
|
|
54
|
+
},
|
|
55
|
+
"detectedDomains": "Domínios detectados: {{domains}}",
|
|
56
|
+
"created": "✓ REQ criado: {{path}}",
|
|
57
|
+
"adrDraftsCreated": "ADR drafts criados:",
|
|
58
|
+
"resolveADRs": "Resolva estes ADRs (defina Status: Accepted) antes de criar um roadmap.",
|
|
59
|
+
"adrWarning": "aviso: não foi possível criar ADR draft para {{slug}}: {{message}}"
|
|
60
|
+
},
|
|
61
|
+
"list": {
|
|
62
|
+
"description": "Listar todos os REQs com status",
|
|
63
|
+
"empty": "Nenhum REQ encontrado em docs/req/"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"roadmap": {
|
|
67
|
+
"description": "Gerenciar Roadmaps",
|
|
68
|
+
"list": {
|
|
69
|
+
"description": "Listar todos os roadmaps agrupados por estado",
|
|
70
|
+
"empty": "Nenhum roadmap encontrado."
|
|
71
|
+
},
|
|
72
|
+
"show": {
|
|
73
|
+
"description": "Exibir roadmap pelo nome (correspondência parcial)",
|
|
74
|
+
"notFound": "Roadmap não encontrado: {{name}}"
|
|
75
|
+
},
|
|
76
|
+
"move": {
|
|
77
|
+
"description": "Mover um roadmap entre estados (backlog|wip|blocked|done|abandoned)",
|
|
78
|
+
"success": "✓ Movido {{name}} → {{state}}",
|
|
79
|
+
"notFound": "Roadmap não encontrado: {{name}}"
|
|
80
|
+
},
|
|
81
|
+
"new": {
|
|
82
|
+
"description": "Criar um novo roadmap a partir de uma REQ",
|
|
83
|
+
"created": "✓ Roadmap criado: {{path}}"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"validate": {
|
|
87
|
+
"description": "Validar regras de governança (use como gate de CI)",
|
|
88
|
+
"ok": "✓ Nenhuma violação encontrada.",
|
|
89
|
+
"violations": "✗ Violações ({{count}}):",
|
|
90
|
+
"warnings": "⚠ Avisos ({{count}}):"
|
|
91
|
+
},
|
|
92
|
+
"status": {
|
|
93
|
+
"description": "Exibir o estado atual de governança do projeto"
|
|
94
|
+
},
|
|
95
|
+
"log": {
|
|
96
|
+
"description": "Exibir histórico de transições de estado dos roadmaps",
|
|
97
|
+
"empty": "Nenhuma transição registrada ainda.",
|
|
98
|
+
"tail": "Número de transições recentes a exibir",
|
|
99
|
+
"header": "── trackfw log ─────────────────────────"
|
|
100
|
+
},
|
|
101
|
+
"plugins": {
|
|
102
|
+
"description": "Gerenciar plugins do trackfw",
|
|
103
|
+
"list": {
|
|
104
|
+
"description": "Listar plugins instalados",
|
|
105
|
+
"empty": "Nenhum plugin instalado. Use `trackfw plugins add <user/repo>` para instalar um."
|
|
106
|
+
},
|
|
107
|
+
"add": {
|
|
108
|
+
"description": "Instalar um plugin do GitHub Releases (user/repo ou user/repo@tag)",
|
|
109
|
+
"installing": "Instalando plugin de {{repo}}...",
|
|
110
|
+
"success": "Plugin \"{{name}}\" instalado com sucesso."
|
|
111
|
+
},
|
|
112
|
+
"remove": {
|
|
113
|
+
"description": "Remover um plugin instalado",
|
|
114
|
+
"success": "Plugin \"{{name}}\" removido."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"errors": {
|
|
118
|
+
"notFound": "Não encontrado: {{path}}",
|
|
119
|
+
"downloadFailed": "falha no download: HTTP {{status}} para {{url}}",
|
|
120
|
+
"pluginNotFound": "plugin \"{{name}}\" não encontrado"
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/validator/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const config = require('../config')
|
|
5
6
|
|
|
6
7
|
const STALE_WIP_DAYS = 7
|
|
7
8
|
|
|
@@ -21,6 +22,22 @@ function listDir(dir) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
// resolveWIPDirs retorna todos os diretórios wip/ conforme o modo de namespacing.
|
|
26
|
+
function resolveWIPDirs(cfg) {
|
|
27
|
+
if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
|
|
28
|
+
let agents = cfg.agents || []
|
|
29
|
+
if (agents.length === 0) {
|
|
30
|
+
try {
|
|
31
|
+
agents = fs.readdirSync(cfg.roadmapDir).filter(f => {
|
|
32
|
+
try { return fs.statSync(path.join(cfg.roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
33
|
+
})
|
|
34
|
+
} catch (_) { agents = [] }
|
|
35
|
+
}
|
|
36
|
+
return agents.map(agent => cfg.roadmapDir + '/' + agent + '/wip')
|
|
37
|
+
}
|
|
38
|
+
return [cfg.roadmapDir + '/wip']
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
// parseBlockedADRs extrai basenames de ADRs da seção "## Blocked by ADRs" de um arquivo REQ.
|
|
25
42
|
function parseBlockedADRs(filePath) {
|
|
26
43
|
let content
|
|
@@ -51,40 +68,52 @@ function parseBlockedADRs(filePath) {
|
|
|
51
68
|
return adrs
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
// adrIsDraft verifica se
|
|
71
|
+
// adrIsDraft verifica se <adrBasename> contém "Status: Draft" em alguma das adrDirs configuradas.
|
|
55
72
|
function adrIsDraft(basename) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
const cfg = config.load()
|
|
74
|
+
for (const adrDir of cfg.adrDirs) {
|
|
75
|
+
const p = path.join(adrDir, basename)
|
|
76
|
+
if (fs.existsSync(p)) {
|
|
77
|
+
try {
|
|
78
|
+
return fs.readFileSync(p, 'utf8').includes('Status: Draft')
|
|
79
|
+
} catch (_) {
|
|
80
|
+
// ignorar erro de leitura
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
}
|
|
84
|
+
return false
|
|
62
85
|
}
|
|
63
86
|
|
|
64
|
-
// validateWIPHasREQ — roadmaps em
|
|
87
|
+
// validateWIPHasREQ — roadmaps em wip/ sem "REQ:" no conteúdo → violation
|
|
88
|
+
// Suporta modo by_agent via resolveWIPDirs.
|
|
65
89
|
function validateWIPHasREQ() {
|
|
66
|
-
const
|
|
90
|
+
const cfg = config.load()
|
|
91
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
67
92
|
const violations = []
|
|
68
|
-
for (const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
for (const wipDir of wipDirs) {
|
|
94
|
+
const entries = listDir(wipDir)
|
|
95
|
+
for (const name of entries) {
|
|
96
|
+
try {
|
|
97
|
+
const content = fs.readFileSync(path.join(wipDir, name), 'utf8')
|
|
98
|
+
if (!content.includes('REQ:') || content.includes('REQ: \n')) {
|
|
99
|
+
violations.push(`roadmap "${name}" is in wip but has no linked REQ`)
|
|
100
|
+
}
|
|
101
|
+
} catch (_) {
|
|
102
|
+
// ignorar erro de leitura
|
|
73
103
|
}
|
|
74
|
-
} catch (_) {
|
|
75
|
-
// ignorar erro de leitura
|
|
76
104
|
}
|
|
77
105
|
}
|
|
78
106
|
return violations
|
|
79
107
|
}
|
|
80
108
|
|
|
81
|
-
// validateREQsHaveADR — REQs em
|
|
109
|
+
// validateREQsHaveADR — REQs em <reqDir>/ sem "ADR:" no conteúdo → violation
|
|
82
110
|
function validateREQsHaveADR() {
|
|
83
|
-
const
|
|
111
|
+
const cfg = config.load()
|
|
112
|
+
const entries = listDir(cfg.reqDir)
|
|
84
113
|
const violations = []
|
|
85
114
|
for (const name of entries) {
|
|
86
115
|
try {
|
|
87
|
-
const content = fs.readFileSync(path.join(
|
|
116
|
+
const content = fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
|
|
88
117
|
if (!content.includes('ADR:') || content.includes('ADR: \n')) {
|
|
89
118
|
violations.push(`req "${name}" has no linked ADR`)
|
|
90
119
|
}
|
|
@@ -95,13 +124,14 @@ function validateREQsHaveADR() {
|
|
|
95
124
|
return violations
|
|
96
125
|
}
|
|
97
126
|
|
|
98
|
-
// validateBlockedHasREQ — roadmaps em
|
|
127
|
+
// validateBlockedHasREQ — roadmaps em <roadmapDir>/blocked/ sem "REQ:" → violation
|
|
99
128
|
function validateBlockedHasREQ() {
|
|
100
|
-
const
|
|
129
|
+
const cfg = config.load()
|
|
130
|
+
const entries = listDir(cfg.roadmapDir + '/blocked')
|
|
101
131
|
const violations = []
|
|
102
132
|
for (const name of entries) {
|
|
103
133
|
try {
|
|
104
|
-
const content = fs.readFileSync(path.join('
|
|
134
|
+
const content = fs.readFileSync(path.join(cfg.roadmapDir + '/blocked', name), 'utf8')
|
|
105
135
|
if (!content.includes('REQ:') || content.includes('REQ: \n')) {
|
|
106
136
|
violations.push(`roadmap "${name}" is in blocked but has no linked REQ`)
|
|
107
137
|
}
|
|
@@ -114,11 +144,12 @@ function validateBlockedHasREQ() {
|
|
|
114
144
|
|
|
115
145
|
// validateREQsHaveRoadmap — REQs sem "Roadmap:" → violation
|
|
116
146
|
function validateREQsHaveRoadmap() {
|
|
117
|
-
const
|
|
147
|
+
const cfg = config.load()
|
|
148
|
+
const entries = listDir(cfg.reqDir)
|
|
118
149
|
const violations = []
|
|
119
150
|
for (const name of entries) {
|
|
120
151
|
try {
|
|
121
|
-
const content = fs.readFileSync(path.join(
|
|
152
|
+
const content = fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
|
|
122
153
|
if (!content.includes('Roadmap:') || content.includes('Roadmap: \n')) {
|
|
123
154
|
violations.push(`req "${name}" has no linked Roadmap`)
|
|
124
155
|
}
|
|
@@ -129,15 +160,19 @@ function validateREQsHaveRoadmap() {
|
|
|
129
160
|
return violations
|
|
130
161
|
}
|
|
131
162
|
|
|
132
|
-
// validateADRsAreReferenced — ADRs em
|
|
163
|
+
// validateADRsAreReferenced — ADRs em adrDirs não referenciados em nenhuma REQ → violation
|
|
133
164
|
function validateADRsAreReferenced() {
|
|
134
|
-
const
|
|
135
|
-
|
|
165
|
+
const cfg = config.load()
|
|
166
|
+
let adrs = []
|
|
167
|
+
for (const adrDir of cfg.adrDirs) {
|
|
168
|
+
adrs = adrs.concat(listDir(adrDir))
|
|
169
|
+
}
|
|
136
170
|
|
|
171
|
+
const reqEntries = listDir(cfg.reqDir)
|
|
137
172
|
let combined = ''
|
|
138
173
|
for (const name of reqEntries) {
|
|
139
174
|
try {
|
|
140
|
-
combined += fs.readFileSync(path.join(
|
|
175
|
+
combined += fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
|
|
141
176
|
} catch (_) {
|
|
142
177
|
// ignorar
|
|
143
178
|
}
|
|
@@ -153,63 +188,104 @@ function validateADRsAreReferenced() {
|
|
|
153
188
|
}
|
|
154
189
|
|
|
155
190
|
// validateWIPHasAcceptanceCriteria — roadmaps wip sem bloco de critérios de aceite → violation
|
|
191
|
+
// Suporta modo by_agent via resolveWIPDirs.
|
|
156
192
|
function validateWIPHasAcceptanceCriteria() {
|
|
157
|
-
const
|
|
193
|
+
const cfg = config.load()
|
|
194
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
158
195
|
const violations = []
|
|
159
|
-
for (const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
content.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
196
|
+
for (const wipDir of wipDirs) {
|
|
197
|
+
const entries = listDir(wipDir)
|
|
198
|
+
for (const name of entries) {
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(path.join(wipDir, name), 'utf8')
|
|
201
|
+
const hasBlock =
|
|
202
|
+
content.includes('## Acceptance Criteria') ||
|
|
203
|
+
content.includes('## Critérios de Aceite') ||
|
|
204
|
+
content.includes('acceptance criteria') ||
|
|
205
|
+
content.includes('Acceptance Criteria:')
|
|
206
|
+
if (!hasBlock) {
|
|
207
|
+
violations.push(`roadmap "${name}" is in wip but has no acceptance criteria block`)
|
|
208
|
+
}
|
|
209
|
+
} catch (_) {
|
|
210
|
+
// ignorar
|
|
169
211
|
}
|
|
170
|
-
} catch (_) {
|
|
171
|
-
// ignorar
|
|
172
212
|
}
|
|
173
213
|
}
|
|
174
214
|
return violations
|
|
175
215
|
}
|
|
176
216
|
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
217
|
+
// validateWIPLimit — mais de wipLimit roadmaps em wip → warning.
|
|
218
|
+
// Em modo by_agent, verifica por agente individualmente.
|
|
219
|
+
function validateWIPLimit() {
|
|
220
|
+
const cfg = config.load()
|
|
221
|
+
|
|
222
|
+
if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
|
|
223
|
+
let agents = cfg.agents || []
|
|
224
|
+
if (agents.length === 0) {
|
|
225
|
+
try {
|
|
226
|
+
agents = fs.readdirSync(cfg.roadmapDir).filter(f => {
|
|
227
|
+
try { return fs.statSync(path.join(cfg.roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
228
|
+
})
|
|
229
|
+
} catch (_) { agents = [] }
|
|
230
|
+
}
|
|
231
|
+
const warnings = []
|
|
232
|
+
const limit = cfg.wipLimit > 0 ? cfg.wipLimit : 1
|
|
233
|
+
for (const agent of agents) {
|
|
234
|
+
const dir = cfg.roadmapDir + '/' + agent + '/wip'
|
|
235
|
+
const entries = listDir(dir)
|
|
236
|
+
if (entries.length > limit) {
|
|
237
|
+
warnings.push(`${entries.length} roadmaps in wip/ for agent "${agent}" (limit: ${limit})`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return warnings
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const entries = listDir(cfg.roadmapDir + '/wip')
|
|
244
|
+
const limit = cfg.wipLimit > 0 ? cfg.wipLimit : 1
|
|
245
|
+
if (entries.length > limit) {
|
|
246
|
+
return [`${entries.length} roadmaps in wip/ (limit: ${limit})`]
|
|
182
247
|
}
|
|
183
248
|
return []
|
|
184
249
|
}
|
|
185
250
|
|
|
251
|
+
// validateSingleWIP — alias retrocompatível de validateWIPLimit (modo flat)
|
|
252
|
+
function validateSingleWIP() {
|
|
253
|
+
return validateWIPLimit()
|
|
254
|
+
}
|
|
255
|
+
|
|
186
256
|
// validateStaleWIP — roadmaps wip com mtime >= 7 dias → warning
|
|
257
|
+
// Suporta modo by_agent via resolveWIPDirs.
|
|
187
258
|
function validateStaleWIP() {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
files = fs.readdirSync('docs/roadmaps/wip')
|
|
191
|
-
.filter(f => f.endsWith('.md'))
|
|
192
|
-
.map(f => path.join('docs/roadmaps/wip', f))
|
|
193
|
-
} catch (_) {
|
|
194
|
-
return []
|
|
195
|
-
}
|
|
196
|
-
|
|
259
|
+
const cfg = config.load()
|
|
260
|
+
const wipDirs = resolveWIPDirs(cfg)
|
|
197
261
|
const warnings = []
|
|
198
262
|
const now = Date.now()
|
|
199
|
-
|
|
263
|
+
|
|
264
|
+
for (const wipDir of wipDirs) {
|
|
265
|
+
let files = []
|
|
200
266
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (days >= STALE_WIP_DAYS) {
|
|
205
|
-
const lastModified = stat.mtime.toISOString().slice(0, 10)
|
|
206
|
-
const basename = path.basename(filePath)
|
|
207
|
-
warnings.push(
|
|
208
|
-
`roadmap/wip/${basename} has been in WIP for ${days} days (last modified ${lastModified})`
|
|
209
|
-
)
|
|
210
|
-
}
|
|
267
|
+
files = fs.readdirSync(wipDir)
|
|
268
|
+
.filter(f => f.endsWith('.md'))
|
|
269
|
+
.map(f => path.join(wipDir, f))
|
|
211
270
|
} catch (_) {
|
|
212
|
-
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const filePath of files) {
|
|
275
|
+
try {
|
|
276
|
+
const stat = fs.statSync(filePath)
|
|
277
|
+
const ageMs = now - stat.mtimeMs
|
|
278
|
+
const days = Math.floor(ageMs / (1000 * 60 * 60 * 24))
|
|
279
|
+
if (days >= STALE_WIP_DAYS) {
|
|
280
|
+
const lastModified = stat.mtime.toISOString().slice(0, 10)
|
|
281
|
+
const basename = path.basename(filePath)
|
|
282
|
+
warnings.push(
|
|
283
|
+
`roadmap/wip/${basename} has been in WIP for ${days} days (last modified ${lastModified})`
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
} catch (_) {
|
|
287
|
+
// ignorar
|
|
288
|
+
}
|
|
213
289
|
}
|
|
214
290
|
}
|
|
215
291
|
return warnings
|
|
@@ -217,10 +293,11 @@ function validateStaleWIP() {
|
|
|
217
293
|
|
|
218
294
|
// validateREQsNotBlockedByDraftADRs — REQs Open com ADRs Draft na seção "## Blocked by ADRs" → violation
|
|
219
295
|
function validateREQsNotBlockedByDraftADRs() {
|
|
220
|
-
const
|
|
296
|
+
const cfg = config.load()
|
|
297
|
+
const entries = listDir(cfg.reqDir)
|
|
221
298
|
const violations = []
|
|
222
299
|
for (const name of entries) {
|
|
223
|
-
const filePath = path.join(
|
|
300
|
+
const filePath = path.join(cfg.reqDir, name)
|
|
224
301
|
let content
|
|
225
302
|
try {
|
|
226
303
|
content = fs.readFileSync(filePath, 'utf8')
|
|
@@ -241,10 +318,11 @@ function validateREQsNotBlockedByDraftADRs() {
|
|
|
241
318
|
|
|
242
319
|
// blockedREQs retorna mapa de reqBasename → [adrBasenames Draft] para uso em getStatus()
|
|
243
320
|
function blockedREQs() {
|
|
244
|
-
const
|
|
321
|
+
const cfg = config.load()
|
|
322
|
+
const entries = listDir(cfg.reqDir)
|
|
245
323
|
const result = {}
|
|
246
324
|
for (const name of entries) {
|
|
247
|
-
const filePath = path.join(
|
|
325
|
+
const filePath = path.join(cfg.reqDir, name)
|
|
248
326
|
let content
|
|
249
327
|
try {
|
|
250
328
|
content = fs.readFileSync(filePath, 'utf8')
|
|
@@ -274,7 +352,7 @@ async function validate() {
|
|
|
274
352
|
...validateREQsNotBlockedByDraftADRs(),
|
|
275
353
|
]
|
|
276
354
|
const warnings = [
|
|
277
|
-
...
|
|
355
|
+
...validateWIPLimit(),
|
|
278
356
|
...validateStaleWIP(),
|
|
279
357
|
]
|
|
280
358
|
return { violations, warnings }
|
|
@@ -282,40 +360,59 @@ async function validate() {
|
|
|
282
360
|
|
|
283
361
|
// getStatus retorna string formatada com o status de governança do projeto
|
|
284
362
|
async function getStatus() {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
const done = listDir('docs/roadmaps/done')
|
|
363
|
+
const cfg = config.load()
|
|
364
|
+
let out = '── trackfw status ──────────────────────\n'
|
|
288
365
|
|
|
289
|
-
|
|
290
|
-
|
|
366
|
+
if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
|
|
367
|
+
let agents = cfg.agents || []
|
|
368
|
+
if (agents.length === 0) {
|
|
369
|
+
try {
|
|
370
|
+
agents = fs.readdirSync(cfg.roadmapDir).filter(f => {
|
|
371
|
+
try { return fs.statSync(path.join(cfg.roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
372
|
+
})
|
|
373
|
+
} catch (_) { agents = [] }
|
|
374
|
+
}
|
|
375
|
+
out += '\n⚙ WIP by Agent\n'
|
|
376
|
+
for (const agent of agents) {
|
|
377
|
+
const wip = listDir(cfg.roadmapDir + '/' + agent + '/wip')
|
|
378
|
+
if (wip.length > 0) {
|
|
379
|
+
out += ` [${agent}] WIP (${wip.length})\n`
|
|
380
|
+
wip.forEach(f => { out += ` ${f}\n` })
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
const wip = listDir(cfg.roadmapDir + '/wip')
|
|
385
|
+
const blocked = listDir(cfg.roadmapDir + '/blocked')
|
|
386
|
+
const done = listDir(cfg.roadmapDir + '/done')
|
|
291
387
|
|
|
292
|
-
|
|
293
|
-
|
|
388
|
+
out += `\n🔄 WIP (${wip.length})\n`
|
|
389
|
+
for (const f of wip) out += ` ${f}\n`
|
|
294
390
|
|
|
295
|
-
|
|
296
|
-
|
|
391
|
+
out += `\n❌ Blocked (${blocked.length})\n`
|
|
392
|
+
for (const f of blocked) out += ` ${f}\n`
|
|
297
393
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
394
|
+
const staleWIPs = validateStaleWIP()
|
|
395
|
+
if (staleWIPs.length > 0) {
|
|
396
|
+
out += `\n⚠ Stale WIP (${staleWIPs.length})\n`
|
|
397
|
+
for (const w of staleWIPs) out += ` ${w}\n`
|
|
398
|
+
}
|
|
303
399
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
400
|
+
const blockedByDraft = blockedREQs()
|
|
401
|
+
const blockedKeys = Object.keys(blockedByDraft)
|
|
402
|
+
if (blockedKeys.length > 0) {
|
|
403
|
+
out += `\n⏳ REQs blocked by Draft ADRs (${blockedKeys.length})\n`
|
|
404
|
+
for (const reqFile of blockedKeys) {
|
|
405
|
+
out += ` ${reqFile}\n`
|
|
406
|
+
for (const adr of blockedByDraft[reqFile]) {
|
|
407
|
+
out += ` → ${adr} (Draft)\n`
|
|
408
|
+
}
|
|
312
409
|
}
|
|
313
410
|
}
|
|
314
|
-
}
|
|
315
411
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
412
|
+
out += `\n✅ Done (last 5)\n`
|
|
413
|
+
const last5 = done.length > 5 ? done.slice(done.length - 5) : done
|
|
414
|
+
for (const f of last5) out += ` ${f}\n`
|
|
415
|
+
}
|
|
319
416
|
|
|
320
417
|
out += '\n────────────────────────────────────────\n'
|
|
321
418
|
return out
|
|
@@ -331,10 +428,12 @@ module.exports = {
|
|
|
331
428
|
validateREQsHaveRoadmap,
|
|
332
429
|
validateADRsAreReferenced,
|
|
333
430
|
validateWIPHasAcceptanceCriteria,
|
|
431
|
+
validateWIPLimit,
|
|
334
432
|
validateSingleWIP,
|
|
335
433
|
validateStaleWIP,
|
|
336
434
|
validateREQsNotBlockedByDraftADRs,
|
|
337
435
|
parseBlockedADRs,
|
|
338
436
|
adrIsDraft,
|
|
339
437
|
listDir,
|
|
438
|
+
resolveWIPDirs,
|
|
340
439
|
}
|