trackfw 2.5.2 → 2.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackfw",
3
- "version": "2.5.2",
3
+ "version": "2.5.3",
4
4
  "description": "CLI de governança para entrega de software: ADR → REQ → ROADMAP → kanban. Suporte nativo a agentes de IA (Claude Code, Gemini CLI, Cursor).",
5
5
  "keywords": [
6
6
  "cli",
@@ -82,6 +82,43 @@ function gitLastModifiedTime(filePath) {
82
82
  return null
83
83
  }
84
84
 
85
+ // resolveReqFiles retorna array de paths completos de arquivos .md de REQs.
86
+ // Em modo by_agent percorre reqDir/<agente>/<estado>/; em modo flat varre reqDir/ diretamente.
87
+ function resolveReqFiles(cfg) {
88
+ const reqDir = cfg.reqDir || cfg.req_dir || ''
89
+ if (!reqDir) return []
90
+ const namespacing = cfg.roadmapNamespacing || cfg.roadmap_namespacing || ''
91
+ if (namespacing === 'by_agent') {
92
+ const STATES = ['backlog', 'wip', 'blocked', 'done', 'abandoned']
93
+ let agents = cfg.agents || []
94
+ if (!agents.length) {
95
+ try {
96
+ agents = fs.readdirSync(reqDir).filter(e => {
97
+ try { return fs.statSync(path.join(reqDir, e)).isDirectory() } catch (_) { return false }
98
+ })
99
+ } catch (_) { return [] }
100
+ }
101
+ const files = []
102
+ for (const agent of agents) {
103
+ for (const state of STATES) {
104
+ const dir = path.join(reqDir, agent, state)
105
+ try {
106
+ for (const name of fs.readdirSync(dir)) {
107
+ if (name.endsWith('.md')) files.push(path.join(dir, name))
108
+ }
109
+ } catch (_) {}
110
+ }
111
+ }
112
+ return files
113
+ }
114
+ // flat (comportamento anterior) — retorna paths completos
115
+ try {
116
+ return fs.readdirSync(reqDir)
117
+ .filter(n => n.endsWith('.md') && !fs.statSync(path.join(reqDir, n)).isDirectory())
118
+ .map(n => path.join(reqDir, n))
119
+ } catch (_) { return [] }
120
+ }
121
+
85
122
  // resolveWIPDirs retorna todos os diretórios wip/ conforme o modo de namespacing.
86
123
  function resolveWIPDirs(cfg) {
87
124
  if (cfg.roadmapNamespacing === config.NAMESPACING_BY_AGENT) {
@@ -172,13 +209,13 @@ function validateWIPHasREQ() {
172
209
  // validateREQsHaveADR — REQs em <reqDir>/ sem marker ADR no conteúdo → violation
173
210
  function validateREQsHaveADR() {
174
211
  const cfg = config.load()
175
- const entries = listDir(cfg.reqDir)
212
+ const files = resolveReqFiles(cfg)
176
213
  const violations = []
177
- for (const name of entries) {
214
+ for (const filePath of files) {
178
215
  try {
179
- const content = fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
216
+ const content = fs.readFileSync(filePath, 'utf8')
180
217
  if (!contentHasMarker(content, cfg.linkFields.adr)) {
181
- violations.push(`req "${name}" has no linked ADR`)
218
+ violations.push(`req "${path.basename(filePath)}" has no linked ADR`)
182
219
  }
183
220
  } catch (_) {
184
221
  // ignorar
@@ -208,13 +245,13 @@ function validateBlockedHasREQ() {
208
245
  // validateREQsHaveRoadmap — REQs sem marker Roadmap → violation
209
246
  function validateREQsHaveRoadmap() {
210
247
  const cfg = config.load()
211
- const entries = listDir(cfg.reqDir)
248
+ const files = resolveReqFiles(cfg)
212
249
  const violations = []
213
- for (const name of entries) {
250
+ for (const filePath of files) {
214
251
  try {
215
- const content = fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
252
+ const content = fs.readFileSync(filePath, 'utf8')
216
253
  if (!contentHasMarker(content, cfg.linkFields.roadmap)) {
217
- violations.push(`req "${name}" has no linked Roadmap`)
254
+ violations.push(`req "${path.basename(filePath)}" has no linked Roadmap`)
218
255
  }
219
256
  } catch (_) {
220
257
  // ignorar
@@ -231,11 +268,11 @@ function validateADRsAreReferenced() {
231
268
  adrs = adrs.concat(walkDirMd(adrDir))
232
269
  }
233
270
 
234
- const reqEntries = listDir(cfg.reqDir)
271
+ const reqFiles = resolveReqFiles(cfg)
235
272
  let combined = ''
236
- for (const name of reqEntries) {
273
+ for (const filePath of reqFiles) {
237
274
  try {
238
- combined += fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
275
+ combined += fs.readFileSync(filePath, 'utf8')
239
276
  } catch (_) {
240
277
  // ignorar
241
278
  }
@@ -424,10 +461,9 @@ function validateStaleWIP() {
424
461
  // validateREQsNotBlockedByDraftADRs — REQs Open com ADRs Draft na seção "## Blocked by ADRs" → violation
425
462
  function validateREQsNotBlockedByDraftADRs() {
426
463
  const cfg = config.load()
427
- const entries = listDir(cfg.reqDir)
464
+ const files = resolveReqFiles(cfg)
428
465
  const violations = []
429
- for (const name of entries) {
430
- const filePath = path.join(cfg.reqDir, name)
466
+ for (const filePath of files) {
431
467
  let content
432
468
  try {
433
469
  content = fs.readFileSync(filePath, 'utf8')
@@ -439,7 +475,7 @@ function validateREQsNotBlockedByDraftADRs() {
439
475
  const blockedADRs = parseBlockedADRs(filePath)
440
476
  for (const adrBasename of blockedADRs) {
441
477
  if (adrIsDraft(adrBasename)) {
442
- violations.push(`REQ ${name} is blocked by Draft ADR: ${adrBasename}`)
478
+ violations.push(`REQ ${path.basename(filePath)} is blocked by Draft ADR: ${adrBasename}`)
443
479
  }
444
480
  }
445
481
  }
@@ -449,10 +485,9 @@ function validateREQsNotBlockedByDraftADRs() {
449
485
  // blockedREQs retorna mapa de reqBasename → [adrBasenames Draft] para uso em getStatus()
450
486
  function blockedREQs() {
451
487
  const cfg = config.load()
452
- const entries = listDir(cfg.reqDir)
488
+ const files = resolveReqFiles(cfg)
453
489
  const result = {}
454
- for (const name of entries) {
455
- const filePath = path.join(cfg.reqDir, name)
490
+ for (const filePath of files) {
456
491
  let content
457
492
  try {
458
493
  content = fs.readFileSync(filePath, 'utf8')
@@ -464,7 +499,7 @@ function blockedREQs() {
464
499
  const adrNames = parseBlockedADRs(filePath)
465
500
  const draftADRs = adrNames.filter(a => adrIsDraft(a))
466
501
  if (draftADRs.length > 0) {
467
- result[name] = draftADRs
502
+ result[path.basename(filePath)] = draftADRs
468
503
  }
469
504
  }
470
505
  return result
@@ -531,13 +566,12 @@ function validateFrontmatterPresence() {
531
566
  }
532
567
  }
533
568
 
534
- let reqFiles = []
535
- try { reqFiles = listDir(cfg.reqDir).filter(f => f.endsWith('.md')) } catch (_) {}
536
- for (const f of reqFiles) {
569
+ const reqFilePaths = resolveReqFiles(cfg)
570
+ for (const filePath of reqFilePaths) {
537
571
  try {
538
- const content = fs.readFileSync(path.join(cfg.reqDir, f), 'utf8')
572
+ const content = fs.readFileSync(filePath, 'utf8')
539
573
  if (!content.startsWith('---')) {
540
- violations.push(`req "${f}" has no frontmatter block`)
574
+ violations.push(`req "${path.basename(filePath)}" has no frontmatter block`)
541
575
  }
542
576
  } catch (_) {}
543
577
  }
@@ -580,9 +614,10 @@ function validateRefTargetsExist() {
580
614
  }
581
615
 
582
616
  // REQs: verificar ADR: e Roadmap:
583
- for (const name of listDir(cfg.reqDir)) {
617
+ for (const filePath of resolveReqFiles(cfg)) {
584
618
  try {
585
- const content = fs.readFileSync(path.join(cfg.reqDir, name), 'utf8')
619
+ const content = fs.readFileSync(filePath, 'utf8')
620
+ const name = path.basename(filePath)
586
621
  const adrRef = extractRefPath(content, 'ADR')
587
622
  if (adrRef && !fs.existsSync(adrRef)) {
588
623
  warnings.push(`req "${name}" links to ADR "${adrRef}" which does not exist`)
@@ -929,6 +964,7 @@ module.exports = {
929
964
  parseBlockedADRs,
930
965
  adrIsDraft,
931
966
  listDir,
967
+ resolveReqFiles,
932
968
  resolveWIPDirs,
933
969
  readGovernanceMode,
934
970
  readWIPConfig,
@@ -91,6 +91,14 @@ function checkTraceIds(reqDir, roadmapDir, fieldName) {
91
91
 
92
92
  const violations = []
93
93
 
94
+ // Salvaguarda one-sided: um lado indexado e o outro não
95
+ if (reqIndex.size === 0 && roadmapIndex.size > 0) {
96
+ violations.push(`traceid_config_warning: trace_id_field is set but REQs (0) were indexed while Roadmaps (${roadmapIndex.size}) were — check reqDir and roadmap_namespacing`)
97
+ }
98
+ if (roadmapIndex.size === 0 && reqIndex.size > 0) {
99
+ violations.push(`traceid_config_warning: trace_id_field is set but Roadmaps (0) were indexed while REQs (${reqIndex.size}) were — check roadmapDir and roadmap_namespacing`)
100
+ }
101
+
94
102
  // traceid_duplicate_req: mesmo req_id em >1 REQ
95
103
  for (const [traceId, entries] of reqIndex.entries()) {
96
104
  if (entries.length > 1) {