trackfw 7.3.0 → 7.4.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 +121 -6
- 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 +13 -17
- package/src/serve/api_metrics.js +2 -8
- package/src/validator/index.js +768 -167
|
@@ -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 } = require('../validator')
|
|
6
|
+
const { normalizeRefSeparator } = require('../lib/pathfmt')
|
|
5
7
|
|
|
6
8
|
const ROADMAP_STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
7
9
|
|
|
@@ -70,7 +72,15 @@ 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
|
-
|
|
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))
|
|
74
84
|
nodes.push({ id, type, title, state, fm })
|
|
75
85
|
}
|
|
76
86
|
return nodes
|
|
@@ -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)
|
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))
|