2121 - name : Checkout repository
2222 uses : actions/checkout@v4
2323
24+ - name : Install validation dependencies
25+ run : pip install pyyaml
26+
2427 - name : Setup Python
2528 uses : actions/setup-python@v5
2629 with :
3235 import os
3336 import re
3437 import sys
35- from collections import defaultdict
38+ from typing import Iterable, List
39+
40+ import yaml
41+
42+ FRONT_MATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---\s*", re.DOTALL)
43+
44+ def ensure_list(value: Iterable | str | None) -> List[str]:
45+ if value is None:
46+ return []
47+ if isinstance(value, str):
48+ return [value.strip()] if value.strip() else []
49+ return [str(item).strip() for item in value if str(item).strip()]
3650
3751 print("Validating requirements traceability...")
3852 print("=" * 80)
4155 requirements = {}
4256 broken_links = []
4357 orphaned_requirements = []
58+ invalid_front_matter = []
4459
4560 # First pass: collect all requirement IDs
46- for root, dirs , files in os.walk('implementacion'):
61+ for root, _ , files in os.walk('implementacion'):
4762 if 'requisitos' not in root:
4863 continue
4964
@@ -56,84 +71,62 @@ jobs:
5671 with open(filepath, 'r', encoding='utf-8') as f:
5772 content = f.read()
5873
59- match = re.match(r'^---\s*
60- (.*?)
61- ---\s*
62- ' , content, re.DOTALL)
74+ match = FRONT_MATTER_PATTERN.match(content)
6375 if not match:
6476 continue
6577
66- yaml_content = match.group(1)
67- fields = {}
68- current_list = None
69-
70- for line in yaml_content.split('
71- ' ):
72- line = line.rstrip()
73-
74- if current_list and line.startswith(' - '):
75- value = line[4:].strip()
76- fields[current_list].append(value)
77- else :
78- current_list = None
79-
80- if ':' in line and not line.startswith(' ') :
81- key, value = line.split(':', 1)
82- key = key.strip()
83- value = value.strip()
84-
85- if value == '[]' or not value :
86- fields[key] = []
87- current_list = key
88- else :
89- fields[key] = value
90-
91- if 'id' in fields :
92- req_id = fields['id']
93- all_req_ids.add(req_id)
94- requirements[req_id] = {
95- ' path ' : filepath,
96- ' tipo ' : fields.get('tipo', ''),
97- ' upward ' : fields.get('trazabilidad_upward', []),
98- ' downward ' : fields.get('trazabilidad_downward', [])
99- }
78+ try:
79+ metadata = yaml.safe_load(match.group(1)) or {}
80+ except yaml.YAMLError as exc:
81+ invalid_front_matter.append({'path': filepath, 'error': str(exc)})
82+ continue
83+
84+ req_id = metadata.get('id')
85+ if not req_id:
86+ continue
87+
88+ all_req_ids.add(req_id)
89+ requirements[req_id] = {
90+ 'path': filepath,
91+ 'tipo': metadata.get('tipo', ''),
92+ 'upward': ensure_list(metadata.get('trazabilidad_upward')),
93+ 'downward': ensure_list(metadata.get('trazabilidad_downward')),
94+ }
10095
10196 print(f"Found {len(all_req_ids)} requirements")
10297
10398 # Second pass: validate traceability links
10499 for req_id, data in requirements.items():
105- # Check upward references
106100 for parent_id in data['upward']:
107101 if parent_id not in all_req_ids:
108102 broken_links.append({
109103 'req_id': req_id,
110104 'path': data['path'],
111105 'missing': parent_id,
112- ' direction ' : ' upward'
106+ 'direction': 'upward',
113107 })
114108
115- # Check downward references
116109 for child_id in data['downward']:
117110 if child_id not in all_req_ids:
118111 broken_links.append({
119112 'req_id': req_id,
120113 'path': data['path'],
121114 'missing': child_id,
122- ' direction ' : ' downward'
115+ 'direction': 'downward',
123116 })
124117
125- # Check for orphaned requirements (no upward traceability)
126118 if data['tipo'] not in ['necesidad'] and not data['upward']:
127119 orphaned_requirements.append({
128120 'req_id': req_id,
129- ' path ' : data['path'],
130- ' tipo ' : data['tipo']
121+ 'path': data['path'],
122+ 'tipo': data['tipo'],
131123 })
132124
133125 print("")
134126 print("Results:")
135127 print(f" Broken links: {len(broken_links)}")
136128 print(f" Orphaned requirements: {len(orphaned_requirements)}")
129+ print(f" Invalid front matter: {len(invalid_front_matter)}")
137130
138131 if broken_links:
139132 print("")
@@ -148,15 +141,22 @@ jobs:
148141 for req in orphaned_requirements:
149142 print(f" {req['req_id']} ({req['tipo']}) - {req['path']}")
150143
144+ if invalid_front_matter:
145+ print("")
146+ print("INVALID FRONT MATTER:")
147+ for item in invalid_front_matter:
148+ print(f" {item['path']}")
149+ print(f" -> {item['error']}")
150+
151151 print("")
152- if broken_links :
153- print("VALIDATION FAILED : Broken traceability links found")
152+ if broken_links or invalid_front_matter :
153+ print("VALIDATION FAILED: Broken traceability links or invalid front matter found")
154154 sys.exit(1)
155- else :
156- print("VALIDATION PASSED : All traceability links are valid")
157- if orphaned_requirements :
158- print(f"WARNING : {len(orphaned_requirements)} orphaned requirements (informational)")
159- sys.exit(0)
155+
156+ print("VALIDATION PASSED: All traceability links are valid")
157+ if orphaned_requirements:
158+ print(f"WARNING: {len(orphaned_requirements)} orphaned requirements (informational)")
159+ sys.exit(0)
160160 EOF
161161
162162 - name : Generate traceability report
0 commit comments