elspais 0.11.1__py3-none-any.whl → 0.11.2__py3-none-any.whl

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.
Files changed (53) hide show
  1. elspais/__init__.py +1 -1
  2. elspais/cli.py +29 -10
  3. elspais/commands/analyze.py +5 -6
  4. elspais/commands/changed.py +2 -6
  5. elspais/commands/config_cmd.py +4 -4
  6. elspais/commands/edit.py +32 -36
  7. elspais/commands/hash_cmd.py +24 -18
  8. elspais/commands/index.py +8 -7
  9. elspais/commands/init.py +4 -4
  10. elspais/commands/reformat_cmd.py +32 -43
  11. elspais/commands/rules_cmd.py +6 -2
  12. elspais/commands/trace.py +23 -19
  13. elspais/commands/validate.py +8 -10
  14. elspais/config/defaults.py +7 -1
  15. elspais/core/content_rules.py +0 -1
  16. elspais/core/git.py +4 -10
  17. elspais/core/parser.py +55 -56
  18. elspais/core/patterns.py +2 -6
  19. elspais/core/rules.py +10 -15
  20. elspais/mcp/__init__.py +2 -0
  21. elspais/mcp/context.py +1 -0
  22. elspais/mcp/serializers.py +1 -1
  23. elspais/mcp/server.py +54 -39
  24. elspais/reformat/__init__.py +13 -13
  25. elspais/reformat/detector.py +9 -16
  26. elspais/reformat/hierarchy.py +8 -7
  27. elspais/reformat/line_breaks.py +36 -38
  28. elspais/reformat/prompts.py +22 -12
  29. elspais/reformat/transformer.py +43 -41
  30. elspais/sponsors/__init__.py +0 -2
  31. elspais/testing/__init__.py +1 -1
  32. elspais/testing/result_parser.py +25 -21
  33. elspais/trace_view/__init__.py +4 -3
  34. elspais/trace_view/coverage.py +5 -5
  35. elspais/trace_view/generators/__init__.py +1 -1
  36. elspais/trace_view/generators/base.py +17 -12
  37. elspais/trace_view/generators/csv.py +2 -6
  38. elspais/trace_view/generators/markdown.py +3 -8
  39. elspais/trace_view/html/__init__.py +4 -2
  40. elspais/trace_view/html/generator.py +423 -289
  41. elspais/trace_view/models.py +25 -0
  42. elspais/trace_view/review/__init__.py +21 -18
  43. elspais/trace_view/review/branches.py +114 -121
  44. elspais/trace_view/review/models.py +232 -237
  45. elspais/trace_view/review/position.py +53 -71
  46. elspais/trace_view/review/server.py +264 -288
  47. elspais/trace_view/review/status.py +43 -58
  48. elspais/trace_view/review/storage.py +48 -72
  49. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/METADATA +1 -1
  50. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/RECORD +53 -53
  51. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/WHEEL +0 -0
  52. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/entry_points.txt +0 -0
  53. {elspais-0.11.1.dist-info → elspais-0.11.2.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,6 @@ Contains:
15
15
  """
16
16
 
17
17
  import json
18
- import re
19
18
  import sys
20
19
  from datetime import datetime
21
20
  from pathlib import Path
@@ -23,8 +22,13 @@ from typing import Dict, List, Optional, Set
23
22
 
24
23
  from jinja2 import Environment, FileSystemLoader, select_autoescape
25
24
 
25
+ from elspais.trace_view.coverage import (
26
+ calculate_coverage,
27
+ count_by_level,
28
+ find_orphaned_requirements,
29
+ get_implementation_status,
30
+ )
26
31
  from elspais.trace_view.models import TraceViewRequirement as Requirement
27
- from elspais.trace_view.coverage import count_by_level, find_orphaned_requirements, calculate_coverage, get_implementation_status
28
32
 
29
33
 
30
34
  class HTMLGenerator:
@@ -45,11 +49,11 @@ class HTMLGenerator:
45
49
  def __init__(
46
50
  self,
47
51
  requirements: Dict[str, Requirement],
48
- base_path: str = '',
49
- mode: str = 'core',
52
+ base_path: str = "",
53
+ mode: str = "core",
50
54
  sponsor: Optional[str] = None,
51
55
  version: int = 16,
52
- repo_root: Optional[Path] = None
56
+ repo_root: Optional[Path] = None,
53
57
  ):
54
58
  self.requirements = requirements
55
59
  self._base_path = base_path
@@ -65,20 +69,17 @@ class HTMLGenerator:
65
69
  template_dir = Path(__file__).parent / "templates"
66
70
  self.env = Environment(
67
71
  loader=FileSystemLoader(template_dir),
68
- autoescape=select_autoescape(['html', 'xml']),
72
+ autoescape=select_autoescape(["html", "xml"]),
69
73
  trim_blocks=True,
70
- lstrip_blocks=True
74
+ lstrip_blocks=True,
71
75
  )
72
76
 
73
77
  # Register custom filters for templates
74
- self.env.filters['status_class'] = lambda s: s.lower() if s else ''
75
- self.env.filters['level_class'] = lambda s: s.lower() if s else ''
78
+ self.env.filters["status_class"] = lambda s: s.lower() if s else ""
79
+ self.env.filters["level_class"] = lambda s: s.lower() if s else ""
76
80
 
77
81
  def generate(
78
- self,
79
- embed_content: bool = False,
80
- edit_mode: bool = False,
81
- review_mode: bool = False
82
+ self, embed_content: bool = False, edit_mode: bool = False, review_mode: bool = False
82
83
  ) -> str:
83
84
  """Generate the complete HTML report using Jinja2 templates.
84
85
 
@@ -94,7 +95,7 @@ class HTMLGenerator:
94
95
  jinja2.TemplateError: If template rendering fails
95
96
  """
96
97
  context = self._build_render_context(embed_content, edit_mode, review_mode)
97
- template = self.env.get_template('base.html')
98
+ template = self.env.get_template("base.html")
98
99
  return template.render(**context)
99
100
 
100
101
  def _count_by_level(self) -> Dict[str, Dict[str, int]]:
@@ -111,14 +112,14 @@ class HTMLGenerator:
111
112
  repo_counts: Dict[str, Dict[str, int]] = {}
112
113
 
113
114
  for req in self.requirements.values():
114
- prefix = req.repo_prefix or 'CORE' # Use CORE for core repo
115
+ prefix = req.repo_prefix or "CORE" # Use CORE for core repo
115
116
 
116
117
  if prefix not in repo_counts:
117
- repo_counts[prefix] = {'active': 0, 'all': 0}
118
+ repo_counts[prefix] = {"active": 0, "all": 0}
118
119
 
119
- repo_counts[prefix]['all'] += 1
120
- if req.status != 'Deprecated':
121
- repo_counts[prefix]['active'] += 1
120
+ repo_counts[prefix]["all"] += 1
121
+ if req.status != "Deprecated":
122
+ repo_counts[prefix]["active"] += 1
122
123
 
123
124
  return repo_counts
124
125
 
@@ -191,16 +192,16 @@ class HTMLGenerator:
191
192
 
192
193
  # Load modules in dependency order (REQ-d00092)
193
194
  module_order = [
194
- "review-data.js", # Data models and state (no deps)
195
- "review-position.js", # Position resolution (depends on data)
196
- "review-line-numbers.js", # Line numbers for click-to-comment (depends on data)
197
- "review-comments.js", # Comments UI (depends on position, line-numbers)
198
- "review-status.js", # Status UI (depends on data)
199
- "review-packages.js", # Package management (depends on data)
200
- "review-sync.js", # Sync operations (depends on data)
201
- "review-help.js", # Help system (depends on data)
202
- "review-resize.js", # Panel resize (depends on DOM)
203
- "review-init.js", # Init orchestration (must be last)
195
+ "review-data.js", # Data models and state (no deps)
196
+ "review-position.js", # Position resolution (depends on data)
197
+ "review-line-numbers.js", # Line numbers for click-to-comment (depends on data)
198
+ "review-comments.js", # Comments UI (depends on position, line-numbers)
199
+ "review-status.js", # Status UI (depends on data)
200
+ "review-packages.js", # Package management (depends on data)
201
+ "review-sync.js", # Sync operations (depends on data)
202
+ "review-help.js", # Help system (depends on data)
203
+ "review-resize.js", # Panel resize (depends on DOM)
204
+ "review-init.js", # Init orchestration (must be last)
204
205
  ]
205
206
 
206
207
  js_parts = []
@@ -213,10 +214,7 @@ class HTMLGenerator:
213
214
  return "\n".join(js_parts)
214
215
 
215
216
  def _build_render_context(
216
- self,
217
- embed_content: bool = False,
218
- edit_mode: bool = False,
219
- review_mode: bool = False
217
+ self, embed_content: bool = False, edit_mode: bool = False, review_mode: bool = False
220
218
  ) -> dict:
221
219
  """Build the template render context.
222
220
 
@@ -236,7 +234,11 @@ class HTMLGenerator:
236
234
  # Collect topics
237
235
  all_topics = set()
238
236
  for req in self.requirements.values():
239
- topic = req.file_path.stem.split('-', 1)[1] if '-' in req.file_path.stem else req.file_path.stem
237
+ topic = (
238
+ req.file_path.stem.split("-", 1)[1]
239
+ if "-" in req.file_path.stem
240
+ else req.file_path.stem
241
+ )
240
242
  all_topics.add(topic)
241
243
  sorted_topics = sorted(all_topics)
242
244
 
@@ -250,54 +252,37 @@ class HTMLGenerator:
250
252
 
251
253
  return {
252
254
  # Configuration flags
253
- 'embed_content': embed_content,
254
- 'edit_mode': edit_mode,
255
- 'review_mode': review_mode,
256
- 'version': self.VERSION,
257
-
255
+ "embed_content": embed_content,
256
+ "edit_mode": edit_mode,
257
+ "review_mode": review_mode,
258
+ "version": self.VERSION,
258
259
  # Statistics
259
- 'stats': {
260
- 'prd': {
261
- 'active': by_level['active']['PRD'],
262
- 'all': by_level['all']['PRD']
263
- },
264
- 'ops': {
265
- 'active': by_level['active']['OPS'],
266
- 'all': by_level['all']['OPS']
267
- },
268
- 'dev': {
269
- 'active': by_level['active']['DEV'],
270
- 'all': by_level['all']['DEV']
271
- },
272
- 'impl_files': self._count_impl_files()
260
+ "stats": {
261
+ "prd": {"active": by_level["active"]["PRD"], "all": by_level["all"]["PRD"]},
262
+ "ops": {"active": by_level["active"]["OPS"], "all": by_level["all"]["OPS"]},
263
+ "dev": {"active": by_level["active"]["DEV"], "all": by_level["all"]["DEV"]},
264
+ "impl_files": self._count_impl_files(),
273
265
  },
274
-
275
266
  # Repo prefix stats (for associated repos like CAL, TTN, etc.)
276
- 'repo_stats': by_repo,
277
-
267
+ "repo_stats": by_repo,
278
268
  # Requirements data
279
- 'topics': sorted_topics,
280
- 'requirements_html': requirements_html,
281
- 'req_json_data': req_json_data,
282
-
269
+ "topics": sorted_topics,
270
+ "requirements_html": requirements_html,
271
+ "req_json_data": req_json_data,
283
272
  # Asset content (CSS/JS loaded from external files)
284
- 'css': self._load_css(),
285
- 'js': self._load_js(),
286
-
273
+ "css": self._load_css(),
274
+ "js": self._load_js(),
287
275
  # Review mode assets (loaded conditionally)
288
- 'review_css': self._load_review_css() if review_mode else '',
289
- 'review_js': self._load_review_js() if review_mode else '',
290
- 'review_json_data': self._generate_review_json_data() if review_mode else '',
291
-
276
+ "review_css": self._load_review_css() if review_mode else "",
277
+ "review_js": self._load_review_js() if review_mode else "",
278
+ "review_json_data": self._generate_review_json_data() if review_mode else "",
292
279
  # Metadata
293
- 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M'),
294
- 'repo_root': str(self.repo_root) if self.repo_root else '',
280
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M"),
281
+ "repo_root": str(self.repo_root) if self.repo_root else "",
295
282
  }
296
283
 
297
284
  def _generate_requirements_html(
298
- self,
299
- embed_content: bool = False,
300
- edit_mode: bool = False
285
+ self, embed_content: bool = False, edit_mode: bool = False
301
286
  ) -> str:
302
287
  """Generate the HTML for all requirements.
303
288
 
@@ -319,13 +304,11 @@ class HTMLGenerator:
319
304
  for item_data in flat_list:
320
305
  html_parts.append(
321
306
  self._format_item_flat_html(
322
- item_data,
323
- embed_content=embed_content,
324
- edit_mode=edit_mode
307
+ item_data, embed_content=embed_content, edit_mode=edit_mode
325
308
  )
326
309
  )
327
310
 
328
- return '\n'.join(html_parts)
311
+ return "\n".join(html_parts)
329
312
 
330
313
  def _generate_legend_html(self) -> str:
331
314
  """Generate HTML legend section"""
@@ -339,9 +322,11 @@ class HTMLGenerator:
339
322
  <li style="margin: 4px 0;">✅ Active requirement</li>
340
323
  <li style="margin: 4px 0;">🚧 Draft requirement</li>
341
324
  <li style="margin: 4px 0;">⚠️ Deprecated requirement</li>
342
- <li style="margin: 4px 0;"><span style="color: #28a745; font-weight: bold;">+</span> NEW (in untracked file)</li>
343
- <li style="margin: 4px 0;"><span style="color: #fd7e14; font-weight: bold;">*</span> MODIFIED (content changed)</li>
344
- <li style="margin: 4px 0;">🗺️ Roadmap (spec/roadmap/) - hidden by default</li>
325
+ <li style="margin: 4px 0;"><span style="color: #28a745; \
326
+ font-weight: bold;">+</span> NEW (in untracked file)</li>
327
+ <li style="margin: 4px 0;"><span style="color: #fd7e14; \
328
+ font-weight: bold;">*</span> MODIFIED (content changed)</li>
329
+ <li style="margin: 4px 0;">🗺️ Roadmap - hidden by default</li>
345
330
  </ul>
346
331
  </div>
347
332
  <div>
@@ -380,31 +365,31 @@ class HTMLGenerator:
380
365
  file_path_url = f"file://{req.external_spec_path}"
381
366
  else:
382
367
  # Core repo: use relative path
383
- spec_subpath = 'spec/roadmap' if req.is_roadmap else 'spec'
368
+ spec_subpath = "spec/roadmap" if req.is_roadmap else "spec"
384
369
  file_path_url = f"{self._base_path}{spec_subpath}/{req.file_path.name}"
385
370
 
386
371
  req_data[req_id] = {
387
- 'title': req.title,
388
- 'status': req.status,
389
- 'level': req.level,
390
- 'body': req.body.strip(),
391
- 'rationale': req.rationale.strip(),
392
- 'file': req.display_filename, # Shows CAL/filename.md for external repos
393
- 'filePath': file_path_url,
394
- 'line': req.line_number,
395
- 'implements': list(req.implements) if req.implements else [],
396
- 'isRoadmap': req.is_roadmap,
397
- 'isConflict': req.is_conflict,
398
- 'conflictWith': req.conflict_with if req.is_conflict else None,
399
- 'isCycle': req.is_cycle,
400
- 'cyclePath': req.cycle_path if req.is_cycle else None,
401
- 'isExternal': req.external_spec_path is not None,
402
- 'repoPrefix': req.repo_prefix # e.g., 'CAL' for associated repos
372
+ "title": req.title,
373
+ "status": req.status,
374
+ "level": req.level,
375
+ "body": req.body.strip(),
376
+ "rationale": req.rationale.strip(),
377
+ "file": req.display_filename, # Shows CAL/filename.md for external repos
378
+ "filePath": file_path_url,
379
+ "line": req.line_number,
380
+ "implements": list(req.implements) if req.implements else [],
381
+ "isRoadmap": req.is_roadmap,
382
+ "isConflict": req.is_conflict,
383
+ "conflictWith": req.conflict_with if req.is_conflict else None,
384
+ "isCycle": req.is_cycle,
385
+ "cyclePath": req.cycle_path if req.is_cycle else None,
386
+ "isExternal": req.external_spec_path is not None,
387
+ "repoPrefix": req.repo_prefix, # e.g., 'CAL' for associated repos
403
388
  }
404
389
  json_str = json.dumps(req_data, indent=2)
405
390
  # Escape </script> to prevent premature closing of the script tag
406
391
  # This is safe because JSON strings already escape the backslash
407
- json_str = json_str.replace('</script>', '<\\/script>')
392
+ json_str = json_str.replace("</script>", "<\\/script>")
408
393
  return json_str
409
394
 
410
395
  def _generate_review_json_data(self) -> str:
@@ -414,61 +399,65 @@ class HTMLGenerator:
414
399
  in the HTML for immediate display. The API is still used for updates.
415
400
  """
416
401
  review_data = {
417
- 'threads': {},
418
- 'flags': {},
419
- 'requests': {},
420
- 'config': {
421
- 'approvalRules': {
422
- 'Draft->Active': ['product_owner', 'tech_lead'],
423
- 'Active->Deprecated': ['product_owner'],
424
- 'Draft->Deprecated': ['product_owner']
402
+ "threads": {},
403
+ "flags": {},
404
+ "requests": {},
405
+ "config": {
406
+ "approvalRules": {
407
+ "Draft->Active": ["product_owner", "tech_lead"],
408
+ "Active->Deprecated": ["product_owner"],
409
+ "Draft->Deprecated": ["product_owner"],
425
410
  },
426
- 'pushOnComment': True,
427
- 'autoFetchOnOpen': True
428
- }
411
+ "pushOnComment": True,
412
+ "autoFetchOnOpen": True,
413
+ },
429
414
  }
430
415
 
431
416
  # Load existing review data from .reviews/ directory
432
417
  if self.repo_root:
433
- reviews_dir = self.repo_root / '.reviews' / 'reqs'
418
+ reviews_dir = self.repo_root / ".reviews" / "reqs"
434
419
  if reviews_dir.exists():
435
420
  for req_dir in reviews_dir.iterdir():
436
421
  if req_dir.is_dir():
437
422
  req_id = req_dir.name
438
423
  # Load threads
439
- threads_file = req_dir / 'threads.json'
424
+ threads_file = req_dir / "threads.json"
440
425
  if threads_file.exists():
441
426
  try:
442
- with open(threads_file, 'r') as f:
427
+ with open(threads_file) as f:
443
428
  threads_data = json.load(f)
444
- if 'threads' in threads_data:
445
- review_data['threads'][req_id] = threads_data['threads']
446
- except (json.JSONDecodeError, IOError) as e:
447
- print(f"Warning: Could not load {threads_file}: {e}", file=sys.stderr)
429
+ if "threads" in threads_data:
430
+ review_data["threads"][req_id] = threads_data["threads"]
431
+ except (OSError, json.JSONDecodeError) as e:
432
+ print(
433
+ f"Warning: Could not load {threads_file}: {e}", file=sys.stderr
434
+ )
448
435
 
449
436
  # Load flags
450
- flag_file = req_dir / 'flag.json'
437
+ flag_file = req_dir / "flag.json"
451
438
  if flag_file.exists():
452
439
  try:
453
- with open(flag_file, 'r') as f:
440
+ with open(flag_file) as f:
454
441
  flag_data = json.load(f)
455
- review_data['flags'][req_id] = flag_data
456
- except (json.JSONDecodeError, IOError) as e:
442
+ review_data["flags"][req_id] = flag_data
443
+ except (OSError, json.JSONDecodeError) as e:
457
444
  print(f"Warning: Could not load {flag_file}: {e}", file=sys.stderr)
458
445
 
459
446
  # Load status requests
460
- status_file = req_dir / 'status.json'
447
+ status_file = req_dir / "status.json"
461
448
  if status_file.exists():
462
449
  try:
463
- with open(status_file, 'r') as f:
450
+ with open(status_file) as f:
464
451
  status_data = json.load(f)
465
- if 'requests' in status_data:
466
- review_data['requests'][req_id] = status_data['requests']
467
- except (json.JSONDecodeError, IOError) as e:
468
- print(f"Warning: Could not load {status_file}: {e}", file=sys.stderr)
452
+ if "requests" in status_data:
453
+ review_data["requests"][req_id] = status_data["requests"]
454
+ except (OSError, json.JSONDecodeError) as e:
455
+ print(
456
+ f"Warning: Could not load {status_file}: {e}", file=sys.stderr
457
+ )
469
458
 
470
459
  json_str = json.dumps(review_data, indent=2)
471
- json_str = json_str.replace('</script>', '<\\/script>')
460
+ json_str = json_str.replace("</script>", "<\\/script>")
472
461
  return json_str
473
462
 
474
463
  def _build_flat_requirement_list(self) -> List[dict]:
@@ -483,7 +472,9 @@ class HTMLGenerator:
483
472
  root_reqs.sort(key=lambda r: r.id)
484
473
 
485
474
  for root_req in root_reqs:
486
- self._add_requirement_and_children(root_req, flat_list, indent=0, parent_instance_id='', ancestor_path=[])
475
+ self._add_requirement_and_children(
476
+ root_req, flat_list, indent=0, parent_instance_id="", ancestor_path=[]
477
+ )
487
478
 
488
479
  # Add any orphaned requirements that weren't included in the tree
489
480
  # (requirements that have implements pointing to non-existent parents)
@@ -495,11 +486,26 @@ class HTMLGenerator:
495
486
  orphaned_reqs = [self.requirements[rid] for rid in orphaned_ids]
496
487
  orphaned_reqs.sort(key=lambda r: r.id)
497
488
  for orphan in orphaned_reqs:
498
- self._add_requirement_and_children(orphan, flat_list, indent=0, parent_instance_id='', ancestor_path=[], is_orphan=True)
489
+ self._add_requirement_and_children(
490
+ orphan,
491
+ flat_list,
492
+ indent=0,
493
+ parent_instance_id="",
494
+ ancestor_path=[],
495
+ is_orphan=True,
496
+ )
499
497
 
500
498
  return flat_list
501
499
 
502
- def _add_requirement_and_children(self, req: Requirement, flat_list: List[dict], indent: int, parent_instance_id: str, ancestor_path: list[str], is_orphan: bool = False):
500
+ def _add_requirement_and_children(
501
+ self,
502
+ req: Requirement,
503
+ flat_list: List[dict],
504
+ indent: int,
505
+ parent_instance_id: str,
506
+ ancestor_path: list[str],
507
+ is_orphan: bool = False,
508
+ ):
503
509
  """Recursively add requirement and its children to flat list
504
510
 
505
511
  Args:
@@ -525,45 +531,50 @@ class HTMLGenerator:
525
531
  self._instance_counter += 1
526
532
 
527
533
  # Find child requirements
528
- children = [
529
- r for r in self.requirements.values()
530
- if req.id in r.implements
531
- ]
534
+ children = [r for r in self.requirements.values() if req.id in r.implements]
532
535
  children.sort(key=lambda r: r.id)
533
536
 
534
537
  # Check if this requirement has children (either child reqs or implementation files)
535
538
  has_children = len(children) > 0 or len(req.implementation_files) > 0
536
539
 
537
540
  # Add this requirement
538
- flat_list.append({
539
- 'req': req,
540
- 'indent': indent,
541
- 'instance_id': instance_id,
542
- 'parent_instance_id': parent_instance_id,
543
- 'has_children': has_children,
544
- 'item_type': 'requirement'
545
- })
541
+ flat_list.append(
542
+ {
543
+ "req": req,
544
+ "indent": indent,
545
+ "instance_id": instance_id,
546
+ "parent_instance_id": parent_instance_id,
547
+ "has_children": has_children,
548
+ "item_type": "requirement",
549
+ }
550
+ )
546
551
 
547
552
  # Add implementation files as child items
548
553
  for file_path, line_num in req.implementation_files:
549
554
  impl_instance_id = f"inst_{self._instance_counter}"
550
555
  self._instance_counter += 1
551
- flat_list.append({
552
- 'file_path': file_path,
553
- 'line_num': line_num,
554
- 'indent': indent + 1,
555
- 'instance_id': impl_instance_id,
556
- 'parent_instance_id': instance_id,
557
- 'has_children': False,
558
- 'item_type': 'implementation'
559
- })
556
+ flat_list.append(
557
+ {
558
+ "file_path": file_path,
559
+ "line_num": line_num,
560
+ "indent": indent + 1,
561
+ "instance_id": impl_instance_id,
562
+ "parent_instance_id": instance_id,
563
+ "has_children": False,
564
+ "item_type": "implementation",
565
+ }
566
+ )
560
567
 
561
568
  # Recursively add child requirements (with updated ancestor path for cycle detection)
562
569
  current_path = ancestor_path + [req.id]
563
570
  for child in children:
564
- self._add_requirement_and_children(child, flat_list, indent + 1, instance_id, current_path)
571
+ self._add_requirement_and_children(
572
+ child, flat_list, indent + 1, instance_id, current_path
573
+ )
565
574
 
566
- def _format_item_flat_html(self, item_data: dict, embed_content: bool = False, edit_mode: bool = False) -> str:
575
+ def _format_item_flat_html(
576
+ self, item_data: dict, embed_content: bool = False, edit_mode: bool = False
577
+ ) -> str:
567
578
  """Format a single item (requirement or implementation file) as flat HTML row
568
579
 
569
580
  Args:
@@ -571,25 +582,30 @@ class HTMLGenerator:
571
582
  embed_content: If True, use onclick handlers instead of href links for portability
572
583
  edit_mode: If True, include edit mode UI elements
573
584
  """
574
- item_type = item_data.get('item_type', 'requirement')
585
+ item_type = item_data.get("item_type", "requirement")
575
586
 
576
- if item_type == 'implementation':
587
+ if item_type == "implementation":
577
588
  return self._format_impl_file_html(item_data, embed_content, edit_mode)
578
589
  else:
579
590
  return self._format_req_html(item_data, embed_content, edit_mode)
580
591
 
581
- def _format_impl_file_html(self, item_data: dict, embed_content: bool = False, edit_mode: bool = False) -> str:
592
+ def _format_impl_file_html(
593
+ self, item_data: dict, embed_content: bool = False, edit_mode: bool = False
594
+ ) -> str:
582
595
  """Format an implementation file as a child row"""
583
- file_path = item_data['file_path']
584
- line_num = item_data['line_num']
585
- indent = item_data['indent']
586
- instance_id = item_data['instance_id']
587
- parent_instance_id = item_data['parent_instance_id']
596
+ file_path = item_data["file_path"]
597
+ line_num = item_data["line_num"]
598
+ indent = item_data["indent"]
599
+ instance_id = item_data["instance_id"]
600
+ parent_instance_id = item_data["parent_instance_id"]
588
601
 
589
602
  # Create link or onclick handler
590
603
  if embed_content:
591
604
  file_url = f"{self._base_path}{file_path}"
592
- file_link = f'<a href="#" onclick="openCodeViewer(\'{file_url}\', {line_num}); return false;" style="color: #0066cc;">{file_path}:{line_num}</a>'
605
+ file_link = (
606
+ f'<a href="#" onclick="openCodeViewer(\'{file_url}\', {line_num}); '
607
+ f'return false;" style="color: #0066cc;">{file_path}:{line_num}</a>'
608
+ )
593
609
  else:
594
610
  link = f"{self._base_path}{file_path}#L{line_num}"
595
611
  file_link = f'<a href="{link}" style="color: #0066cc;">{file_path}:{line_num}</a>'
@@ -599,19 +615,21 @@ class HTMLGenerator:
599
615
  abs_file_path = self.repo_root / file_path
600
616
  vscode_url = f"vscode://file/{abs_file_path}:{line_num}"
601
617
  vscode_link = f'<a href="{vscode_url}" title="Open in VS Code" class="vscode-link">🔧</a>'
602
- file_link = f'{file_link}{vscode_link}'
618
+ file_link = f"{file_link}{vscode_link}"
603
619
 
604
620
  # Edit mode destination column (only if edit mode enabled)
605
- edit_column = '<div class="req-destination edit-mode-column"></div>' if edit_mode else ''
621
+ edit_column = '<div class="req-destination edit-mode-column"></div>' if edit_mode else ""
606
622
 
607
623
  # Build HTML for implementation file row
608
624
  html = f"""
609
- <div class="req-item impl-file" data-instance-id="{instance_id}" data-indent="{indent}" data-parent-instance-id="{parent_instance_id}">
625
+ <div class="req-item impl-file" data-instance-id="{instance_id}" \
626
+ data-indent="{indent}" data-parent-instance-id="{parent_instance_id}">
610
627
  <div class="req-header-container">
611
628
  <span class="collapse-icon"></span>
612
629
  <div class="req-content">
613
630
  <div class="req-id" style="color: #6c757d;">📄</div>
614
- <div class="req-header" style="font-family: 'Consolas', 'Monaco', monospace; font-size: 12px;">{file_link}</div>
631
+ <div class="req-header" style="font-family: 'Consolas', 'Monaco', \
632
+ monospace; font-size: 12px;">{file_link}</div>
615
633
  <div class="req-level"></div>
616
634
  <div class="req-badges"></div>
617
635
  <div class="req-coverage"></div>
@@ -624,7 +642,9 @@ class HTMLGenerator:
624
642
  """
625
643
  return html
626
644
 
627
- def _format_req_html(self, req_data: dict, embed_content: bool = False, edit_mode: bool = False) -> str:
645
+ def _format_req_html(
646
+ self, req_data: dict, embed_content: bool = False, edit_mode: bool = False
647
+ ) -> str:
628
648
  """Format a single requirement as flat HTML row
629
649
 
630
650
  Args:
@@ -632,47 +652,60 @@ class HTMLGenerator:
632
652
  embed_content: If True, use onclick handlers instead of href links for portability
633
653
  edit_mode: If True, include edit mode UI elements
634
654
  """
635
- req = req_data['req']
636
- indent = req_data['indent']
637
- instance_id = req_data['instance_id']
638
- parent_instance_id = req_data['parent_instance_id']
639
- has_children = req_data['has_children']
655
+ req = req_data["req"]
656
+ indent = req_data["indent"]
657
+ instance_id = req_data["instance_id"]
658
+ parent_instance_id = req_data["parent_instance_id"]
659
+ has_children = req_data["has_children"]
640
660
 
641
661
  status_class = req.status.lower()
642
662
  level_class = req.level.lower()
643
663
 
644
664
  # Only show collapse icon if there are children
645
- collapse_icon = '' if has_children else ''
665
+ collapse_icon = "" if has_children else ""
646
666
 
647
667
  # Determine implementation coverage status
648
668
  impl_status = self._get_implementation_status(req.id)
649
- if impl_status == 'Full':
650
- coverage_icon = '' # Filled circle
651
- coverage_title = 'Full implementation coverage'
652
- elif impl_status == 'Partial':
653
- coverage_icon = '' # Half-filled circle
654
- coverage_title = 'Partial implementation coverage'
669
+ if impl_status == "Full":
670
+ coverage_icon = "" # Filled circle
671
+ coverage_title = "Full implementation coverage"
672
+ elif impl_status == "Partial":
673
+ coverage_icon = "" # Half-filled circle
674
+ coverage_title = "Partial implementation coverage"
655
675
  else: # Unimplemented
656
- coverage_icon = '' # Empty circle
657
- coverage_title = 'Unimplemented'
676
+ coverage_icon = "" # Empty circle
677
+ coverage_title = "Unimplemented"
658
678
 
659
679
  # Determine test status
660
- test_badge = ''
680
+ test_badge = ""
661
681
  if req.test_info:
662
682
  test_status = req.test_info.test_status
663
683
  test_count = req.test_info.test_count + req.test_info.manual_test_count
664
684
 
665
- if test_status == 'passed':
666
- test_badge = f'<span class="test-badge test-passed" title="{test_count} tests passed">✅ {test_count}</span>'
667
- elif test_status == 'failed':
668
- test_badge = f'<span class="test-badge test-failed" title="{test_count} tests, some failed">❌ {test_count}</span>'
669
- elif test_status == 'not_tested':
670
- test_badge = '<span class="test-badge test-not-tested" title="No tests implemented">⚡</span>'
685
+ if test_status == "passed":
686
+ test_badge = (
687
+ f'<span class="test-badge test-passed" '
688
+ f'title="{test_count} tests passed">✅ {test_count}</span>'
689
+ )
690
+ elif test_status == "failed":
691
+ test_badge = (
692
+ f'<span class="test-badge test-failed" '
693
+ f'title="{test_count} tests, some failed">❌ {test_count}</span>'
694
+ )
695
+ elif test_status == "not_tested":
696
+ test_badge = (
697
+ '<span class="test-badge test-not-tested" '
698
+ 'title="No tests implemented">⚡</span>'
699
+ )
671
700
  else:
672
- test_badge = '<span class="test-badge test-not-tested" title="No tests implemented">⚡</span>'
701
+ test_badge = (
702
+ '<span class="test-badge test-not-tested" ' 'title="No tests implemented">⚡</span>'
703
+ )
673
704
 
674
705
  # Extract topic from filename
675
- topic = req.file_path.stem.split('-', 1)[1] if '-' in req.file_path.stem else req.file_path.stem
706
+ topic = (
707
+ req.file_path.stem.split("-", 1)[1] if "-" in req.file_path.stem else req.file_path.stem
708
+ )
676
709
 
677
710
  # Create link to source file with REQ anchor
678
711
  # In embedded mode, use onclick to open side panel instead of navigating away
@@ -681,11 +714,11 @@ class HTMLGenerator:
681
714
  # Determine the correct spec path (spec/ or spec/roadmap/, or file:// for external)
682
715
  if req.external_spec_path:
683
716
  # External repo: use file:// URL
684
- spec_url = f'file://{req.external_spec_path}'
717
+ spec_url = f"file://{req.external_spec_path}"
685
718
  else:
686
719
  # Core repo: use relative path
687
- spec_subpath = 'spec/roadmap' if req.is_roadmap else 'spec'
688
- spec_url = f'{self._base_path}{spec_subpath}/{req.file_path.name}'
720
+ spec_subpath = "spec/roadmap" if req.is_roadmap else "spec"
721
+ spec_url = f"{self._base_path}{spec_subpath}/{req.file_path.name}"
689
722
 
690
723
  # Display filename without .md extension but with repo prefix (e.g., CAL/dev-edc-schema)
691
724
  file_stem = req.file_path.stem # removes .md extension
@@ -693,21 +726,32 @@ class HTMLGenerator:
693
726
 
694
727
  # Display ID: strip __conflict suffix (warning icon shows conflict status separately)
695
728
  display_id = req.id
696
- if '__conflict' in req.id:
697
- display_id = req.id.replace('__conflict', '')
729
+ if "__conflict" in req.id:
730
+ display_id = req.id.replace("__conflict", "")
698
731
 
699
732
  if embed_content:
700
- req_link = f'<a href="#" onclick="event.stopPropagation(); openReqPanel(\'{req.id}\'); return false;" style="color: inherit; text-decoration: none; cursor: pointer;">{display_id}</a>'
733
+ req_link = (
734
+ f'<a href="#" onclick="event.stopPropagation(); '
735
+ f"openReqPanel('{req.id}'); return false;\" "
736
+ f'style="color: inherit; text-decoration: none; cursor: pointer;">'
737
+ f"{display_id}</a>"
738
+ )
701
739
  file_line_link = f'<span style="color: inherit;">{display_filename}</span>'
702
740
  else:
703
- req_link = f'<a href="{spec_url}#REQ-{req.id}" style="color: inherit; text-decoration: none;">{display_id}</a>'
704
- file_line_link = f'<a href="{spec_url}#L{req.line_number}" style="color: inherit; text-decoration: none;">{display_filename}</a>'
741
+ req_link = (
742
+ f'<a href="{spec_url}#REQ-{req.id}" '
743
+ f'style="color: inherit; text-decoration: none;">{display_id}</a>'
744
+ )
745
+ file_line_link = (
746
+ f'<a href="{spec_url}#L{req.line_number}" '
747
+ f'style="color: inherit; text-decoration: none;">{display_filename}</a>'
748
+ )
705
749
 
706
750
  # Determine status indicators using distinctive Unicode symbols
707
751
  # ★ (star) = NEW, ◆ (diamond) = MODIFIED, ↝ (wave arrow) = MOVED
708
- status_suffix = ''
709
- status_suffix_class = ''
710
- status_title = ''
752
+ status_suffix = ""
753
+ status_suffix_class = ""
754
+ status_title = ""
711
755
 
712
756
  is_moved = req.is_moved
713
757
  is_new_not_moved = req.is_new and not is_moved
@@ -715,60 +759,56 @@ class HTMLGenerator:
715
759
 
716
760
  if is_moved and is_modified:
717
761
  # Moved AND modified - show both indicators
718
- status_suffix = '↝◆'
719
- status_suffix_class = 'status-moved-modified'
720
- status_title = 'MOVED and MODIFIED'
762
+ status_suffix = "↝◆"
763
+ status_suffix_class = "status-moved-modified"
764
+ status_title = "MOVED and MODIFIED"
721
765
  elif is_moved:
722
766
  # Just moved (might be in new file)
723
- status_suffix = ''
724
- status_suffix_class = 'status-moved'
725
- status_title = 'MOVED from another file'
767
+ status_suffix = ""
768
+ status_suffix_class = "status-moved"
769
+ status_title = "MOVED from another file"
726
770
  elif is_new_not_moved:
727
771
  # Truly new (in new file, not moved)
728
- status_suffix = ''
729
- status_suffix_class = 'status-new'
730
- status_title = 'NEW requirement'
772
+ status_suffix = ""
773
+ status_suffix_class = "status-new"
774
+ status_title = "NEW requirement"
731
775
  elif is_modified:
732
776
  # Modified in place
733
- status_suffix = ''
734
- status_suffix_class = 'status-modified'
735
- status_title = 'MODIFIED content'
736
-
737
- # VS Code link for use in side panel (not in topic column)
738
- if req.external_spec_path:
739
- # External repo: use the absolute path directly
740
- abs_spec_path = req.external_spec_path
741
- else:
742
- # Core repo: construct from repo_root
743
- spec_subpath_for_vscode = 'spec/roadmap' if req.is_roadmap else 'spec'
744
- abs_spec_path = self.repo_root / spec_subpath_for_vscode / req.file_path.name
745
- vscode_url = f"vscode://file/{abs_spec_path}:{req.line_number}"
777
+ status_suffix = ""
778
+ status_suffix_class = "status-modified"
779
+ status_title = "MODIFIED content"
746
780
 
747
781
  # Check if this is a root requirement (no parents)
748
782
  is_root = not req.implements or len(req.implements) == 0
749
783
  is_root_attr = 'data-is-root="true"' if is_root else 'data-is-root="false"'
750
784
  # Two separate modified attributes: uncommitted (since last commit) and branch (vs main)
751
- uncommitted_attr = 'data-uncommitted="true"' if req.is_uncommitted else 'data-uncommitted="false"'
752
- branch_attr = 'data-branch-changed="true"' if req.is_branch_changed else 'data-branch-changed="false"'
785
+ uncommitted_attr = (
786
+ 'data-uncommitted="true"' if req.is_uncommitted else 'data-uncommitted="false"'
787
+ )
788
+ branch_attr = (
789
+ 'data-branch-changed="true"' if req.is_branch_changed else 'data-branch-changed="false"'
790
+ )
753
791
 
754
792
  # Data attribute for has-children (for leaf-only filtering)
755
- has_children_attr = 'data-has-children="true"' if has_children else 'data-has-children="false"'
793
+ has_children_attr = (
794
+ 'data-has-children="true"' if has_children else 'data-has-children="false"'
795
+ )
756
796
 
757
797
  # Data attribute for test status (for test filter)
758
- test_status_value = 'not-tested'
798
+ test_status_value = "not-tested"
759
799
  if req.test_info:
760
- if req.test_info.test_status == 'passed':
761
- test_status_value = 'tested'
762
- elif req.test_info.test_status == 'failed':
763
- test_status_value = 'failed'
800
+ if req.test_info.test_status == "passed":
801
+ test_status_value = "tested"
802
+ elif req.test_info.test_status == "failed":
803
+ test_status_value = "failed"
764
804
  test_status_attr = f'data-test-status="{test_status_value}"'
765
805
 
766
806
  # Data attribute for coverage (for coverage filter)
767
- coverage_value = 'none'
768
- if impl_status == 'Full':
769
- coverage_value = 'full'
770
- elif impl_status == 'Partial':
771
- coverage_value = 'partial'
807
+ coverage_value = "none"
808
+ if impl_status == "Full":
809
+ coverage_value = "full"
810
+ elif impl_status == "Partial":
811
+ coverage_value = "partial"
772
812
  coverage_attr = f'data-coverage="{coverage_value}"'
773
813
 
774
814
  # Data attribute for roadmap (for roadmap filtering)
@@ -776,39 +816,106 @@ class HTMLGenerator:
776
816
 
777
817
  # Edit mode buttons - only generated if edit_mode is enabled
778
818
  if edit_mode:
819
+ req_id = req.id
820
+ file_name = req.file_path.name
779
821
  if req.is_roadmap:
780
- edit_buttons = f'''<span class="edit-actions" onclick="event.stopPropagation();">
781
- <button class="edit-btn from-roadmap" onclick="addPendingMove('{req.id}', '{req.file_path.name}', 'from-roadmap')" title="Move out of roadmap">↩ From Roadmap</button>
782
- <button class="edit-btn move-file" onclick="showMoveToFile('{req.id}', '{req.file_path.name}')" title="Move to different file">📁 Move</button>
783
- </span>'''
822
+ from_roadmap_btn = (
823
+ f'<button class="edit-btn from-roadmap" '
824
+ f"onclick=\"addPendingMove('{req_id}', '{file_name}', 'from-roadmap')\" "
825
+ f'title="Move out of roadmap">↩ From Roadmap</button>'
826
+ )
827
+ move_file_btn = (
828
+ f'<button class="edit-btn move-file" '
829
+ f"onclick=\"showMoveToFile('{req_id}', '{file_name}')\" "
830
+ f'title="Move to different file">📁 Move</button>'
831
+ )
832
+ edit_buttons = (
833
+ f'<span class="edit-actions" onclick="event.stopPropagation();">'
834
+ f"{from_roadmap_btn}{move_file_btn}</span>"
835
+ )
784
836
  else:
785
- edit_buttons = f'''<span class="edit-actions" onclick="event.stopPropagation();">
786
- <button class="edit-btn to-roadmap" onclick="addPendingMove('{req.id}', '{req.file_path.name}', 'to-roadmap')" title="Move to roadmap">🗺️ To Roadmap</button>
787
- <button class="edit-btn move-file" onclick="showMoveToFile('{req.id}', '{req.file_path.name}')" title="Move to different file">📁 Move</button>
788
- </span>'''
837
+ to_roadmap_btn = (
838
+ f'<button class="edit-btn to-roadmap" '
839
+ f"onclick=\"addPendingMove('{req_id}', '{file_name}', 'to-roadmap')\" "
840
+ f'title="Move to roadmap">🗺️ To Roadmap</button>'
841
+ )
842
+ move_file_btn = (
843
+ f'<button class="edit-btn move-file" '
844
+ f"onclick=\"showMoveToFile('{req_id}', '{file_name}')\" "
845
+ f'title="Move to different file">📁 Move</button>'
846
+ )
847
+ edit_buttons = (
848
+ f'<span class="edit-actions" onclick="event.stopPropagation();">'
849
+ f"{to_roadmap_btn}{move_file_btn}</span>"
850
+ )
789
851
  else:
790
- edit_buttons = ''
852
+ edit_buttons = ""
791
853
 
792
854
  # Roadmap indicator icon (shown after REQ ID)
793
- roadmap_icon = '<span class="roadmap-icon" title="In roadmap">🛤️</span>' if req.is_roadmap else ''
855
+ roadmap_icon = (
856
+ '<span class="roadmap-icon" title="In roadmap">🛤️</span>' if req.is_roadmap else ""
857
+ )
794
858
 
795
859
  # Conflict indicator icon (shown for roadmap REQs that conflict with existing REQs)
796
- conflict_icon = f'<span class="conflict-icon" title="Conflicts with REQ-{req.conflict_with}">⚠️</span>' if req.is_conflict else ''
797
- conflict_attr = f'data-conflict="true" data-conflict-with="{req.conflict_with}"' if req.is_conflict else 'data-conflict="false"'
860
+ conflict_icon = (
861
+ f'<span class="conflict-icon" title="Conflicts with REQ-{req.conflict_with}">⚠️</span>'
862
+ if req.is_conflict
863
+ else ""
864
+ )
865
+ conflict_attr = (
866
+ f'data-conflict="true" data-conflict-with="{req.conflict_with}"'
867
+ if req.is_conflict
868
+ else 'data-conflict="false"'
869
+ )
798
870
 
799
871
  # Cycle indicator icon (shown for REQs involved in dependency cycles)
800
- cycle_icon = f'<span class="cycle-icon" title="Cycle: {req.cycle_path}">🔄</span>' if req.is_cycle else ''
801
- cycle_attr = f'data-cycle="true" data-cycle-path="{req.cycle_path}"' if req.is_cycle else 'data-cycle="false"'
872
+ cycle_icon = (
873
+ f'<span class="cycle-icon" title="Cycle: {req.cycle_path}">🔄</span>'
874
+ if req.is_cycle
875
+ else ""
876
+ )
877
+ cycle_attr = (
878
+ f'data-cycle="true" data-cycle-path="{req.cycle_path}"'
879
+ if req.is_cycle
880
+ else 'data-cycle="false"'
881
+ )
802
882
 
803
883
  # Determine item class based on status
804
- item_class = 'conflict-item' if req.is_conflict else ('cycle-item' if req.is_cycle else '')
884
+ item_class = "conflict-item" if req.is_conflict else ("cycle-item" if req.is_cycle else "")
805
885
 
806
886
  # Repo prefix for filtering (CORE for core repo, CAL/TTN/etc. for associated repos)
807
- repo_prefix = req.repo_prefix or 'CORE'
887
+ repo_prefix = req.repo_prefix or "CORE"
888
+
889
+ # Build data attributes for the div
890
+ deprecated_class = status_class if req.status == "Deprecated" else ""
891
+ data_attrs = (
892
+ f'data-req-id="{req.id}" data-instance-id="{instance_id}" '
893
+ f'data-level="{req.level}" data-indent="{indent}" '
894
+ f'data-parent-instance-id="{parent_instance_id}" data-topic="{topic}" '
895
+ f'data-status="{req.status}" data-title="{req.title.lower()}" '
896
+ f'data-file="{req.file_path.name}" data-repo="{repo_prefix}" '
897
+ f"{is_root_attr} {uncommitted_attr} {branch_attr} {has_children_attr} "
898
+ f"{test_status_attr} {coverage_attr} {roadmap_attr} {conflict_attr} {cycle_attr}"
899
+ )
900
+
901
+ # Build status badges HTML
902
+ status_badges_html = (
903
+ f'<span class="status-badge status-{status_class}">{req.status}</span>'
904
+ f'<span class="status-suffix {status_suffix_class}" '
905
+ f'title="{status_title}">{status_suffix}</span>'
906
+ )
907
+
908
+ # Build edit mode column if enabled
909
+ edit_column_html = ""
910
+ if edit_mode:
911
+ edit_column_html = (
912
+ f'<div class="req-destination edit-mode-column" data-req-id="{req.id}">'
913
+ f'{edit_buttons}<span class="dest-text"></span></div>'
914
+ )
808
915
 
809
916
  # Build HTML for single flat row with unique instance ID
810
917
  html = f"""
811
- <div class="req-item {level_class} {status_class if req.status == 'Deprecated' else ''} {item_class}" data-req-id="{req.id}" data-instance-id="{instance_id}" data-level="{req.level}" data-indent="{indent}" data-parent-instance-id="{parent_instance_id}" data-topic="{topic}" data-status="{req.status}" data-title="{req.title.lower()}" data-file="{req.file_path.name}" data-repo="{repo_prefix}" {is_root_attr} {uncommitted_attr} {branch_attr} {has_children_attr} {test_status_attr} {coverage_attr} {roadmap_attr} {conflict_attr} {cycle_attr}>
918
+ <div class="req-item {level_class} {deprecated_class} {item_class}" {data_attrs}>
812
919
  <div class="req-header-container" onclick="toggleRequirement(this)">
813
920
  <span class="collapse-icon">{collapse_icon}</span>
814
921
  <div class="req-content">
@@ -816,24 +923,27 @@ class HTMLGenerator:
816
923
  <div class="req-header">{req.title}</div>
817
924
  <div class="req-level">{req.level}</div>
818
925
  <div class="req-badges">
819
- <span class="status-badge status-{status_class}">{req.status}</span><span class="status-suffix {status_suffix_class}" title="{status_title}">{status_suffix}</span>
926
+ {status_badges_html}
820
927
  </div>
821
928
  <div class="req-coverage" title="{coverage_title}">{coverage_icon}</div>
822
929
  <div class="req-status">{test_badge}</div>
823
930
  <div class="req-location">{file_line_link}</div>
824
- {'<div class="req-destination edit-mode-column" data-req-id="' + req.id + '">' + edit_buttons + '<span class="dest-text"></span></div>' if edit_mode else ''}
931
+ {edit_column_html}
825
932
  </div>
826
933
  </div>
827
934
  </div>
828
935
  """
829
936
  return html
830
937
 
831
- def _format_req_tree_html(self, req: Requirement, ancestor_path: list[str] | None = None) -> str:
938
+ def _format_req_tree_html(
939
+ self, req: Requirement, ancestor_path: list[str] | None = None
940
+ ) -> str:
832
941
  """Format requirement and children as HTML tree (legacy non-collapsible).
833
942
 
834
943
  Args:
835
944
  req: The requirement to format
836
- ancestor_path: List of requirement IDs in the current traversal path (for cycle detection)
945
+ ancestor_path: List of requirement IDs in the current traversal path
946
+ (for cycle detection)
837
947
 
838
948
  Returns:
839
949
  Formatted HTML string
@@ -846,13 +956,20 @@ class HTMLGenerator:
846
956
  cycle_path = ancestor_path + [req.id]
847
957
  cycle_str = " -> ".join([f"REQ-{rid}" for rid in cycle_path])
848
958
  print(f"⚠️ CYCLE DETECTED: {cycle_str}", file=sys.stderr)
849
- return f' <div class="req-item cycle-detected"><strong>⚠️ CYCLE DETECTED:</strong> REQ-{req.id} (path: {cycle_str})</div>\n'
959
+ return (
960
+ f' <div class="req-item cycle-detected">'
961
+ f"<strong>⚠️ CYCLE DETECTED:</strong> REQ-{req.id} "
962
+ f"(path: {cycle_str})</div>\n"
963
+ )
850
964
 
851
965
  # Safety depth limit
852
966
  MAX_DEPTH = 50
853
967
  if len(ancestor_path) > MAX_DEPTH:
854
968
  print(f"⚠️ MAX DEPTH ({MAX_DEPTH}) exceeded at REQ-{req.id}", file=sys.stderr)
855
- return f' <div class="req-item depth-exceeded"><strong>⚠️ MAX DEPTH EXCEEDED:</strong> REQ-{req.id}</div>\n'
969
+ return (
970
+ f' <div class="req-item depth-exceeded">'
971
+ f"<strong>⚠️ MAX DEPTH EXCEEDED:</strong> REQ-{req.id}</div>\n"
972
+ )
856
973
 
857
974
  status_class = req.status.lower()
858
975
  level_class = req.level.lower()
@@ -870,10 +987,7 @@ class HTMLGenerator:
870
987
  """
871
988
 
872
989
  # Find children
873
- children = [
874
- r for r in self.requirements.values()
875
- if req.id in r.implements
876
- ]
990
+ children = [r for r in self.requirements.values() if req.id in r.implements]
877
991
  children.sort(key=lambda r: r.id)
878
992
 
879
993
  if children:
@@ -882,17 +996,20 @@ class HTMLGenerator:
882
996
  html += ' <div class="child-reqs">\n'
883
997
  for child in children:
884
998
  html += self._format_req_tree_html(child, current_path)
885
- html += ' </div>\n'
999
+ html += " </div>\n"
886
1000
 
887
- html += ' </div>\n'
1001
+ html += " </div>\n"
888
1002
  return html
889
1003
 
890
- def _format_req_tree_html_collapsible(self, req: Requirement, ancestor_path: list[str] | None = None) -> str:
1004
+ def _format_req_tree_html_collapsible(
1005
+ self, req: Requirement, ancestor_path: list[str] | None = None
1006
+ ) -> str:
891
1007
  """Format requirement and children as collapsible HTML tree.
892
1008
 
893
1009
  Args:
894
1010
  req: The requirement to format
895
- ancestor_path: List of requirement IDs in the current traversal path (for cycle detection)
1011
+ ancestor_path: List of requirement IDs in the current traversal path
1012
+ (for cycle detection)
896
1013
 
897
1014
  Returns:
898
1015
  Formatted HTML string
@@ -905,7 +1022,7 @@ class HTMLGenerator:
905
1022
  cycle_path = ancestor_path + [req.id]
906
1023
  cycle_str = " -> ".join([f"REQ-{rid}" for rid in cycle_path])
907
1024
  print(f"⚠️ CYCLE DETECTED: {cycle_str}", file=sys.stderr)
908
- return f'''
1025
+ return f"""
909
1026
  <div class="req-item cycle-detected" data-req-id="{req.id}">
910
1027
  <div class="req-header-container">
911
1028
  <span class="collapse-icon"></span>
@@ -920,13 +1037,13 @@ class HTMLGenerator:
920
1037
  </div>
921
1038
  </div>
922
1039
  </div>
923
- '''
1040
+ """
924
1041
 
925
1042
  # Safety depth limit
926
1043
  MAX_DEPTH = 50
927
1044
  if len(ancestor_path) > MAX_DEPTH:
928
1045
  print(f"⚠️ MAX DEPTH ({MAX_DEPTH}) exceeded at REQ-{req.id}", file=sys.stderr)
929
- return f'''
1046
+ return f"""
930
1047
  <div class="req-item depth-exceeded" data-req-id="{req.id}">
931
1048
  <div class="req-header-container">
932
1049
  <span class="collapse-icon"></span>
@@ -940,44 +1057,62 @@ class HTMLGenerator:
940
1057
  </div>
941
1058
  </div>
942
1059
  </div>
943
- '''
1060
+ """
944
1061
 
945
1062
  status_class = req.status.lower()
946
1063
  level_class = req.level.lower()
947
1064
 
948
1065
  # Find children
949
- children = [
950
- r for r in self.requirements.values()
951
- if req.id in r.implements
952
- ]
1066
+ children = [r for r in self.requirements.values() if req.id in r.implements]
953
1067
  children.sort(key=lambda r: r.id)
954
1068
 
955
1069
  # Only show collapse icon if there are children
956
- collapse_icon = '' if children else ''
1070
+ collapse_icon = "" if children else ""
957
1071
 
958
1072
  # Determine test status
959
- test_badge = ''
1073
+ test_badge = ""
960
1074
  if req.test_info:
961
1075
  test_status = req.test_info.test_status
962
1076
  test_count = req.test_info.test_count + req.test_info.manual_test_count
963
1077
 
964
- if test_status == 'passed':
965
- test_badge = f'<span class="test-badge test-passed" title="{test_count} tests passed">✅ {test_count}</span>'
966
- elif test_status == 'failed':
967
- test_badge = f'<span class="test-badge test-failed" title="{test_count} tests, some failed">❌ {test_count}</span>'
968
- elif test_status == 'not_tested':
969
- test_badge = '<span class="test-badge test-not-tested" title="No tests implemented">⚡</span>'
1078
+ if test_status == "passed":
1079
+ test_badge = (
1080
+ f'<span class="test-badge test-passed" '
1081
+ f'title="{test_count} tests passed">✅ {test_count}</span>'
1082
+ )
1083
+ elif test_status == "failed":
1084
+ test_badge = (
1085
+ f'<span class="test-badge test-failed" '
1086
+ f'title="{test_count} tests, some failed">❌ {test_count}</span>'
1087
+ )
1088
+ elif test_status == "not_tested":
1089
+ test_badge = (
1090
+ '<span class="test-badge test-not-tested" '
1091
+ 'title="No tests implemented">⚡</span>'
1092
+ )
970
1093
  else:
971
- test_badge = '<span class="test-badge test-not-tested" title="No tests implemented">⚡</span>'
1094
+ test_badge = (
1095
+ '<span class="test-badge test-not-tested" ' 'title="No tests implemented">⚡</span>'
1096
+ )
972
1097
 
973
1098
  # Extract topic from filename (e.g., prd-security.md -> security)
974
- topic = req.file_path.stem.split('-', 1)[1] if '-' in req.file_path.stem else req.file_path.stem
1099
+ topic = (
1100
+ req.file_path.stem.split("-", 1)[1] if "-" in req.file_path.stem else req.file_path.stem
1101
+ )
975
1102
 
976
1103
  # Repo prefix for filtering (CORE for core repo, CAL/TTN/etc. for associated repos)
977
- repo_prefix = req.repo_prefix or 'CORE'
1104
+ repo_prefix = req.repo_prefix or "CORE"
1105
+
1106
+ # Build data attributes
1107
+ deprecated_class = status_class if req.status == "Deprecated" else ""
1108
+ data_attrs = (
1109
+ f'data-req-id="{req.id}" data-level="{req.level}" data-topic="{topic}" '
1110
+ f'data-status="{req.status}" data-title="{req.title.lower()}" '
1111
+ f'data-repo="{repo_prefix}"'
1112
+ )
978
1113
 
979
1114
  html = f"""
980
- <div class="req-item {level_class} {status_class if req.status == 'Deprecated' else ''}" data-req-id="{req.id}" data-level="{req.level}" data-topic="{topic}" data-status="{req.status}" data-title="{req.title.lower()}" data-repo="{repo_prefix}">
1115
+ <div class="req-item {level_class} {deprecated_class}" {data_attrs}>
981
1116
  <div class="req-header-container" onclick="toggleRequirement(this)">
982
1117
  <span class="collapse-icon">{collapse_icon}</span>
983
1118
  <div class="req-content">
@@ -999,8 +1134,7 @@ class HTMLGenerator:
999
1134
  html += ' <div class="child-reqs">\n'
1000
1135
  for child in children:
1001
1136
  html += self._format_req_tree_html_collapsible(child, current_path)
1002
- html += ' </div>\n'
1137
+ html += " </div>\n"
1003
1138
 
1004
- html += ' </div>\n'
1139
+ html += " </div>\n"
1005
1140
  return html
1006
-