trackfw 7.6.0 → 8.0.0-rc2

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.
Files changed (124) hide show
  1. package/README.md +28 -11
  2. package/bin/trackfw.js +81 -0
  3. package/package.json +14 -8
  4. package/bin/trackfw +0 -41
  5. package/src/branch/prune.js +0 -450
  6. package/src/branch/runner.js +0 -153
  7. package/src/changelog/index.js +0 -114
  8. package/src/commands/adr.js +0 -56
  9. package/src/commands/agents.js +0 -3
  10. package/src/commands/audit-surface.js +0 -520
  11. package/src/commands/barrier.js +0 -822
  12. package/src/commands/baseline.js +0 -13
  13. package/src/commands/branch.js +0 -98
  14. package/src/commands/changelog.js +0 -29
  15. package/src/commands/commit.js +0 -66
  16. package/src/commands/configure.js +0 -106
  17. package/src/commands/context.js +0 -215
  18. package/src/commands/discover.js +0 -628
  19. package/src/commands/doctor.js +0 -66
  20. package/src/commands/help.js +0 -358
  21. package/src/commands/identity-wizard.js +0 -258
  22. package/src/commands/index.js +0 -68
  23. package/src/commands/init.js +0 -296
  24. package/src/commands/integrations.js +0 -305
  25. package/src/commands/log.js +0 -31
  26. package/src/commands/metrics.js +0 -242
  27. package/src/commands/note.js +0 -24
  28. package/src/commands/push.js +0 -51
  29. package/src/commands/release.js +0 -47
  30. package/src/commands/req.js +0 -98
  31. package/src/commands/roadmap.js +0 -44
  32. package/src/commands/serve.js +0 -345
  33. package/src/commands/ship.js +0 -58
  34. package/src/commands/skills.js +0 -3
  35. package/src/commands/status.js +0 -12
  36. package/src/commands/sync.js +0 -348
  37. package/src/commands/thirdparty.js +0 -390
  38. package/src/commands/update-harness.js +0 -1002
  39. package/src/commands/update.js +0 -500
  40. package/src/commands/validate.js +0 -57
  41. package/src/commands/version.js +0 -10
  42. package/src/commit/runner.js +0 -352
  43. package/src/config/index.js +0 -457
  44. package/src/forge/adapter.js +0 -189
  45. package/src/forge/resolve.js +0 -191
  46. package/src/generators/adr.js +0 -198
  47. package/src/generators/codex.js +0 -79
  48. package/src/generators/date.js +0 -23
  49. package/src/generators/hooks.js +0 -2414
  50. package/src/generators/init.js +0 -1563
  51. package/src/generators/note.js +0 -154
  52. package/src/generators/req.js +0 -436
  53. package/src/generators/roadmap.js +0 -841
  54. package/src/homedir.js +0 -30
  55. package/src/i18n/index.js +0 -53
  56. package/src/i18n/locales/en-US.json +0 -124
  57. package/src/i18n/locales/es-ES.json +0 -124
  58. package/src/i18n/locales/pt-BR.json +0 -124
  59. package/src/identity/config.js +0 -134
  60. package/src/identity/index.js +0 -19
  61. package/src/identity/preset.js +0 -219
  62. package/src/identity/slug.js +0 -62
  63. package/src/integrations/assets/agents/architect.md +0 -61
  64. package/src/integrations/assets/agents/backend.md +0 -41
  65. package/src/integrations/assets/agents/code-quality.md +0 -48
  66. package/src/integrations/assets/agents/data.md +0 -41
  67. package/src/integrations/assets/agents/dba.md +0 -41
  68. package/src/integrations/assets/agents/frontend.md +0 -41
  69. package/src/integrations/assets/agents/iac.md +0 -46
  70. package/src/integrations/assets/agents/infra.md +0 -43
  71. package/src/integrations/assets/agents/qa.md +0 -41
  72. package/src/integrations/assets/agents/security.md +0 -51
  73. package/src/integrations/assets/agents/tooling.md +0 -47
  74. package/src/integrations/assets/agents/ux.md +0 -48
  75. package/src/integrations/assets/catalog.json +0 -48
  76. package/src/integrations/assets/skills/architecture.md +0 -66
  77. package/src/integrations/assets/skills/backend.md +0 -67
  78. package/src/integrations/assets/skills/code-quality.md +0 -78
  79. package/src/integrations/assets/skills/data.md +0 -75
  80. package/src/integrations/assets/skills/dba.md +0 -77
  81. package/src/integrations/assets/skills/frontend.md +0 -70
  82. package/src/integrations/assets/skills/governance.md +0 -6
  83. package/src/integrations/assets/skills/iac.md +0 -90
  84. package/src/integrations/assets/skills/implement.md +0 -6
  85. package/src/integrations/assets/skills/infra.md +0 -62
  86. package/src/integrations/assets/skills/plan.md +0 -6
  87. package/src/integrations/assets/skills/qa.md +0 -63
  88. package/src/integrations/assets/skills/release.md +0 -6
  89. package/src/integrations/assets/skills/review.md +0 -6
  90. package/src/integrations/assets/skills/security.md +0 -74
  91. package/src/integrations/assets/skills/tooling.md +0 -80
  92. package/src/integrations/assets/skills/ux.md +0 -70
  93. package/src/integrations/catalog.js +0 -58
  94. package/src/integrations/doctor.js +0 -0
  95. package/src/integrations/doctor_remote.js +0 -224
  96. package/src/integrations/index.js +0 -171
  97. package/src/integrations/legacy.js +0 -35
  98. package/src/integrations/manager.js +0 -438
  99. package/src/integrations/register-agent.js +0 -106
  100. package/src/integrations/render.js +0 -563
  101. package/src/integrations/scaffold_doctor.js +0 -395
  102. package/src/lib/fatal-error.js +0 -64
  103. package/src/lib/pathfmt.js +0 -43
  104. package/src/lib/unknown-command.js +0 -82
  105. package/src/lib/update-engine.js +0 -242
  106. package/src/push/runner.js +0 -226
  107. package/src/release/runner.js +0 -474
  108. package/src/serve/api_attention.js +0 -24
  109. package/src/serve/api_board.js +0 -105
  110. package/src/serve/api_chain.js +0 -246
  111. package/src/serve/api_file.js +0 -117
  112. package/src/serve/api_metrics.js +0 -139
  113. package/src/serve/static/app.js +0 -1151
  114. package/src/serve/static/index.html +0 -338
  115. package/src/serve/static/style.css +0 -297
  116. package/src/ship/runner.js +0 -775
  117. package/src/thirdparty/fetch.js +0 -140
  118. package/src/thirdparty/markers.js +0 -207
  119. package/src/thirdparty/provenance.js +0 -114
  120. package/src/thirdparty/quarantine.js +0 -113
  121. package/src/thirdparty/references.js +0 -194
  122. package/src/validator/git-exec.js +0 -72
  123. package/src/validator/index.js +0 -4086
  124. package/src/validator/traceid.js +0 -155
