trackfw 1.0.3 → 1.1.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/bin/trackfw +4 -0
- package/package.json +10 -13
- package/src/commands/adr.js +31 -0
- package/src/commands/index.js +28 -0
- package/src/commands/init.js +174 -0
- package/src/commands/log.js +30 -0
- package/src/commands/plugins.js +97 -0
- package/src/commands/req.js +70 -0
- package/src/commands/roadmap.js +37 -0
- package/src/commands/status.js +12 -0
- package/src/commands/validate.js +29 -0
- package/src/generators/adr.js +172 -0
- package/src/generators/init.js +702 -0
- package/src/generators/req.js +239 -0
- package/src/generators/roadmap.js +224 -0
- 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 +340 -0
- package/bin/.gitkeep +0 -0
- package/bin/README.md +0 -301
- package/bin/trackfw-darwin-amd64 +0 -0
- package/bin/trackfw-darwin-arm64 +0 -0
- package/bin/trackfw-linux-amd64 +0 -0
- package/bin/trackfw-linux-arm64 +0 -0
- package/bin/trackfw-windows-amd64.exe +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"init": {
|
|
3
|
+
"description": "Inicializa la gobernanza trackfw en el proyecto actual",
|
|
4
|
+
"prompt": {
|
|
5
|
+
"projectName": "¿Nombre del proyecto?",
|
|
6
|
+
"projectType": "¿Tipo de proyecto?",
|
|
7
|
+
"frontendStack": "¿Stack de frontend?",
|
|
8
|
+
"pkgManager": "¿Gestor de paquetes?",
|
|
9
|
+
"backendLang": "¿Lenguaje de backend?",
|
|
10
|
+
"backendFramework": "¿Framework de backend?",
|
|
11
|
+
"gitHooks": "¿Git hooks?",
|
|
12
|
+
"ci": "¿Sistema de CI?",
|
|
13
|
+
"aiTools": "¿Qué asistentes de IA usas?",
|
|
14
|
+
"projectType_fullstack": "Full-stack (frontend + backend)",
|
|
15
|
+
"projectType_frontend": "Solo frontend",
|
|
16
|
+
"projectType_backend": "Solo backend",
|
|
17
|
+
"projectType_governance": "Solo gobernanza (sin stack de build)"
|
|
18
|
+
},
|
|
19
|
+
"success": "✓ trackfw inicializado — ejecuta 'trackfw status' para ver el estado de gobernanza."
|
|
20
|
+
},
|
|
21
|
+
"adr": {
|
|
22
|
+
"description": "Gestionar Architecture Decision Records",
|
|
23
|
+
"new": {
|
|
24
|
+
"description": "Crear un nuevo Architecture Decision Record",
|
|
25
|
+
"prompt": {
|
|
26
|
+
"title": "¿Título del ADR?",
|
|
27
|
+
"status": "¿Estado inicial?",
|
|
28
|
+
"context": "¿Contexto (qué motiva esta decisión)?",
|
|
29
|
+
"decision": "¿Decisión (qué fue decidido)?",
|
|
30
|
+
"consequences": "¿Consecuencias (positivas y negativas)?",
|
|
31
|
+
"alternatives": "¿Alternativas consideradas?"
|
|
32
|
+
},
|
|
33
|
+
"created": "✓ ADR creado: {{path}}"
|
|
34
|
+
},
|
|
35
|
+
"list": {
|
|
36
|
+
"description": "Listar todos los ADRs con estado",
|
|
37
|
+
"empty": "No se encontraron ADRs en docs/adr/"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"req": {
|
|
41
|
+
"description": "Gestionar Requisitos",
|
|
42
|
+
"new": {
|
|
43
|
+
"description": "Crear un nuevo requisito",
|
|
44
|
+
"prompt": {
|
|
45
|
+
"title": "Requisito del proyecto",
|
|
46
|
+
"motivation": "¿Motivación (por qué lo necesitamos)?",
|
|
47
|
+
"criteria": "Criterios de aceptación (uno por línea)",
|
|
48
|
+
"domainQuestion_authentication": "¿Cómo se autenticarán los usuarios?",
|
|
49
|
+
"domainQuestion_ui": "¿Existe un framework de UI o design system ya elegido?",
|
|
50
|
+
"domainQuestion_persistence": "¿Qué motor de base de datos se usará?",
|
|
51
|
+
"domainQuestion_api": "¿Qué protocolo de API se usará?",
|
|
52
|
+
"domainQuestion_deploy": "¿Cuál es el destino de despliegue?",
|
|
53
|
+
"domainQuestion_events": "¿Qué message broker se usará?"
|
|
54
|
+
},
|
|
55
|
+
"detectedDomains": "Dominios detectados: {{domains}}",
|
|
56
|
+
"created": "✓ REQ creado: {{path}}",
|
|
57
|
+
"adrDraftsCreated": "ADR borradores creados:",
|
|
58
|
+
"resolveADRs": "Resuelve estos ADRs (establece Status: Accepted) antes de crear un roadmap.",
|
|
59
|
+
"adrWarning": "advertencia: no se pudo crear ADR borrador para {{slug}}: {{message}}"
|
|
60
|
+
},
|
|
61
|
+
"list": {
|
|
62
|
+
"description": "Listar todos los REQs con estado",
|
|
63
|
+
"empty": "No se encontraron REQs en docs/req/"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"roadmap": {
|
|
67
|
+
"description": "Gestionar Roadmaps",
|
|
68
|
+
"list": {
|
|
69
|
+
"description": "Listar todos los roadmaps agrupados por estado",
|
|
70
|
+
"empty": "No se encontraron roadmaps."
|
|
71
|
+
},
|
|
72
|
+
"show": {
|
|
73
|
+
"description": "Mostrar roadmap por nombre (coincidencia parcial)",
|
|
74
|
+
"notFound": "Roadmap no encontrado: {{name}}"
|
|
75
|
+
},
|
|
76
|
+
"move": {
|
|
77
|
+
"description": "Mover un roadmap entre estados (backlog|wip|blocked|done|abandoned)",
|
|
78
|
+
"success": "✓ Movido {{name}} → {{state}}",
|
|
79
|
+
"notFound": "Roadmap no encontrado: {{name}}"
|
|
80
|
+
},
|
|
81
|
+
"new": {
|
|
82
|
+
"description": "Crear un nuevo roadmap desde un REQ",
|
|
83
|
+
"created": "✓ Roadmap creado: {{path}}"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"validate": {
|
|
87
|
+
"description": "Validar reglas de gobernanza (úsalo como gate de CI)",
|
|
88
|
+
"ok": "✓ No se encontraron violaciones.",
|
|
89
|
+
"violations": "✗ Violaciones ({{count}}):",
|
|
90
|
+
"warnings": "⚠ Avisos ({{count}}):"
|
|
91
|
+
},
|
|
92
|
+
"status": {
|
|
93
|
+
"description": "Mostrar el estado actual de gobernanza del proyecto"
|
|
94
|
+
},
|
|
95
|
+
"log": {
|
|
96
|
+
"description": "Mostrar historial de transiciones de estado de los roadmaps",
|
|
97
|
+
"empty": "Aún no hay transiciones registradas.",
|
|
98
|
+
"tail": "Número de transiciones recientes a mostrar",
|
|
99
|
+
"header": "── trackfw log ─────────────────────────"
|
|
100
|
+
},
|
|
101
|
+
"plugins": {
|
|
102
|
+
"description": "Gestionar plugins de trackfw",
|
|
103
|
+
"list": {
|
|
104
|
+
"description": "Listar plugins instalados",
|
|
105
|
+
"empty": "No hay plugins instalados. Usa `trackfw plugins add <user/repo>` para instalar uno."
|
|
106
|
+
},
|
|
107
|
+
"add": {
|
|
108
|
+
"description": "Instalar un plugin desde GitHub Releases (user/repo o user/repo@tag)",
|
|
109
|
+
"installing": "Instalando plugin desde {{repo}}...",
|
|
110
|
+
"success": "Plugin \"{{name}}\" instalado correctamente."
|
|
111
|
+
},
|
|
112
|
+
"remove": {
|
|
113
|
+
"description": "Eliminar un plugin instalado",
|
|
114
|
+
"success": "Plugin \"{{name}}\" eliminado."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"errors": {
|
|
118
|
+
"notFound": "No encontrado: {{path}}",
|
|
119
|
+
"downloadFailed": "fallo en la descarga: HTTP {{status}} para {{url}}",
|
|
120
|
+
"pluginNotFound": "plugin \"{{name}}\" no encontrado"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const STALE_WIP_DAYS = 7
|
|
7
|
+
|
|
8
|
+
// listDir retorna array de nomes de arquivo (não-diretórios) em dir.
|
|
9
|
+
// Retorna [] se o diretório não existir.
|
|
10
|
+
function listDir(dir) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.readdirSync(dir).filter(name => {
|
|
13
|
+
try {
|
|
14
|
+
return !fs.statSync(path.join(dir, name)).isDirectory()
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
} catch (_) {
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// parseBlockedADRs extrai basenames de ADRs da seção "## Blocked by ADRs" de um arquivo REQ.
|
|
25
|
+
function parseBlockedADRs(filePath) {
|
|
26
|
+
let content
|
|
27
|
+
try {
|
|
28
|
+
content = fs.readFileSync(filePath, 'utf8')
|
|
29
|
+
} catch (_) {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
const lines = content.split('\n')
|
|
33
|
+
const adrs = []
|
|
34
|
+
let inSection = false
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (line === '## Blocked by ADRs') {
|
|
37
|
+
inSection = true
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (inSection) {
|
|
41
|
+
if (line.startsWith('## ')) break
|
|
42
|
+
if (line.startsWith('- ')) {
|
|
43
|
+
const item = line.slice(2).trim()
|
|
44
|
+
const parts = item.split(/\s+/)
|
|
45
|
+
if (parts.length > 0 && parts[0].endsWith('.md')) {
|
|
46
|
+
adrs.push(parts[0])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return adrs
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// adrIsDraft verifica se docs/adr/<basename> contém "Status: Draft".
|
|
55
|
+
function adrIsDraft(basename) {
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(path.join('docs', 'adr', basename), 'utf8')
|
|
58
|
+
return content.includes('Status: Draft')
|
|
59
|
+
} catch (_) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// validateWIPHasREQ — roadmaps em docs/roadmaps/wip/ sem "REQ:" no conteúdo → violation
|
|
65
|
+
function validateWIPHasREQ() {
|
|
66
|
+
const entries = listDir('docs/roadmaps/wip')
|
|
67
|
+
const violations = []
|
|
68
|
+
for (const name of entries) {
|
|
69
|
+
try {
|
|
70
|
+
const content = fs.readFileSync(path.join('docs/roadmaps/wip', name), 'utf8')
|
|
71
|
+
if (!content.includes('REQ:') || content.includes('REQ: \n')) {
|
|
72
|
+
violations.push(`roadmap "${name}" is in wip but has no linked REQ`)
|
|
73
|
+
}
|
|
74
|
+
} catch (_) {
|
|
75
|
+
// ignorar erro de leitura
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return violations
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// validateREQsHaveADR — REQs em docs/req/ sem "ADR:" no conteúdo → violation
|
|
82
|
+
function validateREQsHaveADR() {
|
|
83
|
+
const entries = listDir('docs/req')
|
|
84
|
+
const violations = []
|
|
85
|
+
for (const name of entries) {
|
|
86
|
+
try {
|
|
87
|
+
const content = fs.readFileSync(path.join('docs/req', name), 'utf8')
|
|
88
|
+
if (!content.includes('ADR:') || content.includes('ADR: \n')) {
|
|
89
|
+
violations.push(`req "${name}" has no linked ADR`)
|
|
90
|
+
}
|
|
91
|
+
} catch (_) {
|
|
92
|
+
// ignorar
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return violations
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// validateBlockedHasREQ — roadmaps em docs/roadmaps/blocked/ sem "REQ:" → violation
|
|
99
|
+
function validateBlockedHasREQ() {
|
|
100
|
+
const entries = listDir('docs/roadmaps/blocked')
|
|
101
|
+
const violations = []
|
|
102
|
+
for (const name of entries) {
|
|
103
|
+
try {
|
|
104
|
+
const content = fs.readFileSync(path.join('docs/roadmaps/blocked', name), 'utf8')
|
|
105
|
+
if (!content.includes('REQ:') || content.includes('REQ: \n')) {
|
|
106
|
+
violations.push(`roadmap "${name}" is in blocked but has no linked REQ`)
|
|
107
|
+
}
|
|
108
|
+
} catch (_) {
|
|
109
|
+
// ignorar
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return violations
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// validateREQsHaveRoadmap — REQs sem "Roadmap:" → violation
|
|
116
|
+
function validateREQsHaveRoadmap() {
|
|
117
|
+
const entries = listDir('docs/req')
|
|
118
|
+
const violations = []
|
|
119
|
+
for (const name of entries) {
|
|
120
|
+
try {
|
|
121
|
+
const content = fs.readFileSync(path.join('docs/req', name), 'utf8')
|
|
122
|
+
if (!content.includes('Roadmap:') || content.includes('Roadmap: \n')) {
|
|
123
|
+
violations.push(`req "${name}" has no linked Roadmap`)
|
|
124
|
+
}
|
|
125
|
+
} catch (_) {
|
|
126
|
+
// ignorar
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return violations
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// validateADRsAreReferenced — ADRs em docs/adr/ não referenciados em nenhuma REQ → violation
|
|
133
|
+
function validateADRsAreReferenced() {
|
|
134
|
+
const adrs = listDir('docs/adr')
|
|
135
|
+
const reqEntries = listDir('docs/req')
|
|
136
|
+
|
|
137
|
+
let combined = ''
|
|
138
|
+
for (const name of reqEntries) {
|
|
139
|
+
try {
|
|
140
|
+
combined += fs.readFileSync(path.join('docs/req', name), 'utf8')
|
|
141
|
+
} catch (_) {
|
|
142
|
+
// ignorar
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const violations = []
|
|
147
|
+
for (const adr of adrs) {
|
|
148
|
+
if (!combined.includes(adr)) {
|
|
149
|
+
violations.push(`adr "${adr}" is not referenced by any REQ`)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return violations
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// validateWIPHasAcceptanceCriteria — roadmaps wip sem bloco de critérios de aceite → violation
|
|
156
|
+
function validateWIPHasAcceptanceCriteria() {
|
|
157
|
+
const entries = listDir('docs/roadmaps/wip')
|
|
158
|
+
const violations = []
|
|
159
|
+
for (const name of entries) {
|
|
160
|
+
try {
|
|
161
|
+
const content = fs.readFileSync(path.join('docs/roadmaps/wip', name), 'utf8')
|
|
162
|
+
const hasBlock =
|
|
163
|
+
content.includes('## Acceptance Criteria') ||
|
|
164
|
+
content.includes('## Critérios de Aceite') ||
|
|
165
|
+
content.includes('acceptance criteria') ||
|
|
166
|
+
content.includes('Acceptance Criteria:')
|
|
167
|
+
if (!hasBlock) {
|
|
168
|
+
violations.push(`roadmap "${name}" is in wip but has no acceptance criteria block`)
|
|
169
|
+
}
|
|
170
|
+
} catch (_) {
|
|
171
|
+
// ignorar
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return violations
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// validateSingleWIP — mais de 1 roadmap em wip → warning
|
|
178
|
+
function validateSingleWIP() {
|
|
179
|
+
const entries = listDir('docs/roadmaps/wip')
|
|
180
|
+
if (entries.length > 1) {
|
|
181
|
+
return [`${entries.length} roadmaps in wip/ (recommended: keep only 1 active at a time)`]
|
|
182
|
+
}
|
|
183
|
+
return []
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// validateStaleWIP — roadmaps wip com mtime >= 7 dias → warning
|
|
187
|
+
function validateStaleWIP() {
|
|
188
|
+
let files = []
|
|
189
|
+
try {
|
|
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
|
+
|
|
197
|
+
const warnings = []
|
|
198
|
+
const now = Date.now()
|
|
199
|
+
for (const filePath of files) {
|
|
200
|
+
try {
|
|
201
|
+
const stat = fs.statSync(filePath)
|
|
202
|
+
const ageMs = now - stat.mtimeMs
|
|
203
|
+
const days = Math.floor(ageMs / (1000 * 60 * 60 * 24))
|
|
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
|
+
}
|
|
211
|
+
} catch (_) {
|
|
212
|
+
// ignorar
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return warnings
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// validateREQsNotBlockedByDraftADRs — REQs Open com ADRs Draft na seção "## Blocked by ADRs" → violation
|
|
219
|
+
function validateREQsNotBlockedByDraftADRs() {
|
|
220
|
+
const entries = listDir('docs/req')
|
|
221
|
+
const violations = []
|
|
222
|
+
for (const name of entries) {
|
|
223
|
+
const filePath = path.join('docs/req', name)
|
|
224
|
+
let content
|
|
225
|
+
try {
|
|
226
|
+
content = fs.readFileSync(filePath, 'utf8')
|
|
227
|
+
} catch (_) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
if (!content.includes('Status: Open')) continue
|
|
231
|
+
|
|
232
|
+
const blockedADRs = parseBlockedADRs(filePath)
|
|
233
|
+
for (const adrBasename of blockedADRs) {
|
|
234
|
+
if (adrIsDraft(adrBasename)) {
|
|
235
|
+
violations.push(`REQ ${name} is blocked by Draft ADR: ${adrBasename}`)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return violations
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// blockedREQs retorna mapa de reqBasename → [adrBasenames Draft] para uso em getStatus()
|
|
243
|
+
function blockedREQs() {
|
|
244
|
+
const entries = listDir('docs/req')
|
|
245
|
+
const result = {}
|
|
246
|
+
for (const name of entries) {
|
|
247
|
+
const filePath = path.join('docs/req', name)
|
|
248
|
+
let content
|
|
249
|
+
try {
|
|
250
|
+
content = fs.readFileSync(filePath, 'utf8')
|
|
251
|
+
} catch (_) {
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (!content.includes('Status: Open')) continue
|
|
255
|
+
|
|
256
|
+
const adrNames = parseBlockedADRs(filePath)
|
|
257
|
+
const draftADRs = adrNames.filter(a => adrIsDraft(a))
|
|
258
|
+
if (draftADRs.length > 0) {
|
|
259
|
+
result[name] = draftADRs
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return result
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// validate executa todas as validações e retorna { violations, warnings }
|
|
266
|
+
async function validate() {
|
|
267
|
+
const violations = [
|
|
268
|
+
...validateWIPHasREQ(),
|
|
269
|
+
...validateREQsHaveADR(),
|
|
270
|
+
...validateBlockedHasREQ(),
|
|
271
|
+
...validateREQsHaveRoadmap(),
|
|
272
|
+
...validateADRsAreReferenced(),
|
|
273
|
+
...validateWIPHasAcceptanceCriteria(),
|
|
274
|
+
...validateREQsNotBlockedByDraftADRs(),
|
|
275
|
+
]
|
|
276
|
+
const warnings = [
|
|
277
|
+
...validateSingleWIP(),
|
|
278
|
+
...validateStaleWIP(),
|
|
279
|
+
]
|
|
280
|
+
return { violations, warnings }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// getStatus retorna string formatada com o status de governança do projeto
|
|
284
|
+
async function getStatus() {
|
|
285
|
+
const wip = listDir('docs/roadmaps/wip')
|
|
286
|
+
const blocked = listDir('docs/roadmaps/blocked')
|
|
287
|
+
const done = listDir('docs/roadmaps/done')
|
|
288
|
+
|
|
289
|
+
let out = ''
|
|
290
|
+
out += '── trackfw status ──────────────────────\n'
|
|
291
|
+
|
|
292
|
+
out += `\n🔄 WIP (${wip.length})\n`
|
|
293
|
+
for (const f of wip) out += ` ${f}\n`
|
|
294
|
+
|
|
295
|
+
out += `\n❌ Blocked (${blocked.length})\n`
|
|
296
|
+
for (const f of blocked) out += ` ${f}\n`
|
|
297
|
+
|
|
298
|
+
const staleWIPs = validateStaleWIP()
|
|
299
|
+
if (staleWIPs.length > 0) {
|
|
300
|
+
out += `\n⚠ Stale WIP (${staleWIPs.length})\n`
|
|
301
|
+
for (const w of staleWIPs) out += ` ${w}\n`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const blockedByDraft = blockedREQs()
|
|
305
|
+
const blockedKeys = Object.keys(blockedByDraft)
|
|
306
|
+
if (blockedKeys.length > 0) {
|
|
307
|
+
out += `\n⏳ REQs blocked by Draft ADRs (${blockedKeys.length})\n`
|
|
308
|
+
for (const reqFile of blockedKeys) {
|
|
309
|
+
out += ` ${reqFile}\n`
|
|
310
|
+
for (const adr of blockedByDraft[reqFile]) {
|
|
311
|
+
out += ` → ${adr} (Draft)\n`
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
out += `\n✅ Done (last 5)\n`
|
|
317
|
+
const last5 = done.length > 5 ? done.slice(done.length - 5) : done
|
|
318
|
+
for (const f of last5) out += ` ${f}\n`
|
|
319
|
+
|
|
320
|
+
out += '\n────────────────────────────────────────\n'
|
|
321
|
+
return out
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = {
|
|
325
|
+
validate,
|
|
326
|
+
getStatus,
|
|
327
|
+
// exportadas para testes unitários
|
|
328
|
+
validateWIPHasREQ,
|
|
329
|
+
validateREQsHaveADR,
|
|
330
|
+
validateBlockedHasREQ,
|
|
331
|
+
validateREQsHaveRoadmap,
|
|
332
|
+
validateADRsAreReferenced,
|
|
333
|
+
validateWIPHasAcceptanceCriteria,
|
|
334
|
+
validateSingleWIP,
|
|
335
|
+
validateStaleWIP,
|
|
336
|
+
validateREQsNotBlockedByDraftADRs,
|
|
337
|
+
parseBlockedADRs,
|
|
338
|
+
adrIsDraft,
|
|
339
|
+
listDir,
|
|
340
|
+
}
|
package/bin/.gitkeep
DELETED
|
File without changes
|