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 +1 -1
- package/src/validator/index.js +62 -26
- package/src/validator/traceid.js +8 -0
package/package.json
CHANGED
package/src/validator/index.js
CHANGED
|
@@ -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
|
|
212
|
+
const files = resolveReqFiles(cfg)
|
|
176
213
|
const violations = []
|
|
177
|
-
for (const
|
|
214
|
+
for (const filePath of files) {
|
|
178
215
|
try {
|
|
179
|
-
const content = fs.readFileSync(
|
|
216
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
180
217
|
if (!contentHasMarker(content, cfg.linkFields.adr)) {
|
|
181
|
-
violations.push(`req "${
|
|
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
|
|
248
|
+
const files = resolveReqFiles(cfg)
|
|
212
249
|
const violations = []
|
|
213
|
-
for (const
|
|
250
|
+
for (const filePath of files) {
|
|
214
251
|
try {
|
|
215
|
-
const content = fs.readFileSync(
|
|
252
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
216
253
|
if (!contentHasMarker(content, cfg.linkFields.roadmap)) {
|
|
217
|
-
violations.push(`req "${
|
|
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
|
|
271
|
+
const reqFiles = resolveReqFiles(cfg)
|
|
235
272
|
let combined = ''
|
|
236
|
-
for (const
|
|
273
|
+
for (const filePath of reqFiles) {
|
|
237
274
|
try {
|
|
238
|
-
combined += fs.readFileSync(
|
|
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
|
|
464
|
+
const files = resolveReqFiles(cfg)
|
|
428
465
|
const violations = []
|
|
429
|
-
for (const
|
|
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 ${
|
|
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
|
|
488
|
+
const files = resolveReqFiles(cfg)
|
|
453
489
|
const result = {}
|
|
454
|
-
for (const
|
|
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[
|
|
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
|
-
|
|
535
|
-
|
|
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(
|
|
572
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
539
573
|
if (!content.startsWith('---')) {
|
|
540
|
-
violations.push(`req "${
|
|
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
|
|
617
|
+
for (const filePath of resolveReqFiles(cfg)) {
|
|
584
618
|
try {
|
|
585
|
-
const content = fs.readFileSync(
|
|
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,
|
package/src/validator/traceid.js
CHANGED
|
@@ -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) {
|