trackfw 1.0.3 → 1.0.4

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.
@@ -0,0 +1,239 @@
1
+ 'use strict'
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ /**
6
+ * listREQs — lista arquivos .md em dir, imprimindo filename e status (coluna 60 chars).
7
+ * Extrai status da linha `> Date: ... | Status: ...`.
8
+ * Se dir não existe ou vazio: imprime "No REQs found in <dir>".
9
+ */
10
+ function listREQs(dir) {
11
+ let files = []
12
+ try {
13
+ files = fs.readdirSync(dir).filter(f => f.endsWith('.md'))
14
+ } catch (_) {
15
+ // dir não existe
16
+ }
17
+
18
+ if (files.length === 0) {
19
+ console.log(`No REQs found in ${dir}`)
20
+ return
21
+ }
22
+
23
+ for (const filename of files) {
24
+ const filepath = path.join(dir, filename)
25
+ const status = parseREQStatus(filepath)
26
+ console.log(`${filename.padEnd(60)} ${status}`)
27
+ }
28
+ }
29
+
30
+ /**
31
+ * parseREQStatus — extrai o status da linha `> Date: ... | Status: ...` de um arquivo REQ.
32
+ * Status termina no próximo " |" ou fim da linha.
33
+ */
34
+ function parseREQStatus(filepath) {
35
+ let content
36
+ try {
37
+ content = fs.readFileSync(filepath, 'utf8')
38
+ } catch (_) {
39
+ return 'unknown'
40
+ }
41
+
42
+ for (const line of content.split('\n')) {
43
+ const idx = line.indexOf('| Status: ')
44
+ if (idx >= 0) {
45
+ let rest = line.slice(idx + '| Status: '.length)
46
+ const pipeIdx = rest.indexOf(' |')
47
+ if (pipeIdx >= 0) {
48
+ rest = rest.slice(0, pipeIdx)
49
+ }
50
+ rest = rest.replace(/[\s>|]+$/, '')
51
+ return rest.trim() || 'unknown'
52
+ }
53
+ }
54
+ return 'unknown'
55
+ }
56
+
57
+ /**
58
+ * toSlug — converte string em slug kebab-case lowercase.
59
+ * @param {string} s
60
+ * @returns {string}
61
+ */
62
+ function toSlug(s) {
63
+ return s.toLowerCase().replace(/ /g, '-')
64
+ }
65
+
66
+ /**
67
+ * newREQ — cria docs/req/REQ-YYYY-MM-DD-<slug>.md.
68
+ * @param {{ title: string, motivation?: string, criteria?: string, dependsOnADRs?: string[] }} content
69
+ * @returns {Promise<void>}
70
+ */
71
+ async function newREQ(content) {
72
+ fs.mkdirSync('docs/req', { recursive: true })
73
+
74
+ const slug = toSlug(content.title)
75
+ const date = new Date().toISOString().slice(0, 10)
76
+ const filename = `docs/req/REQ-${date}-${slug}.md`
77
+
78
+ const motivationSection = content.motivation || '<!-- Why is this requirement needed? What problem does it solve? -->'
79
+ const criteriaSection = content.criteria || '- [ ]\n- [ ]'
80
+ const linkedADRSection = ''
81
+ const linkedRoadmapSection = ''
82
+
83
+ const dependsOnADRs = content.dependsOnADRs || []
84
+
85
+ // Linha de status — inclui contador de ADRs bloqueantes quando presente
86
+ let statusLine = `> Date: ${date} | Status: Open`
87
+ if (dependsOnADRs.length > 0) {
88
+ statusLine = `> Date: ${date} | Status: Open | Blocked by ADRs: ${dependsOnADRs.length}`
89
+ }
90
+
91
+ // Seção "Blocked by ADRs"
92
+ let blockedSection
93
+ if (dependsOnADRs.length === 0) {
94
+ blockedSection = '<!-- none -->'
95
+ } else {
96
+ const lines = ['<!-- ADRs in Draft status that must be Accepted before a roadmap can be created -->']
97
+ for (const adr of dependsOnADRs) {
98
+ lines.push(`- ${adr} (Draft)`)
99
+ }
100
+ blockedSection = lines.join('\n')
101
+ }
102
+
103
+ const body = `# REQ: ${content.title}
104
+
105
+ ${statusLine}
106
+
107
+ ## Motivation
108
+ ${motivationSection}
109
+
110
+ ## Acceptance Criteria
111
+ ${criteriaSection}
112
+
113
+ ## Linked ADR
114
+ <!-- Reference the ADR that governs this requirement -->
115
+ ADR: ${linkedADRSection}
116
+
117
+ ## Blocked by ADRs
118
+ ${blockedSection}
119
+
120
+ ## Linked Roadmap
121
+ <!-- Reference the roadmap that implements this requirement -->
122
+ Roadmap: ${linkedRoadmapSection}
123
+ `
124
+
125
+ fs.writeFileSync(filename, body, 'utf8')
126
+ console.log(`created ${filename}`)
127
+ }
128
+
129
+ /**
130
+ * PROBES_CATALOG — catálogo de domínios técnicos detectáveis (porte exato do Go).
131
+ */
132
+ const PROBES_CATALOG = [
133
+ {
134
+ domain: 'authentication',
135
+ keywords: ['login', 'auth', 'senha', 'password', 'sso', 'jwt', 'session', 'token', 'autenticação', 'autenticar'],
136
+ questions: [
137
+ {
138
+ text: 'How will users authenticate?',
139
+ options: [
140
+ { label: 'Local login (email + password)', decided: true, adrSlug: '' },
141
+ { label: 'SSO (Google, Azure AD, Okta...)', decided: false, adrSlug: 'sso-provider' },
142
+ { label: 'Both (local + SSO)', decided: false, adrSlug: 'authentication-strategy' },
143
+ { label: 'Not decided yet', decided: false, adrSlug: 'authentication-strategy' },
144
+ ],
145
+ },
146
+ {
147
+ text: 'How will sessions be managed?',
148
+ options: [
149
+ { label: 'JWT (stateless)', decided: true, adrSlug: '' },
150
+ { label: 'Server-side sessions (cookies)', decided: true, adrSlug: '' },
151
+ { label: 'Not decided yet', decided: false, adrSlug: 'session-management' },
152
+ ],
153
+ },
154
+ ],
155
+ },
156
+ {
157
+ domain: 'ui',
158
+ keywords: ['tela', 'screen', 'ui', 'frontend', 'componente', 'component', 'design', 'layout', 'interface'],
159
+ questions: [
160
+ {
161
+ text: 'Is there an existing UI framework or design system?',
162
+ options: [
163
+ { label: 'Yes, already chosen', decided: true, adrSlug: '' },
164
+ { label: 'No, need to choose a UI framework', decided: false, adrSlug: 'ui-framework' },
165
+ { label: 'Not relevant for this REQ', decided: true, adrSlug: '' },
166
+ ],
167
+ },
168
+ ],
169
+ },
170
+ {
171
+ domain: 'persistence',
172
+ keywords: ['banco', 'database', 'db', 'tabela', 'table', 'migração', 'migration', 'modelo', 'model', 'persistência', 'persist'],
173
+ questions: [
174
+ {
175
+ text: 'Which database engine will be used?',
176
+ options: [
177
+ { label: 'Already decided', decided: true, adrSlug: '' },
178
+ { label: 'Not decided yet', decided: false, adrSlug: 'database-engine' },
179
+ ],
180
+ },
181
+ ],
182
+ },
183
+ {
184
+ domain: 'api',
185
+ keywords: ['api', 'endpoint', 'rest', 'grpc', 'graphql', 'rota', 'route', 'http'],
186
+ questions: [
187
+ {
188
+ text: 'Which API protocol will be used?',
189
+ options: [
190
+ { label: 'REST (already decided)', decided: true, adrSlug: '' },
191
+ { label: 'gRPC (already decided)', decided: true, adrSlug: '' },
192
+ { label: 'GraphQL (already decided)', decided: true, adrSlug: '' },
193
+ { label: 'Not decided yet', decided: false, adrSlug: 'api-protocol' },
194
+ ],
195
+ },
196
+ ],
197
+ },
198
+ {
199
+ domain: 'deploy',
200
+ keywords: ['deploy', 'cloud', 'container', 'kubernetes', 'k8s', 'docker', 'infra', 'aws', 'gcp', 'azure'],
201
+ questions: [
202
+ {
203
+ text: 'Is the deployment infrastructure already defined?',
204
+ options: [
205
+ { label: 'Yes, fully defined', decided: true, adrSlug: '' },
206
+ { label: 'Cloud provider not decided', decided: false, adrSlug: 'cloud-provider' },
207
+ { label: 'Container strategy not decided', decided: false, adrSlug: 'container-strategy' },
208
+ ],
209
+ },
210
+ ],
211
+ },
212
+ {
213
+ domain: 'events',
214
+ keywords: ['kafka', 'fila', 'queue', 'notificação', 'notification', 'evento', 'event', 'pubsub', 'pub/sub', 'broker', 'sqs', 'redis'],
215
+ questions: [
216
+ {
217
+ text: 'Which event broker will be used?',
218
+ options: [
219
+ { label: 'Already decided', decided: true, adrSlug: '' },
220
+ { label: 'Not decided yet', decided: false, adrSlug: 'event-broker' },
221
+ ],
222
+ },
223
+ ],
224
+ },
225
+ ]
226
+
227
+ /**
228
+ * detectDomains — retorna probes cujos keywords aparecem na intention (case-insensitive).
229
+ * @param {string} intention
230
+ * @returns {Array}
231
+ */
232
+ function detectDomains(intention) {
233
+ const lower = intention.toLowerCase()
234
+ return PROBES_CATALOG.filter(probe =>
235
+ probe.keywords.some(kw => lower.includes(kw.toLowerCase()))
236
+ )
237
+ }
238
+
239
+ module.exports = { listREQs, parseREQStatus, newREQ, PROBES_CATALOG, detectDomains }
@@ -0,0 +1,224 @@
1
+ 'use strict'
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ const VALID_STATES = {
6
+ backlog: 'docs/roadmaps/backlog',
7
+ wip: 'docs/roadmaps/wip',
8
+ blocked: 'docs/roadmaps/blocked',
9
+ done: 'docs/roadmaps/done',
10
+ abandoned: 'docs/roadmaps/abandoned',
11
+ }
12
+
13
+ const STATE_ORDER = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
14
+
15
+ const TRANSITION_LOG_PATH = 'docs/roadmaps/.trackfw-log'
16
+
17
+ /**
18
+ * listRoadmaps — lista roadmaps agrupados por estado (wip, backlog, blocked, done, abandoned).
19
+ * Se nenhum encontrado imprime mensagem orientando o usuário.
20
+ */
21
+ function listRoadmaps() {
22
+ let found = false
23
+
24
+ for (const state of STATE_ORDER) {
25
+ const dir = VALID_STATES[state]
26
+ let files = []
27
+ try {
28
+ files = fs.readdirSync(dir).filter(f => !fs.statSync(path.join(dir, f)).isDirectory() && f.endsWith('.md'))
29
+ } catch (_) {
30
+ continue
31
+ }
32
+ if (files.length === 0) continue
33
+
34
+ found = true
35
+ console.log(`[${state}]`)
36
+ for (const f of files) {
37
+ console.log(` ${f}`)
38
+ }
39
+ }
40
+
41
+ if (!found) {
42
+ console.log("Nenhum roadmap encontrado. Crie um com 'trackfw roadmap new'.")
43
+ }
44
+ }
45
+
46
+ /**
47
+ * showRoadmap — busca docs/roadmaps/ESTADO/NOME*.md (partial match), imprime cabeçalho + conteúdo.
48
+ * 0 matches: erro. múltiplos: lista + erro. 1 match: imprime cabeçalho e conteúdo.
49
+ */
50
+ function showRoadmap(name) {
51
+ const matches = findRoadmapMatches(name)
52
+
53
+ if (matches.length === 0) {
54
+ console.error(`no roadmap found matching "${name}"`)
55
+ process.exitCode = 1
56
+ return
57
+ }
58
+
59
+ if (matches.length > 1) {
60
+ console.log('Multiple roadmaps found — be more specific:')
61
+ for (const m of matches) {
62
+ console.log(` ${m}`)
63
+ }
64
+ console.error(`ambiguous match for "${name}"`)
65
+ process.exitCode = 1
66
+ return
67
+ }
68
+
69
+ const filepath = matches[0]
70
+ const basename = path.basename(filepath)
71
+ const state = path.basename(path.dirname(filepath)).toUpperCase()
72
+ const content = fs.readFileSync(filepath, 'utf8')
73
+
74
+ console.log(`── ${basename} ── [${state}] ──────────────────────\n`)
75
+ console.log(content)
76
+ console.log(`Location: ${filepath}`)
77
+ }
78
+
79
+ /**
80
+ * moveRoadmap — move arquivo para diretório do estado alvo.
81
+ * Valida estado, procura arquivo em qualquer estado (case-insensitive partial match),
82
+ * move com fs.renameSync, chama appendTransitionLog, imprime confirmação.
83
+ */
84
+ function moveRoadmap(name, state) {
85
+ const targetDir = VALID_STATES[state]
86
+ if (!targetDir) {
87
+ console.error(`invalid state "${state}" — valid states: backlog, wip, blocked, done, abandoned`)
88
+ process.exitCode = 1
89
+ return
90
+ }
91
+
92
+ const matches = findRoadmapMatches(name)
93
+ if (matches.length === 0) {
94
+ console.error(`roadmap "${name}" not found in any state directory`)
95
+ process.exitCode = 1
96
+ return
97
+ }
98
+ if (matches.length > 1) {
99
+ console.log('Multiple roadmaps found — be more specific:')
100
+ for (const m of matches) {
101
+ console.log(` ${m}`)
102
+ }
103
+ console.error(`ambiguous match for "${name}"`)
104
+ process.exitCode = 1
105
+ return
106
+ }
107
+
108
+ const src = matches[0]
109
+ const basename = path.basename(src)
110
+ const fromState = path.basename(path.dirname(src))
111
+
112
+ try {
113
+ fs.mkdirSync(targetDir, { recursive: true })
114
+ } catch (_) {}
115
+
116
+ const dst = path.join(targetDir, basename)
117
+ fs.renameSync(src, dst)
118
+
119
+ appendTransitionLog(basename, fromState, state)
120
+ console.log(`✓ moved ${basename} → ${targetDir}`)
121
+ }
122
+
123
+ /**
124
+ * appendTransitionLog — append em docs/roadmaps/.trackfw-log.
125
+ * Formato: `YYYY-MM-DD HH:mm <basename padded to 50> <fromState> → <toState>\n`
126
+ */
127
+ function appendTransitionLog(basename, fromState, toState) {
128
+ const now = new Date()
129
+ const yyyy = now.getFullYear()
130
+ const mm = String(now.getMonth() + 1).padStart(2, '0')
131
+ const dd = String(now.getDate()).padStart(2, '0')
132
+ const hh = String(now.getHours()).padStart(2, '0')
133
+ const min = String(now.getMinutes()).padStart(2, '0')
134
+ const timestamp = `${yyyy}-${mm}-${dd} ${hh}:${min}`
135
+ const line = `${timestamp} ${basename.padEnd(50)} ${fromState} → ${toState}\n`
136
+
137
+ try {
138
+ fs.mkdirSync(path.dirname(TRANSITION_LOG_PATH), { recursive: true })
139
+ fs.appendFileSync(TRANSITION_LOG_PATH, line, 'utf8')
140
+ } catch (_) {}
141
+ }
142
+
143
+ /**
144
+ * newRoadmap — cria roadmap em docs/roadmaps/backlog/ROADMAP-YYYY-MM-DD-<slug>.md.
145
+ */
146
+ function newRoadmap(title, reqPath) {
147
+ const now = new Date()
148
+ const yyyy = now.getFullYear()
149
+ const mm = String(now.getMonth() + 1).padStart(2, '0')
150
+ const dd = String(now.getDate()).padStart(2, '0')
151
+ const date = `${yyyy}-${mm}-${dd}`
152
+ const slug = toSlug(title)
153
+ const filename = `docs/roadmaps/backlog/ROADMAP-${date}-${slug}.md`
154
+
155
+ fs.mkdirSync('docs/roadmaps/backlog', { recursive: true })
156
+
157
+ const body = `# Roadmap: ${title}
158
+
159
+ > Created: ${date} | Status: backlog
160
+
161
+ ## Context
162
+ <!-- What problem does this roadmap solve? Link the REQ. -->
163
+ REQ: ${reqPath || ''}
164
+
165
+ ## Wave 1 — <name> (parallel MLs)
166
+ > Dependencies: none
167
+
168
+ ### ML-1A — ${title}
169
+ **Status:** pending
170
+ **Files affected:**
171
+ **Actions:**
172
+ **Acceptance criteria:**
173
+ - [ ] build passes
174
+ - [ ] tests green
175
+ - [ ] validate passes
176
+ `
177
+
178
+ fs.writeFileSync(filename, body, 'utf8')
179
+ console.log(`✓ created ${filename}`)
180
+ }
181
+
182
+ // --- helpers ---
183
+
184
+ /**
185
+ * findRoadmapMatches — retorna array de paths que contêm `name` (case-insensitive) em qualquer estado.
186
+ */
187
+ function findRoadmapMatches(name) {
188
+ const matches = []
189
+ const nameLower = name.toLowerCase()
190
+ for (const state of STATE_ORDER) {
191
+ const dir = VALID_STATES[state]
192
+ let files = []
193
+ try {
194
+ files = fs.readdirSync(dir)
195
+ } catch (_) {
196
+ continue
197
+ }
198
+ for (const f of files) {
199
+ if (f.toLowerCase().includes(nameLower) && f.endsWith('.md')) {
200
+ matches.push(path.join(dir, f))
201
+ }
202
+ }
203
+ }
204
+ return matches
205
+ }
206
+
207
+ /**
208
+ * toSlug — converte string para slug lowercase com hífens.
209
+ */
210
+ function toSlug(s) {
211
+ return s
212
+ .toLowerCase()
213
+ .replace(/[^a-z0-9]+/g, '-')
214
+ .replace(/^-+|-+$/g, '')
215
+ }
216
+
217
+ module.exports = {
218
+ VALID_STATES,
219
+ listRoadmaps,
220
+ showRoadmap,
221
+ moveRoadmap,
222
+ appendTransitionLog,
223
+ newRoadmap,
224
+ }