trackfw 7.3.0 → 7.5.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 +67 -1
- package/package.json +1 -1
- package/src/commands/adr.js +2 -1
- package/src/commands/barrier.js +293 -26
- package/src/commands/context.js +42 -36
- package/src/commands/discover.js +72 -21
- package/src/commands/doctor.js +23 -3
- package/src/commands/init.js +2 -1
- package/src/commands/integrations.js +5 -4
- package/src/commands/req.js +2 -1
- package/src/commands/thirdparty.js +2 -1
- package/src/commands/update-harness.js +3 -2
- package/src/commands/update.js +127 -9
- package/src/config/index.js +4 -3
- package/src/generators/hooks.js +152 -15
- package/src/generators/init.js +103 -7
- package/src/generators/req.js +15 -53
- package/src/generators/roadmap.js +81 -28
- package/src/homedir.js +30 -0
- package/src/integrations/doctor.js +0 -0
- package/src/integrations/doctor_remote.js +224 -0
- package/src/integrations/index.js +2 -1
- package/src/integrations/manager.js +10 -2
- package/src/integrations/render.js +39 -8
- package/src/integrations/scaffold_doctor.js +29 -0
- package/src/lib/pathfmt.js +43 -0
- package/src/lib/update-engine.js +15 -2
- package/src/serve/api_board.js +11 -10
- package/src/serve/api_chain.js +43 -20
- package/src/serve/api_metrics.js +2 -8
- package/src/validator/index.js +914 -177
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pathfmt — ponto único de normalização de separador do CLI Node
|
|
5
|
+
* (ADR-2026-09-04 "Separador POSIX nos artefatos autorados cujo consumidor não é o
|
|
6
|
+
* sistema de arquivos", D3: "Não espalhar `strings.ReplaceAll` pelos chamadores. Uma
|
|
7
|
+
* função por runtime, aplicada onde o valor SAI").
|
|
8
|
+
*
|
|
9
|
+
* 🔴 ESCOPO NEGATIVO (D2 da mesma ADR) — esta função é de EMISSÃO, nunca de travessia.
|
|
10
|
+
* Não aplicar a um caminho antes de `fs.readFileSync`/`fs.statSync`/`fs.writeFileSync`:
|
|
11
|
+
* normalizar cegamente quebra UNC (`\\server\share`) e o prefixo de caminho longo
|
|
12
|
+
* (`\\?\`), que exige backslash EXCLUSIVAMENTE — nem o Windows converte. O modo de falha
|
|
13
|
+
* seria intermitente, que é o pior. `path.join`/`path.relative` continuam sendo o certo
|
|
14
|
+
* para caminho que vai a uma syscall.
|
|
15
|
+
*
|
|
16
|
+
* Aplicar apenas às três categorias que a D1 nomeia, cujo consumidor NÃO é o filesystem:
|
|
17
|
+
* 1. texto de relatório / saída para humano (displayPath, mensagens)
|
|
18
|
+
* 2. chave de dicionário ou identificador (provenanceKey, node ID, edge.to)
|
|
19
|
+
* 3. string de comando interpretada por shell (command de hook, gate de wave)
|
|
20
|
+
*
|
|
21
|
+
* Substituição INCONDICIONAL de "\" por "/", não `path.posix.normalize` nem uma troca de
|
|
22
|
+
* `path.sep`: em POSIX `path.sep` é "/" e uma troca por separador seria no-op estrutural,
|
|
23
|
+
* o que não normalizaria um valor sujo herdado de um commit feito no Windows — exatamente
|
|
24
|
+
* o defeito que esta função existe para curar
|
|
25
|
+
* (docs/seguranca/2026-09-01-modelo-de-ameaca-do-separador-em-artefato.md). É a mesma
|
|
26
|
+
* semântica já pinada em Go (`internal/validator/validator.go:normalizeRefSeparator`) e
|
|
27
|
+
* Python (`pypi/trackfw/pathfmt.py:normalize_ref_separator`); D4 exige que os 3 sejam
|
|
28
|
+
* byte-idênticos, então esta escolha não é livre — é herdada.
|
|
29
|
+
*
|
|
30
|
+
* Custo aceito e nomeado: em POSIX um "\" literal dentro de um nome de arquivo é DADO, e
|
|
31
|
+
* esta função o converteria. Vale só para o texto emitido (display/chave), nunca para o
|
|
32
|
+
* caminho realmente aberto, que não passa por aqui.
|
|
33
|
+
*
|
|
34
|
+
* NÃO aplicar ao buffer inteiro de um arquivo — só ao valor já extraído/derivado.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} p valor já extraído ou derivado (nunca o buffer de um arquivo)
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function normalizeRefSeparator(p) {
|
|
40
|
+
return p.replace(/\\/g, '/')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { normalizeRefSeparator }
|
package/src/lib/update-engine.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { normalizeRefSeparator } = require('./pathfmt')
|
|
4
|
+
|
|
3
5
|
// update-engine.js — shared state machine for `trackfw update` and
|
|
4
6
|
// `trackfw update harness` (ML-6C of ROADMAP-2026-07-29-barrier-governanca-
|
|
5
7
|
// e-autoridade-do-orquestrador). Both commands report one of exactly four
|
|
@@ -169,6 +171,17 @@ function summarize(targets) {
|
|
|
169
171
|
// fell through to the absolute-path branch even though homeRoot had
|
|
170
172
|
// already been normalized. Stripping a trailing separator from
|
|
171
173
|
// normalizedHome (root "/" excepted) before the prefix check closes this.
|
|
174
|
+
// ML-2A (ADR-2026-09-04, D1 categoria 1 — "texto de relatorio / saida para humano"):
|
|
175
|
+
// os TRES retornos de caminho passam por normalizeRefSeparator. A variavel que recebe
|
|
176
|
+
// esta funcao chama-se literalmente displayPath (npm/src/commands/update-harness.js) e
|
|
177
|
+
// vai para o campo `path` do documento JSON do relatorio — o consumidor nunca e o
|
|
178
|
+
// sistema de arquivos. E o "~" que ela emite ja e POSIX-ismo puro (nenhum shell do
|
|
179
|
+
// Windows expande til), entao devolver "~\\.claude\\settings.json" era incoerente com a
|
|
180
|
+
// decisao ja tomada ao escolher o til.
|
|
181
|
+
//
|
|
182
|
+
// 🔴 A normalizacao e do VALOR DE SAIDA. path.normalize acima continua operando no
|
|
183
|
+
// separador nativo, e `absPath` (o caminho que o chamador realmente abre) nao e
|
|
184
|
+
// alterado por esta funcao — ela nao muda como o produto abre arquivo (ADR D2).
|
|
172
185
|
function tildeify(homeRoot, absPath) {
|
|
173
186
|
let normalizedHome = path.normalize(homeRoot)
|
|
174
187
|
if (normalizedHome.length > path.sep.length && normalizedHome.endsWith(path.sep)) {
|
|
@@ -176,8 +189,8 @@ function tildeify(homeRoot, absPath) {
|
|
|
176
189
|
}
|
|
177
190
|
const normalizedPath = path.normalize(absPath)
|
|
178
191
|
if (normalizedPath === normalizedHome) return '~'
|
|
179
|
-
if (normalizedPath.startsWith(normalizedHome + path.sep)) return '~' + normalizedPath.slice(normalizedHome.length)
|
|
180
|
-
return normalizedPath
|
|
192
|
+
if (normalizedPath.startsWith(normalizedHome + path.sep)) return normalizeRefSeparator('~' + normalizedPath.slice(normalizedHome.length))
|
|
193
|
+
return normalizeRefSeparator(normalizedPath)
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
// validateTargets — throws (usage error) when the caller asked for an id
|
package/src/serve/api_board.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const { resolveAgentNamespaces } = require('../validator')
|
|
6
|
+
const { normalizeRefSeparator } = require('../lib/pathfmt')
|
|
5
7
|
|
|
6
8
|
const STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
7
9
|
|
|
@@ -45,9 +47,15 @@ function scanState(dir, state, agent, roadmapDir) {
|
|
|
45
47
|
let content = ''
|
|
46
48
|
try { content = fs.readFileSync(path.join(dir, file), 'utf8') } catch (_) {}
|
|
47
49
|
const title = extractTitle(content, file)
|
|
48
|
-
|
|
50
|
+
// IDENTIFICADOR emitido em JSON, nao caminho de travessia (ADR-2026-09-04, D1
|
|
51
|
+
// categoria 2): o frontend devolve este valor verbatim em GET /api/file?path=...,
|
|
52
|
+
// onde o servidor refaz path.join — no Windows o join reconverte "/" para o
|
|
53
|
+
// separador nativo, entao o round-trip fecha. O path.join da linha 47 acima, que
|
|
54
|
+
// e o caminho realmente lido por fs.readFileSync, permanece intocado: as duas
|
|
55
|
+
// expressoes sao independentes sobre os mesmos operandos (ADR D2).
|
|
56
|
+
const relPath = normalizeRefSeparator(agent
|
|
49
57
|
? path.join(roadmapDir, agent, state, file)
|
|
50
|
-
: path.join(roadmapDir, state, file)
|
|
58
|
+
: path.join(roadmapDir, state, file))
|
|
51
59
|
|
|
52
60
|
items.push({ file, title, state, agent: agent || '', path: relPath })
|
|
53
61
|
}
|
|
@@ -68,14 +76,7 @@ function handleBoard(cfg, req, res) {
|
|
|
68
76
|
const agentSet = new Set()
|
|
69
77
|
|
|
70
78
|
if (namespacing === 'by_agent') {
|
|
71
|
-
|
|
72
|
-
if (!agents.length) {
|
|
73
|
-
try {
|
|
74
|
-
agents = fs.readdirSync(roadmapDir).filter(f => {
|
|
75
|
-
try { return fs.statSync(path.join(roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
76
|
-
})
|
|
77
|
-
} catch (_) { agents = [] }
|
|
78
|
-
}
|
|
79
|
+
const agents = resolveAgentNamespaces(cfg, roadmapDir)
|
|
79
80
|
for (const agent of agents) {
|
|
80
81
|
agentSet.add(agent)
|
|
81
82
|
for (const state of STATES) {
|
package/src/serve/api_chain.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const { resolveAgentNamespaces, extractRefPath } = require('../validator')
|
|
6
|
+
const { normalizeRefSeparator } = require('../lib/pathfmt')
|
|
5
7
|
|
|
6
8
|
const ROADMAP_STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
7
9
|
|
|
@@ -70,8 +72,16 @@ function collectNodes(dir, type, state) {
|
|
|
70
72
|
try { content = fs.readFileSync(path.join(dir, file), 'utf8') } catch (_) {}
|
|
71
73
|
const fm = parseFrontmatter(content)
|
|
72
74
|
const title = extractTitle(content, file)
|
|
73
|
-
|
|
74
|
-
|
|
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 })
|
|
75
85
|
}
|
|
76
86
|
return nodes
|
|
77
87
|
}
|
|
@@ -102,14 +112,7 @@ function handleChain(cfg, req, res) {
|
|
|
102
112
|
|
|
103
113
|
// REQs
|
|
104
114
|
if (namespacing === 'by_agent') {
|
|
105
|
-
|
|
106
|
-
if (!agents.length) {
|
|
107
|
-
try {
|
|
108
|
-
agents = fs.readdirSync(reqDir).filter(f => {
|
|
109
|
-
try { return fs.statSync(path.join(reqDir, f)).isDirectory() } catch (_) { return false }
|
|
110
|
-
})
|
|
111
|
-
} catch (_) {}
|
|
112
|
-
}
|
|
115
|
+
const agents = resolveAgentNamespaces(cfg, reqDir)
|
|
113
116
|
for (const agent of agents) {
|
|
114
117
|
for (const state of ROADMAP_STATES) {
|
|
115
118
|
const dir = path.join(reqDir, agent, state)
|
|
@@ -122,14 +125,7 @@ function handleChain(cfg, req, res) {
|
|
|
122
125
|
|
|
123
126
|
// Roadmaps
|
|
124
127
|
if (namespacing === 'by_agent') {
|
|
125
|
-
|
|
126
|
-
if (!agents.length) {
|
|
127
|
-
try {
|
|
128
|
-
agents = fs.readdirSync(roadmapDir).filter(f => {
|
|
129
|
-
try { return fs.statSync(path.join(roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
130
|
-
})
|
|
131
|
-
} catch (_) {}
|
|
132
|
-
}
|
|
128
|
+
const agents = resolveAgentNamespaces(cfg, roadmapDir)
|
|
133
129
|
for (const agent of agents) {
|
|
134
130
|
for (const state of ROADMAP_STATES) {
|
|
135
131
|
const dir = path.join(roadmapDir, agent, state)
|
|
@@ -173,8 +169,21 @@ function handleChain(cfg, req, res) {
|
|
|
173
169
|
|
|
174
170
|
function resolveRef(val) {
|
|
175
171
|
if (!val) return null
|
|
176
|
-
// Tenta pelo nome de arquivo
|
|
177
|
-
|
|
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()
|
|
178
187
|
if (fileIndex.has(base)) return fileIndex.get(base)
|
|
179
188
|
// Tenta pelo título
|
|
180
189
|
const norm = val.toLowerCase().trim()
|
|
@@ -206,6 +215,20 @@ function handleChain(cfg, req, res) {
|
|
|
206
215
|
if (target && target !== id) addEdge(id, target)
|
|
207
216
|
}
|
|
208
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
|
+
}
|
|
209
232
|
}
|
|
210
233
|
|
|
211
234
|
// Montar nodes de saída sem o campo fm interno
|
package/src/serve/api_metrics.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { parseLog, calculate } = require('../commands/metrics')
|
|
6
|
+
const { resolveAgentNamespaces } = require('../validator')
|
|
6
7
|
|
|
7
8
|
const STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
8
9
|
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|
@@ -34,14 +35,7 @@ function buildStateDistribution(cfg) {
|
|
|
34
35
|
const namespacing = cfg.roadmapNamespacing || 'flat'
|
|
35
36
|
|
|
36
37
|
if (namespacing === 'by_agent') {
|
|
37
|
-
|
|
38
|
-
if (!agents.length) {
|
|
39
|
-
try {
|
|
40
|
-
agents = fs.readdirSync(roadmapDir).filter(f => {
|
|
41
|
-
try { return fs.statSync(path.join(roadmapDir, f)).isDirectory() } catch (_) { return false }
|
|
42
|
-
})
|
|
43
|
-
} catch (_) { agents = [] }
|
|
44
|
-
}
|
|
38
|
+
const agents = resolveAgentNamespaces(cfg, roadmapDir)
|
|
45
39
|
for (const agent of agents) {
|
|
46
40
|
for (const state of STATES) {
|
|
47
41
|
dist[state] = (dist[state] || 0) + countFilesInState(path.join(roadmapDir, agent, state))
|