trackfw 2.4.1 → 2.5.1
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/commands/discover.js +13 -8
- package/src/commands/help.js +42 -0
- package/src/commands/validate.js +23 -3
- package/src/config/index.js +4 -2
- package/src/validator/index.js +47 -8
- package/src/validator/traceid.js +142 -0
package/package.json
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -330,14 +330,19 @@ cmd.action((opts) => {
|
|
|
330
330
|
console.log(`\nGovernance Score: ${r.governanceScore}/100`);
|
|
331
331
|
|
|
332
332
|
if (opts.init) {
|
|
333
|
-
const
|
|
334
|
-
fs.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
333
|
+
const yamlPath = path.join(cwd, 'trackfw.yaml');
|
|
334
|
+
if (fs.existsSync(yamlPath)) {
|
|
335
|
+
console.log('\n⚠ trackfw.yaml already exists — skipping (remove it first to regenerate)');
|
|
336
|
+
} else {
|
|
337
|
+
const yaml = generateYAML(r);
|
|
338
|
+
fs.writeFileSync(yamlPath, yaml, 'utf8');
|
|
339
|
+
console.log('\n✓ trackfw.yaml generated');
|
|
340
|
+
try {
|
|
341
|
+
installGates(r, cwd);
|
|
342
|
+
console.log('✓ governance gates installed');
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.log(`⚠ gates install partial: ${e.message}`);
|
|
345
|
+
}
|
|
341
346
|
}
|
|
342
347
|
}
|
|
343
348
|
|
package/src/commands/help.js
CHANGED
|
@@ -162,6 +162,48 @@ const configDocs = {
|
|
|
162
162
|
description: 'Severidade: REQ bloqueada por ADR em rascunho.',
|
|
163
163
|
example: 'rules:\n blocked_by_draft_adr: warning',
|
|
164
164
|
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
165
|
+
},
|
|
166
|
+
trace_id_field: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
default: '""',
|
|
169
|
+
description: 'Campo de frontmatter usado como trace ID para verificação bidirecional REQ↔Roadmap. Vazio desativa a verificação.',
|
|
170
|
+
example: 'trace_id_field: req_id',
|
|
171
|
+
impact: 'Quando configurado, ativa as regras traceid_* que garantem que toda REQ tem Roadmap correspondente e vice-versa.'
|
|
172
|
+
},
|
|
173
|
+
'rules.traceid_orphan_roadmap': {
|
|
174
|
+
type: 'off|warning|error',
|
|
175
|
+
default: '"error"',
|
|
176
|
+
description: 'Severidade: Roadmap com trace ID sem REQ correspondente.',
|
|
177
|
+
example: 'rules:\n traceid_orphan_roadmap: warning',
|
|
178
|
+
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
179
|
+
},
|
|
180
|
+
'rules.traceid_orphan_req': {
|
|
181
|
+
type: 'off|warning|error',
|
|
182
|
+
default: '"error"',
|
|
183
|
+
description: 'Severidade: REQ com trace ID sem Roadmap correspondente.',
|
|
184
|
+
example: 'rules:\n traceid_orphan_req: warning',
|
|
185
|
+
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
186
|
+
},
|
|
187
|
+
'rules.traceid_state_mismatch': {
|
|
188
|
+
type: 'off|warning|error',
|
|
189
|
+
default: '"error"',
|
|
190
|
+
description: 'Severidade: REQ e Roadmap com mesmo trace ID em estados diferentes (ex: REQ em done, Roadmap em wip).',
|
|
191
|
+
example: 'rules:\n traceid_state_mismatch: warning',
|
|
192
|
+
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
193
|
+
},
|
|
194
|
+
'rules.traceid_duplicate_req': {
|
|
195
|
+
type: 'off|warning|error',
|
|
196
|
+
default: '"error"',
|
|
197
|
+
description: 'Severidade: mesmo trace ID aparece em mais de uma REQ.',
|
|
198
|
+
example: 'rules:\n traceid_duplicate_req: warning',
|
|
199
|
+
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
200
|
+
},
|
|
201
|
+
'rules.traceid_duplicate_roadmap': {
|
|
202
|
+
type: 'off|warning|error',
|
|
203
|
+
default: '"error"',
|
|
204
|
+
description: 'Severidade: mesmo trace ID aparece em mais de um Roadmap.',
|
|
205
|
+
example: 'rules:\n traceid_duplicate_roadmap: warning',
|
|
206
|
+
impact: 'error bloqueia; warning apenas reporta; off desativa a regra.'
|
|
165
207
|
}
|
|
166
208
|
}
|
|
167
209
|
|
package/src/commands/validate.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { Command } = require('commander')
|
|
3
|
-
const { validate, isLenient, lenientUntilDate } = require('../validator')
|
|
3
|
+
const { validate, isLenient, lenientUntilDate, getItemMeta } = require('../validator')
|
|
4
4
|
const { t } = require('../i18n')
|
|
5
5
|
|
|
6
6
|
const cmd = new Command('validate')
|
|
7
7
|
cmd.description(t('validate.description'))
|
|
8
|
-
cmd.
|
|
8
|
+
cmd.option('--json', 'output result as JSON')
|
|
9
|
+
cmd.action(async (options) => {
|
|
9
10
|
const { violations, warnings } = await validate()
|
|
11
|
+
const lenient = isLenient()
|
|
12
|
+
const mode = lenient ? 'lenient' : 'strict'
|
|
13
|
+
const exitCode = violations.length > 0 ? 1 : 0
|
|
14
|
+
|
|
15
|
+
if (options.json) {
|
|
16
|
+
const output = {
|
|
17
|
+
summary: {
|
|
18
|
+
violations: violations.length,
|
|
19
|
+
warnings: warnings.length,
|
|
20
|
+
mode,
|
|
21
|
+
exit_code: exitCode,
|
|
22
|
+
},
|
|
23
|
+
violations: violations.map(v => { const m = getItemMeta(v); return { message: v, rule: m.rule, file: m.file } }),
|
|
24
|
+
warnings: warnings.map(w => { const m = getItemMeta(w); return { message: w, rule: m.rule, file: m.file } }),
|
|
25
|
+
}
|
|
26
|
+
console.log(JSON.stringify(output, null, 2))
|
|
27
|
+
process.exit(exitCode)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
10
30
|
|
|
11
31
|
// Informar usuário sobre modo lenient
|
|
12
|
-
if (
|
|
32
|
+
if (lenient) {
|
|
13
33
|
const until = lenientUntilDate()
|
|
14
34
|
if (until) {
|
|
15
35
|
console.log(`[LENIENT MODE] ${t('validate.lenient_mode', { date: until })}`)
|
package/src/config/index.js
CHANGED
|
@@ -15,6 +15,7 @@ function defaults() {
|
|
|
15
15
|
wipLimit: 1,
|
|
16
16
|
wipBySquad: false,
|
|
17
17
|
requireReqInCommit: false,
|
|
18
|
+
traceIdField: '',
|
|
18
19
|
// NOVOS campos:
|
|
19
20
|
linkFields: {
|
|
20
21
|
req: ['REQ:'],
|
|
@@ -165,8 +166,8 @@ function parse(content, cfg) {
|
|
|
165
166
|
|
|
166
167
|
switch (key) {
|
|
167
168
|
case 'adr_dirs': inAdrDirs = true; adrDirs = []; break;
|
|
168
|
-
case 'req_dir': cfg.reqDir = val; break;
|
|
169
|
-
case 'roadmap_dir': cfg.roadmapDir = val; break;
|
|
169
|
+
case 'req_dir': cfg.reqDir = val.replace(/^["']|["']$/g, ''); break;
|
|
170
|
+
case 'roadmap_dir': cfg.roadmapDir = val.replace(/^["']|["']$/g, ''); break;
|
|
170
171
|
case 'roadmap_namespacing': cfg.roadmapNamespacing = val; break;
|
|
171
172
|
case 'agents': inAgents = true; agents = []; break;
|
|
172
173
|
case 'governance_mode': cfg.governanceMode = val; break;
|
|
@@ -174,6 +175,7 @@ function parse(content, cfg) {
|
|
|
174
175
|
case 'wip_limit': { const n = parseInt(val, 10); if (n > 0) cfg.wipLimit = n; break; }
|
|
175
176
|
case 'wip_by_squad': cfg.wipBySquad = val === 'true'; break;
|
|
176
177
|
case 'require_req_in_commit': cfg.requireReqInCommit = val === 'true'; break;
|
|
178
|
+
case 'trace_id_field': cfg.traceIdField = val.replace(/^["']|["']$/g, ''); break;
|
|
177
179
|
case 'link_fields': inLinkFields = true; break;
|
|
178
180
|
case 'acceptance_markers': inAcceptanceMarkers = true; acceptanceMarkers = []; break;
|
|
179
181
|
case 'rules': inRules = true; rules = {}; break;
|
package/src/validator/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('fs')
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const { execSync } = require('child_process')
|
|
6
6
|
const config = require('../config')
|
|
7
|
+
const { checkTraceIds } = require('./traceid')
|
|
7
8
|
|
|
8
9
|
const STALE_WIP_DAYS = 7
|
|
9
10
|
|
|
@@ -699,6 +700,27 @@ function validateFilenameUniqueness() {
|
|
|
699
700
|
return violations
|
|
700
701
|
}
|
|
701
702
|
|
|
703
|
+
// _itemMeta: mapa de message → { rule, file } para enriquecer saída JSON.
|
|
704
|
+
// Populado em applyRule e nos pushs diretos do validateUnfiltered.
|
|
705
|
+
// Permanece em memória apenas durante a execução de uma chamada validate*.
|
|
706
|
+
const _itemMeta = new Map()
|
|
707
|
+
|
|
708
|
+
// _setMeta registra metadados de rule/file para uma mensagem.
|
|
709
|
+
function _setMeta(msg, ruleName) {
|
|
710
|
+
const m = /"([^"]+)"/.exec(msg)
|
|
711
|
+
_itemMeta.set(msg, { rule: ruleName, file: m ? m[1] : '' })
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// getItemMeta retorna { rule, file } para uma mensagem, ou { rule: '', file: '' } se ausente.
|
|
715
|
+
function getItemMeta(msg) {
|
|
716
|
+
return _itemMeta.get(msg) || { rule: '', file: '' }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// resetMeta limpa o mapa entre execuções (usado internamente).
|
|
720
|
+
function resetMeta() {
|
|
721
|
+
_itemMeta.clear()
|
|
722
|
+
}
|
|
723
|
+
|
|
702
724
|
// ruleSeverity retorna a severidade configurada para uma regra ('error'|'warning'|'off').
|
|
703
725
|
function ruleSeverity(name) {
|
|
704
726
|
const cfg = config.load()
|
|
@@ -707,14 +729,15 @@ function ruleSeverity(name) {
|
|
|
707
729
|
|
|
708
730
|
// applyRule distribui msgs para violations ou warnings conforme a severidade configurada.
|
|
709
731
|
// Se severidade for 'off', descarta silenciosamente.
|
|
732
|
+
// Também popula _itemMeta com rule/file para cada mensagem aceita.
|
|
710
733
|
function applyRule(ruleName, msgs, violations, warnings) {
|
|
711
734
|
if (!msgs || msgs.length === 0) return
|
|
712
735
|
const severity = ruleSeverity(ruleName)
|
|
713
736
|
if (severity === 'off') return
|
|
714
737
|
if (severity === 'warning') {
|
|
715
|
-
warnings.push(
|
|
738
|
+
for (const msg of msgs) { _setMeta(msg, ruleName); warnings.push(msg) }
|
|
716
739
|
} else {
|
|
717
|
-
violations.push(
|
|
740
|
+
for (const msg of msgs) { _setMeta(msg, ruleName); violations.push(msg) }
|
|
718
741
|
}
|
|
719
742
|
}
|
|
720
743
|
|
|
@@ -744,11 +767,12 @@ function saveBaseline(violations, warnings) {
|
|
|
744
767
|
|
|
745
768
|
// validateUnfiltered executa todas as validações e retorna { violations, warnings } sem ratchet.
|
|
746
769
|
async function validateUnfiltered() {
|
|
770
|
+
resetMeta()
|
|
747
771
|
const wipLimitResult = validateWIPLimit()
|
|
748
772
|
const violations = []
|
|
749
773
|
const warnings = []
|
|
750
774
|
|
|
751
|
-
// Regras com severidade configurável via applyRule
|
|
775
|
+
// Regras com severidade configurável via applyRule (popula _itemMeta automaticamente)
|
|
752
776
|
applyRule('wip_has_req', validateWIPHasREQ(), violations, warnings)
|
|
753
777
|
applyRule('wip_acceptance', validateWIPHasAcceptanceCriteria(), violations, warnings)
|
|
754
778
|
applyRule('wip_limit', wipLimitResult.violations, violations, warnings)
|
|
@@ -760,13 +784,25 @@ async function validateUnfiltered() {
|
|
|
760
784
|
applyRule('blocked_by_draft_adr', validateREQsNotBlockedByDraftADRs(), violations, warnings)
|
|
761
785
|
|
|
762
786
|
// Regras diretas (sem configuração de severidade): violations sempre
|
|
763
|
-
|
|
764
|
-
violations.push(
|
|
765
|
-
violations.push(
|
|
766
|
-
violations.push(
|
|
787
|
+
// Popular _itemMeta manualmente para manter rastreabilidade no JSON
|
|
788
|
+
for (const msg of validateREQsHaveADR()) { _setMeta(msg, 'req_has_adr'); violations.push(msg) }
|
|
789
|
+
for (const msg of validateBlockedHasREQ()) { _setMeta(msg, 'blocked_has_req'); violations.push(msg) }
|
|
790
|
+
for (const msg of validateREQsHaveRoadmap()) { _setMeta(msg, 'req_has_roadmap'); violations.push(msg) }
|
|
791
|
+
for (const msg of validateFrontmatterPresence()) { _setMeta(msg, 'frontmatter_presence'); violations.push(msg) }
|
|
767
792
|
|
|
768
793
|
// warnings diretos do WIP limit (não configuráveis)
|
|
769
|
-
|
|
794
|
+
for (const msg of wipLimitResult.warnings) { _setMeta(msg, 'wip_limit'); warnings.push(msg) }
|
|
795
|
+
|
|
796
|
+
// Verificação bidirecional de trace ID (somente se traceIdField configurado)
|
|
797
|
+
const cfg = config.load()
|
|
798
|
+
if (cfg.traceIdField) {
|
|
799
|
+
for (const msg of checkTraceIds(cfg.reqDir, cfg.roadmapDir, cfg.traceIdField)) {
|
|
800
|
+
// O prefixo da mensagem traceid já carrega o nome da regra (ex: "traceid_orphan_roadmap: ...")
|
|
801
|
+
const ruleName = msg.split(':')[0].trim()
|
|
802
|
+
_setMeta(msg, ruleName)
|
|
803
|
+
violations.push(msg)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
770
806
|
|
|
771
807
|
return { violations, warnings }
|
|
772
808
|
}
|
|
@@ -910,4 +946,7 @@ module.exports = {
|
|
|
910
946
|
contentHasMarker,
|
|
911
947
|
ruleSeverity,
|
|
912
948
|
applyRule,
|
|
949
|
+
// novas funções ML-1B (v2.5.1)
|
|
950
|
+
getItemMeta,
|
|
951
|
+
resetMeta,
|
|
913
952
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// ESTADOS reconhecidos para REQs e Roadmaps (baseado na pasta onde o arquivo reside)
|
|
7
|
+
const KNOWN_STATES = ['wip', 'backlog', 'blocked', 'done', 'abandoned']
|
|
8
|
+
|
|
9
|
+
// extractFrontmatterField extrai o valor de um campo do frontmatter YAML simples de um arquivo .md.
|
|
10
|
+
// Retorna string vazia se o campo não for encontrado ou o arquivo não tiver frontmatter.
|
|
11
|
+
function extractFrontmatterField(filePath, fieldName) {
|
|
12
|
+
let content
|
|
13
|
+
try { content = fs.readFileSync(filePath, 'utf8') } catch (_) { return '' }
|
|
14
|
+
if (!content.startsWith('---')) return ''
|
|
15
|
+
const end = content.indexOf('\n---', 3)
|
|
16
|
+
const block = end > 0 ? content.slice(3, end) : content.slice(3)
|
|
17
|
+
for (const line of block.split('\n')) {
|
|
18
|
+
const trimmed = line.trim()
|
|
19
|
+
const prefix = fieldName + ':'
|
|
20
|
+
if (trimmed.startsWith(prefix)) {
|
|
21
|
+
return trimmed.slice(prefix.length).trim().replace(/^["']|["']$/g, '')
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// stateFromPath extrai o estado (wip/backlog/blocked/done/abandoned) a partir do caminho do arquivo.
|
|
28
|
+
// Percorre os segmentos de path de trás para frente e retorna o primeiro que for um estado reconhecido.
|
|
29
|
+
// Retorna '' se nenhum segmento for reconhecido.
|
|
30
|
+
function stateFromPath(filePath) {
|
|
31
|
+
const segments = filePath.split(path.sep)
|
|
32
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
33
|
+
if (KNOWN_STATES.includes(segments[i])) return segments[i]
|
|
34
|
+
}
|
|
35
|
+
return ''
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// walkMd retorna array de caminhos absolutos de todos .md recursivamente dentro de dir.
|
|
39
|
+
function walkMd(dir) {
|
|
40
|
+
const results = []
|
|
41
|
+
function walk(d) {
|
|
42
|
+
let entries
|
|
43
|
+
try { entries = fs.readdirSync(d) } catch (_) { return }
|
|
44
|
+
for (const name of entries) {
|
|
45
|
+
const full = path.join(d, name)
|
|
46
|
+
try {
|
|
47
|
+
if (fs.statSync(full).isDirectory()) { walk(full) }
|
|
48
|
+
else if (name.endsWith('.md')) { results.push(full) }
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
walk(dir)
|
|
53
|
+
return results
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// checkTraceIds verifica a consistência bidirecional de req_id entre REQs e Roadmaps.
|
|
57
|
+
// Parâmetros:
|
|
58
|
+
// reqDir — caminho absoluto ou relativo do diretório de REQs
|
|
59
|
+
// roadmapDir — caminho absoluto ou relativo do diretório de Roadmaps
|
|
60
|
+
// fieldName — nome do campo de frontmatter que contém o trace ID (ex: 'req_id')
|
|
61
|
+
// Retorna array de strings de violation.
|
|
62
|
+
function checkTraceIds(reqDir, roadmapDir, fieldName) {
|
|
63
|
+
if (!fieldName) return []
|
|
64
|
+
|
|
65
|
+
// --- Indexar REQs ---
|
|
66
|
+
// reqIndex: Map<traceId, [{file, state}]>
|
|
67
|
+
const reqIndex = new Map()
|
|
68
|
+
for (const filePath of walkMd(reqDir)) {
|
|
69
|
+
const traceId = extractFrontmatterField(filePath, fieldName)
|
|
70
|
+
if (!traceId) continue
|
|
71
|
+
const state = stateFromPath(filePath)
|
|
72
|
+
if (!reqIndex.has(traceId)) reqIndex.set(traceId, [])
|
|
73
|
+
reqIndex.get(traceId).push({ file: path.basename(filePath), state })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Indexar Roadmaps ---
|
|
77
|
+
// roadmapIndex: Map<traceId, [{file, state}]>
|
|
78
|
+
const roadmapIndex = new Map()
|
|
79
|
+
for (const filePath of walkMd(roadmapDir)) {
|
|
80
|
+
const traceId = extractFrontmatterField(filePath, fieldName)
|
|
81
|
+
if (!traceId) continue
|
|
82
|
+
const state = stateFromPath(filePath)
|
|
83
|
+
if (!roadmapIndex.has(traceId)) roadmapIndex.set(traceId, [])
|
|
84
|
+
roadmapIndex.get(traceId).push({ file: path.basename(filePath), state })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const violations = []
|
|
88
|
+
|
|
89
|
+
// traceid_duplicate_req: mesmo req_id em >1 REQ
|
|
90
|
+
for (const [traceId, entries] of reqIndex.entries()) {
|
|
91
|
+
if (entries.length > 1) {
|
|
92
|
+
const files = entries.map(e => e.file).join(', ')
|
|
93
|
+
violations.push(`traceid_duplicate_req: req_id "${traceId}" appears in ${entries.length} REQs: ${files}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// traceid_duplicate_roadmap: mesmo req_id em >1 Roadmap
|
|
98
|
+
for (const [traceId, entries] of roadmapIndex.entries()) {
|
|
99
|
+
if (entries.length > 1) {
|
|
100
|
+
const files = entries.map(e => e.file).join(', ')
|
|
101
|
+
violations.push(`traceid_duplicate_roadmap: req_id "${traceId}" appears in ${entries.length} Roadmaps: ${files}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// traceid_orphan_roadmap: Roadmap com req_id sem REQ correspondente
|
|
106
|
+
for (const [traceId, entries] of roadmapIndex.entries()) {
|
|
107
|
+
if (!reqIndex.has(traceId)) {
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
violations.push(`traceid_orphan_roadmap: roadmap "${e.file}" has req_id "${traceId}" but no matching REQ`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// traceid_orphan_req: REQ com req_id sem Roadmap correspondente
|
|
115
|
+
for (const [traceId, entries] of reqIndex.entries()) {
|
|
116
|
+
if (!roadmapIndex.has(traceId)) {
|
|
117
|
+
for (const e of entries) {
|
|
118
|
+
violations.push(`traceid_orphan_req: req "${e.file}" has req_id "${traceId}" but no matching Roadmap`)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// traceid_state_mismatch: REQ e Roadmap com mesmo req_id em estados diferentes
|
|
124
|
+
for (const [traceId, reqEntries] of reqIndex.entries()) {
|
|
125
|
+
if (!roadmapIndex.has(traceId)) continue
|
|
126
|
+
const roadmapEntries = roadmapIndex.get(traceId)
|
|
127
|
+
// Comparar todos os pares (normalmente 1x1, mas suporta duplicados já reportados acima)
|
|
128
|
+
for (const req of reqEntries) {
|
|
129
|
+
for (const rm of roadmapEntries) {
|
|
130
|
+
if (req.state && rm.state && req.state !== rm.state) {
|
|
131
|
+
violations.push(
|
|
132
|
+
`traceid_state_mismatch: req_id "${traceId}" — REQ "${req.file}" is in "${req.state}" but Roadmap "${rm.file}" is in "${rm.state}"`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return violations
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { checkTraceIds, extractFrontmatterField, stateFromPath }
|