@@ -1,246 +0,0 @@
1
- 'use strict'
2
-
3
- const fs = require('fs')
4
- const path = require('path')
5
- const { resolveAgentNamespaces, extractRefPath } = require('../validator')
6
- const { normalizeRefSeparator } = require('../lib/pathfmt')
7
-
8
- const ROADMAP_STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
9
-
10
- /**
11
- * parseFrontmatter extrai todos os campos de um bloco frontmatter YAML (entre --- e ---).
12
- * Retorna objeto com chaves/valores string.
13
- * @param {string} content
14
- * @returns {Object<string, string>}
15
- */
16
- function parseFrontmatter(content) {
17
- const fields = {}
18
- const lines = content.split('\n')
19
- let started = false
20
- let inFm = false
21
- for (const line of lines) {
22
- const trimmed = line.trim()
23
- if (trimmed === '---') {
24
- if (!started) { started = true; inFm = true; continue }
25
- break // fecha frontmatter
26
- }
27
- if (!inFm) break
28
- const colonIdx = trimmed.indexOf(':')
29
- if (colonIdx < 0) continue
30
- const key = trimmed.slice(0, colonIdx).trim()
31
- let val = trimmed.slice(colonIdx + 1).trim()
32
- val = val.replace(/^["']|["']$/g, '')
33
- if (key) fields[key] = val
34
- }
35
- return fields
36
- }
37
-
38
- /**
39
- * extractTitle retorna a primeira linha '# ...' ou o nome do arquivo sem extensão.
40
- * @param {string} content
41
- * @param {string} filename
42
- * @returns {string}
43
- */
44
- function extractTitle(content, filename) {
45
- for (const line of content.split('\n')) {
46
- const t = line.trim()
47
- if (t.startsWith('# ')) return t.slice(2).trim()
48
- }
49
- return filename.replace(/\.md$/, '')
50
- }
51
-
52
- /**
53
- * collectNodes lê arquivos .md de um diretório e retorna nós do grafo.
54
- * @param {string} dir
55
- * @param {'adr'|'req'|'roadmap'} type
56
- * @param {string} state
57
- * @returns {Array<{id: string, type: string, title: string, state: string, fm: Object}>}
58
- */
59
- function collectNodes(dir, type, state) {
60
- const nodes = []
61
- let files = []
62
- try {
63
- files = fs.readdirSync(dir).filter(f => {
64
- if (!f.endsWith('.md')) return false
65
- try { return !fs.statSync(path.join(dir, f)).isDirectory() } catch (_) { return false }
66
- })
67
- } catch (_) {
68
- return nodes
69
- }
70
- for (const file of files) {
71
- let content = ''
72
- try { content = fs.readFileSync(path.join(dir, file), 'utf8') } catch (_) {}
73
- const fm = parseFrontmatter(content)
74
- const title = extractTitle(content, file)
75
- // node ID do grafo — IDENTIFICADOR emitido em JSON (ADR-2026-09-04, D1 categoria 2),
76
- // e tambem o alvo de toda aresta: resolveRef() abaixo so devolve valores vindos de
77
- // fileIndex/titleIndex, isto e, sempre um node id — nunca o valor cru do frontmatter.
78
- // Normalizar o id aqui, portanto, normaliza `edge.to` por construcao.
79
- //
80
- // 🔴 O path.join da linha 71 (fs.readFileSync) e uma EXPRESSAO INDEPENDENTE sobre os
81
- // mesmos operandos: normalizar esta nao pode afeta-la. O caminho de travessia
82
- // permanece nativo (ADR D2). Espelha internal/serve/api_chain.go:111.
83
- const id = normalizeRefSeparator(path.join(dir, file))
84
- nodes.push({ id, type, title, state, fm, content })
85
- }
86
- return nodes
87
- }
88
-
89
- /**
90
- * handleChain responde ao GET /api/chain com nodes e edges em JSON.
91
- * @param {object} cfg
92
- * @param {http.IncomingMessage} req
93
- * @param {http.ServerResponse} res
94
- */
95
- function handleChain(cfg, req, res) {
96
- const adrDirs = cfg.adrDirs || ['docs/adr']
97
- const reqDir = cfg.reqDir || 'docs/req'
98
- const roadmapDir = cfg.roadmapDir || 'docs/roadmaps'
99
- const namespacing = cfg.roadmapNamespacing || 'flat'
100
-
101
- const allNodes = []
102
-
103
- // ADRs — scan recursivo de subpastas (done/, wip/, etc.)
104
- for (const adrDir of adrDirs) {
105
- const subfolders = ['done', 'wip', 'backlog', 'blocked', 'abandoned', '']
106
- for (const sub of subfolders) {
107
- const dir = sub ? path.join(adrDir, sub) : adrDir
108
- const nodes = collectNodes(dir, 'adr', sub || 'unknown')
109
- allNodes.push(...nodes)
110
- }
111
- }
112
-
113
- // REQs
114
- if (namespacing === 'by_agent') {
115
- const agents = resolveAgentNamespaces(cfg, reqDir)
116
- for (const agent of agents) {
117
- for (const state of ROADMAP_STATES) {
118
- const dir = path.join(reqDir, agent, state)
119
- allNodes.push(...collectNodes(dir, 'req', state))
120
- }
121
- }
122
- } else {
123
- allNodes.push(...collectNodes(reqDir, 'req', 'unknown'))
124
- }
125
-
126
- // Roadmaps
127
- if (namespacing === 'by_agent') {
128
- const agents = resolveAgentNamespaces(cfg, roadmapDir)
129
- for (const agent of agents) {
130
- for (const state of ROADMAP_STATES) {
131
- const dir = path.join(roadmapDir, agent, state)
132
- allNodes.push(...collectNodes(dir, 'roadmap', state))
133
- }
134
- }
135
- } else {
136
- for (const state of ROADMAP_STATES) {
137
- const dir = path.join(roadmapDir, state)
138
- allNodes.push(...collectNodes(dir, 'roadmap', state))
139
- }
140
- }
141
-
142
- // Deduplicar por id (path)
143
- const nodeMap = new Map()
144
- for (const n of allNodes) {
145
- if (!nodeMap.has(n.id)) nodeMap.set(n.id, n)
146
- }
147
-
148
- // Construir índice por título e nome de arquivo para resolver referências
149
- const titleIndex = new Map() // titulo normalizado -> id
150
- const fileIndex = new Map() // basename -> id
151
- for (const [id, n] of nodeMap) {
152
- const norm = n.title.toLowerCase().trim()
153
- if (!titleIndex.has(norm)) titleIndex.set(norm, id)
154
- const base = path.basename(id).replace(/\.md$/, '').toLowerCase()
155
- if (!fileIndex.has(base)) fileIndex.set(base, id)
156
- }
157
-
158
- // Construir edges a partir dos campos de frontmatter
159
- const edges = []
160
- const edgeSet = new Set()
161
-
162
- function addEdge(from, to) {
163
- const key = `${from}→${to}`
164
- if (!edgeSet.has(key)) {
165
- edgeSet.add(key)
166
- edges.push({ from, to })
167
- }
168
- }
169
-
170
- function resolveRef(val) {
171
- if (!val) return null
172
- // Tenta pelo nome de arquivo — path.basename(val), não val cru: o formato canônico
173
- // gravado por `trackfw req new`/`roadmap new` é o CAMINHO COMPLETO (ADR-2026-08-01), não o
174
- // basename isolado. Sem o basename() aqui, um vínculo real como
175
- // "docs/roadmaps/wip/ROADMAP-x.md" nunca batia contra a chave "roadmap-x" do fileIndex —
176
- // achado do ML-3D: nenhuma aresta REQ/ADR/Roadmap gerada pelo CLI jamais resolvia por este
177
- // caminho, mascarado porque os testes existentes só cobrem valor já-basename.
178
- //
179
- // Por que este arquivo NÃO importa validator.resolveRoadmapRef (ao contrário de
180
- // internal/serve/api_chain.go, ML-3D): fileIndex é indexado por basename ACROSS todos os
181
- // estados (wip/backlog/blocked/done/abandoned) desde antes desta correção — um vínculo
182
- // `roadmap:` "wip/X.md" já resolve contra o node real em "done/X.md" sem fallback extra.
183
- // O mecanismo do ML-3B/3D (caminho literal gravado com pasta de estado velha) não afeta
184
- // este CLI porque ele nunca comparou por igualdade de caminho completo — só por basename/
185
- // título. Ver docs/cli-parity.md, seção "serve: /api/chain".
186
- const base = path.basename(val).replace(/\.md$/, '').toLowerCase().trim()
187
- if (fileIndex.has(base)) return fileIndex.get(base)
188
- // Tenta pelo título
189
- const norm = val.toLowerCase().trim()
190
- if (titleIndex.has(norm)) return titleIndex.get(norm)
191
- return null
192
- }
193
-
194
- // Campos de link padrão
195
- const reqFields = (cfg.linkFields && cfg.linkFields.req) || ['REQ:', 'req:']
196
- const adrFields = (cfg.linkFields && cfg.linkFields.adr) || ['ADR:', 'adr:']
197
- const roadmapFields = (cfg.linkFields && cfg.linkFields.roadmap) || ['Roadmap:', 'roadmap:']
198
-
199
- for (const [id, node] of nodeMap) {
200
- const fm = node.fm || {}
201
-
202
- // Checar campos de frontmatter para criar arestas
203
- for (const [fmKey, fmVal] of Object.entries(fm)) {
204
- const keyLower = fmKey.toLowerCase()
205
- if (keyLower === 'req' || reqFields.some(f => f.toLowerCase().replace(':', '') === keyLower)) {
206
- const target = resolveRef(fmVal)
207
- if (target && target !== id) addEdge(id, target)
208
- }
209
- if (keyLower === 'adr' || adrFields.some(f => f.toLowerCase().replace(':', '') === keyLower)) {
210
- const target = resolveRef(fmVal)
211
- if (target && target !== id) addEdge(id, target)
212
- }
213
- if (keyLower === 'roadmap' || roadmapFields.some(f => f.toLowerCase().replace(':', '') === keyLower)) {
214
- const target = resolveRef(fmVal)
215
- if (target && target !== id) addEdge(id, target)
216
- }
217
- }
218
-
219
- // ML-3D: `trackfw req new` (npm/src/generators/req.js) grava `adr: ""` e `roadmap: ""`
220
- // SEMPRE vazios no frontmatter — o valor real vive no corpo, em "## Linked ADR / ADR:
221
- // <path>" e "## Linked Roadmap / Roadmap: <path>". O laço de frontmatter acima nunca
222
- // encontra esse vínculo para uma REQ gerada pelo próprio CLI (não é caso de borda: é o
223
- // formato canônico inteiro). validator.extractRefPath varre o CONTEÚDO inteiro
224
- // linha-a-linha (mesma função usada por validateRefTargetsExist) — ponto único de
225
- // extração, não duplicado aqui.
226
- for (const field of ['req', 'adr', 'roadmap']) {
227
- const val = extractRefPath(node.content || '', field)
228
- if (!val) continue
229
- const target = resolveRef(val)
230
- if (target && target !== id) addEdge(id, target)
231
- }
232
- }
233
-
234
- // Montar nodes de saída sem o campo fm interno
235
- const outputNodes = Array.from(nodeMap.values()).map(n => ({
236
- id: n.id,
237
- type: n.type,
238
- title: n.title,
239
- state: n.state,
240
- }))
241
-
242
- res.writeHead(200, { 'Content-Type': 'application/json' })
243
- res.end(JSON.stringify({ nodes: outputNodes, edges }))
244
- }
245
-
246
- module.exports = { handleChain }
@@ -1,117 +0,0 @@
1
- 'use strict'
2
-
3
- const fs = require('fs')
4
- const path = require('path')
5
-
6
- /**
7
- * isPathAllowed verifica se absPath está dentro de um dos diretórios permitidos.
8
- * Usa separador no sufixo para evitar que /docs/adr case com /docs/adr2.
9
- * @param {string} absPath
10
- * @param {string[]} allowedDirs
11
- * @returns {boolean}
12
- */
13
- function isPathAllowed(absPath, allowedDirs) {
14
- for (const dir of allowedDirs) {
15
- if (absPath === dir || absPath.startsWith(dir + path.sep)) {
16
- return true
17
- }
18
- }
19
- return false
20
- }
21
-
22
- /**
23
- * handleFile responde ao GET /api/file?path=... com o conteúdo do arquivo.
24
- *
25
- * Modelo de segurança — contenção em dois estágios:
26
- *
27
- * 1. Contenção léxica (sem acesso ao filesystem): path.resolve() normaliza ".."
28
- * e o resultado deve começar com uma das raízes autorizadas. Caminhos fora
29
- * das raízes recebem 403 antes de qualquer acesso ao disco, impedindo que a
30
- * distinção 403 vs 404 sirva de oracle de existência para caminhos arbitrários.
31
- *
32
- * 2. Contenção física (resolução de symlinks): fs.realpathSync.native() é chamado
33
- * no arquivo pedido e em cada raiz autorizada. Um symlink cujo nome está
34
- * dentro de uma raiz autorizada mas cujo destino físico não está → 403.
35
- * Qualquer falha de canonicalização (ENOENT, symlink pendente, …) → 404.
36
- *
37
- * Um symlink legítimo cujo destino também está dentro de uma raiz autorizada
38
- * continua sendo servido normalmente.
39
- *
40
- * @param {object} cfg
41
- * @param {http.IncomingMessage} req
42
- * @param {http.ServerResponse} res
43
- */
44
- function handleFile(cfg, req, res) {
45
- let urlObj
46
- try {
47
- urlObj = new URL(req.url, 'http://localhost')
48
- } catch (_) {
49
- res.writeHead(400, { 'Content-Type': 'text/plain' })
50
- res.end('Bad Request')
51
- return
52
- }
53
-
54
- const filePath = urlObj.searchParams.get('path')
55
- if (!filePath) {
56
- res.writeHead(400, { 'Content-Type': 'text/plain' })
57
- res.end('Missing path parameter')
58
- return
59
- }
60
-
61
- // ── Estágio 1: Contenção léxica ────────────────────────────────────────────
62
- // path.resolve() resolve '..' mas NÃO segue symlinks (operação puramente léxica).
63
- const resolved = path.resolve(filePath)
64
-
65
- const adrDirs = (cfg.adrDirs || ['docs/adr']).map(d => path.resolve(d))
66
- const reqDir = path.resolve(cfg.reqDir || 'docs/req')
67
- const roadmapDir = path.resolve(cfg.roadmapDir || 'docs/roadmaps')
68
- const allowedDirs = [...adrDirs, reqDir, roadmapDir]
69
-
70
- if (!isPathAllowed(resolved, allowedDirs)) {
71
- res.writeHead(403, { 'Content-Type': 'text/plain' })
72
- res.end('Forbidden')
73
- return
74
- }
75
-
76
- // ── Estágio 2: Contenção física ────────────────────────────────────────────
77
- // Canonicalizar o arquivo pedido. Qualquer erro (ENOENT, symlink pendente,
78
- // loop, permissão) → 404. O arquivo está nominalmente dentro de uma raiz
79
- // autorizada (estágio 1 passou), portanto 404 é o resultado correto quando
80
- // ele simplesmente não existe.
81
- let realResolved
82
- try {
83
- realResolved = fs.realpathSync.native(resolved)
84
- } catch (_) {
85
- res.writeHead(404, { 'Content-Type': 'text/plain' })
86
- res.end('Not Found')
87
- return
88
- }
89
-
90
- // Canonicalizar as raízes autorizadas. Raízes inexistentes são ignoradas —
91
- // nenhum arquivo pode residir em um diretório que não existe.
92
- const realAllowedDirs = allowedDirs.flatMap(d => {
93
- try { return [fs.realpathSync.native(d)] } catch (_) { return [] }
94
- })
95
-
96
- // Contenção física: o destino canônico deve estar dentro de uma raiz canônica.
97
- if (!isPathAllowed(realResolved, realAllowedDirs)) {
98
- res.writeHead(403, { 'Content-Type': 'text/plain' })
99
- res.end('Forbidden')
100
- return
101
- }
102
-
103
- // O arquivo existe (realpathSync não falhou) — ler da rota canônica.
104
- let content
105
- try {
106
- content = fs.readFileSync(realResolved, 'utf8')
107
- } catch (err) {
108
- res.writeHead(500, { 'Content-Type': 'text/plain' })
109
- res.end('Internal Server Error')
110
- return
111
- }
112
-
113
- res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' })
114
- res.end(content)
115
- }
116
-
117
- module.exports = { handleFile }
@@ -1,139 +0,0 @@
1
- 'use strict'
2
-
3
- const fs = require('fs')
4
- const path = require('path')
5
- const { parseLog, calculate } = require('../commands/metrics')
6
- const { resolveAgentNamespaces } = require('../validator')
7
-
8
- const STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
9
- const MS_PER_DAY = 24 * 60 * 60 * 1000
10
-
11
- /**
12
- * countFilesInState conta arquivos .md em um diretório (sem recursão).
13
- * @param {string} dir
14
- * @returns {number}
15
- */
16
- function countFilesInState(dir) {
17
- try {
18
- return fs.readdirSync(dir).filter(f => {
19
- if (!f.endsWith('.md')) return false
20
- try { return !fs.statSync(path.join(dir, f)).isDirectory() } catch (_) { return false }
21
- }).length
22
- } catch (_) {
23
- return 0
24
- }
25
- }
26
-
27
- /**
28
- * buildStateDistribution conta roadmaps por estado.
29
- * @param {object} cfg
30
- * @returns {Object<string, number>}
31
- */
32
- function buildStateDistribution(cfg) {
33
- const dist = { wip: 0, backlog: 0, blocked: 0, done: 0, abandoned: 0 }
34
- const roadmapDir = cfg.roadmapDir || 'docs/roadmaps'
35
- const namespacing = cfg.roadmapNamespacing || 'flat'
36
-
37
- if (namespacing === 'by_agent') {
38
- const agents = resolveAgentNamespaces(cfg, roadmapDir)
39
- for (const agent of agents) {
40
- for (const state of STATES) {
41
- dist[state] = (dist[state] || 0) + countFilesInState(path.join(roadmapDir, agent, state))
42
- }
43
- }
44
- } else {
45
- for (const state of STATES) {
46
- dist[state] = (dist[state] || 0) + countFilesInState(path.join(roadmapDir, state))
47
- }
48
- }
49
-
50
- return dist
51
- }
52
-
53
- /**
54
- * buildBurndown calcula pontos semanais a partir do log de transições.
55
- * @param {Array} transitions
56
- * @returns {Array<{date: string, open: number, closed: number}>}
57
- */
58
- function buildBurndown(transitions) {
59
- if (!transitions.length) return []
60
-
61
- let minTs = Infinity
62
- let maxTs = -Infinity
63
- for (const tr of transitions) {
64
- const ms = tr.timestamp.getTime()
65
- if (ms < minTs) minTs = ms
66
- if (ms > maxTs) maxTs = ms
67
- }
68
-
69
- // Agrupar por semana (domingo como início)
70
- const weekMap = new Map()
71
-
72
- function weekKey(ms) {
73
- const d = new Date(ms)
74
- // arredondar para o domingo anterior
75
- const day = d.getDay() // 0=domingo
76
- const sunday = new Date(ms - day * MS_PER_DAY)
77
- return sunday.toISOString().slice(0, 10)
78
- }
79
-
80
- for (const tr of transitions) {
81
- const wk = weekKey(tr.timestamp.getTime())
82
- if (!weekMap.has(wk)) weekMap.set(wk, { open: 0, closed: 0 })
83
- const entry = weekMap.get(wk)
84
- if (tr.to === 'done' || tr.to === 'abandoned') {
85
- entry.closed++
86
- } else if (tr.to === 'wip' || tr.to === 'backlog') {
87
- entry.open++
88
- }
89
- }
90
-
91
- const sorted = Array.from(weekMap.entries()).sort(([a], [b]) => a.localeCompare(b))
92
- return sorted.map(([date, v]) => ({ date, open: v.open, closed: v.closed }))
93
- }
94
-
95
- /**
96
- * handleMetrics responde ao GET /api/metrics com JSON de métricas.
97
- * @param {object} cfg
98
- * @param {http.IncomingMessage} req
99
- * @param {http.ServerResponse} res
100
- */
101
- function handleMetrics(cfg, req, res) {
102
- const roadmapDir = cfg.roadmapDir || 'docs/roadmaps'
103
- const logPath = path.join(roadmapDir, '.trackfw-log')
104
-
105
- const transitions = parseLog(logPath)
106
- const metrics = calculate(transitions)
107
-
108
- // Converter cycle time de ms para dias
109
- const cycleTimeAvgDays = metrics.cycleTimeMeanMs > 0
110
- ? parseFloat((metrics.cycleTimeMeanMs / MS_PER_DAY).toFixed(2))
111
- : 0
112
-
113
- // Lead time: da criação (primeira entrada em qualquer estado) até done
114
- // Usando as mesmas transições — aproximar como cycle time se não houver campo de criação
115
- // (o campo creation_date não está no log; usamos start → done como lead time também)
116
- const leadTimeAvgDays = cycleTimeAvgDays
117
-
118
- // Abandonment rate: abandonados / (done + abandonados)
119
- const dist = buildStateDistribution(cfg)
120
- const totalCompleted = (dist.done || 0) + (dist.abandoned || 0)
121
- const abandonmentRate = totalCompleted > 0
122
- ? parseFloat(((dist.abandoned || 0) / totalCompleted).toFixed(4))
123
- : 0
124
-
125
- const burndown = buildBurndown(transitions)
126
-
127
- const result = {
128
- lead_time_avg_days: leadTimeAvgDays,
129
- cycle_time_avg_days: cycleTimeAvgDays,
130
- abandonment_rate: abandonmentRate,
131
- state_distribution: dist,
132
- burndown,
133
- }
134
-
135
- res.writeHead(200, { 'Content-Type': 'application/json' })
136
- res.end(JSON.stringify(result))
137
- }
138
-
139
- module.exports = { handleMetrics }