elspais 0.11.2__py3-none-any.whl → 0.43.5__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 (147) hide show
  1. elspais/__init__.py +1 -10
  2. elspais/{sponsors/__init__.py → associates.py} +102 -56
  3. elspais/cli.py +366 -69
  4. elspais/commands/__init__.py +9 -3
  5. elspais/commands/analyze.py +118 -169
  6. elspais/commands/changed.py +12 -23
  7. elspais/commands/config_cmd.py +10 -13
  8. elspais/commands/edit.py +33 -13
  9. elspais/commands/example_cmd.py +319 -0
  10. elspais/commands/hash_cmd.py +161 -183
  11. elspais/commands/health.py +1177 -0
  12. elspais/commands/index.py +98 -115
  13. elspais/commands/init.py +99 -22
  14. elspais/commands/reformat_cmd.py +41 -433
  15. elspais/commands/rules_cmd.py +2 -2
  16. elspais/commands/trace.py +443 -324
  17. elspais/commands/validate.py +193 -411
  18. elspais/config/__init__.py +799 -5
  19. elspais/{core/content_rules.py → content_rules.py} +20 -2
  20. elspais/docs/cli/assertions.md +67 -0
  21. elspais/docs/cli/commands.md +304 -0
  22. elspais/docs/cli/config.md +262 -0
  23. elspais/docs/cli/format.md +66 -0
  24. elspais/docs/cli/git.md +45 -0
  25. elspais/docs/cli/health.md +190 -0
  26. elspais/docs/cli/hierarchy.md +60 -0
  27. elspais/docs/cli/ignore.md +72 -0
  28. elspais/docs/cli/mcp.md +245 -0
  29. elspais/docs/cli/quickstart.md +58 -0
  30. elspais/docs/cli/traceability.md +89 -0
  31. elspais/docs/cli/validation.md +96 -0
  32. elspais/graph/GraphNode.py +383 -0
  33. elspais/graph/__init__.py +40 -0
  34. elspais/graph/annotators.py +927 -0
  35. elspais/graph/builder.py +1886 -0
  36. elspais/graph/deserializer.py +248 -0
  37. elspais/graph/factory.py +284 -0
  38. elspais/graph/metrics.py +127 -0
  39. elspais/graph/mutations.py +161 -0
  40. elspais/graph/parsers/__init__.py +156 -0
  41. elspais/graph/parsers/code.py +213 -0
  42. elspais/graph/parsers/comments.py +112 -0
  43. elspais/graph/parsers/config_helpers.py +29 -0
  44. elspais/graph/parsers/heredocs.py +225 -0
  45. elspais/graph/parsers/journey.py +131 -0
  46. elspais/graph/parsers/remainder.py +79 -0
  47. elspais/graph/parsers/requirement.py +347 -0
  48. elspais/graph/parsers/results/__init__.py +6 -0
  49. elspais/graph/parsers/results/junit_xml.py +229 -0
  50. elspais/graph/parsers/results/pytest_json.py +313 -0
  51. elspais/graph/parsers/test.py +305 -0
  52. elspais/graph/relations.py +78 -0
  53. elspais/graph/serialize.py +216 -0
  54. elspais/html/__init__.py +8 -0
  55. elspais/html/generator.py +731 -0
  56. elspais/html/templates/trace_view.html.j2 +2151 -0
  57. elspais/mcp/__init__.py +45 -29
  58. elspais/mcp/__main__.py +5 -1
  59. elspais/mcp/file_mutations.py +138 -0
  60. elspais/mcp/server.py +1998 -244
  61. elspais/testing/__init__.py +3 -3
  62. elspais/testing/config.py +3 -0
  63. elspais/testing/mapper.py +1 -1
  64. elspais/testing/scanner.py +301 -12
  65. elspais/utilities/__init__.py +1 -0
  66. elspais/utilities/docs_loader.py +115 -0
  67. elspais/utilities/git.py +607 -0
  68. elspais/{core → utilities}/hasher.py +8 -22
  69. elspais/utilities/md_renderer.py +189 -0
  70. elspais/{core → utilities}/patterns.py +56 -51
  71. elspais/utilities/reference_config.py +626 -0
  72. elspais/validation/__init__.py +19 -0
  73. elspais/validation/format.py +264 -0
  74. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/METADATA +7 -4
  75. elspais-0.43.5.dist-info/RECORD +80 -0
  76. elspais/config/defaults.py +0 -179
  77. elspais/config/loader.py +0 -494
  78. elspais/core/__init__.py +0 -21
  79. elspais/core/git.py +0 -346
  80. elspais/core/models.py +0 -320
  81. elspais/core/parser.py +0 -639
  82. elspais/core/rules.py +0 -509
  83. elspais/mcp/context.py +0 -172
  84. elspais/mcp/serializers.py +0 -112
  85. elspais/reformat/__init__.py +0 -50
  86. elspais/reformat/detector.py +0 -112
  87. elspais/reformat/hierarchy.py +0 -247
  88. elspais/reformat/line_breaks.py +0 -218
  89. elspais/reformat/prompts.py +0 -133
  90. elspais/reformat/transformer.py +0 -266
  91. elspais/trace_view/__init__.py +0 -55
  92. elspais/trace_view/coverage.py +0 -183
  93. elspais/trace_view/generators/__init__.py +0 -12
  94. elspais/trace_view/generators/base.py +0 -334
  95. elspais/trace_view/generators/csv.py +0 -118
  96. elspais/trace_view/generators/markdown.py +0 -170
  97. elspais/trace_view/html/__init__.py +0 -33
  98. elspais/trace_view/html/generator.py +0 -1140
  99. elspais/trace_view/html/templates/base.html +0 -283
  100. elspais/trace_view/html/templates/components/code_viewer_modal.html +0 -14
  101. elspais/trace_view/html/templates/components/file_picker_modal.html +0 -20
  102. elspais/trace_view/html/templates/components/legend_modal.html +0 -69
  103. elspais/trace_view/html/templates/components/review_panel.html +0 -118
  104. elspais/trace_view/html/templates/partials/review/help/help-panel.json +0 -244
  105. elspais/trace_view/html/templates/partials/review/help/onboarding.json +0 -77
  106. elspais/trace_view/html/templates/partials/review/help/tooltips.json +0 -237
  107. elspais/trace_view/html/templates/partials/review/review-comments.js +0 -928
  108. elspais/trace_view/html/templates/partials/review/review-data.js +0 -961
  109. elspais/trace_view/html/templates/partials/review/review-help.js +0 -679
  110. elspais/trace_view/html/templates/partials/review/review-init.js +0 -177
  111. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +0 -429
  112. elspais/trace_view/html/templates/partials/review/review-packages.js +0 -1029
  113. elspais/trace_view/html/templates/partials/review/review-position.js +0 -540
  114. elspais/trace_view/html/templates/partials/review/review-resize.js +0 -115
  115. elspais/trace_view/html/templates/partials/review/review-status.js +0 -659
  116. elspais/trace_view/html/templates/partials/review/review-sync.js +0 -992
  117. elspais/trace_view/html/templates/partials/review-styles.css +0 -2238
  118. elspais/trace_view/html/templates/partials/scripts.js +0 -1741
  119. elspais/trace_view/html/templates/partials/styles.css +0 -1756
  120. elspais/trace_view/models.py +0 -378
  121. elspais/trace_view/review/__init__.py +0 -63
  122. elspais/trace_view/review/branches.py +0 -1142
  123. elspais/trace_view/review/models.py +0 -1200
  124. elspais/trace_view/review/position.py +0 -591
  125. elspais/trace_view/review/server.py +0 -1032
  126. elspais/trace_view/review/status.py +0 -455
  127. elspais/trace_view/review/storage.py +0 -1343
  128. elspais/trace_view/scanning.py +0 -213
  129. elspais/trace_view/specs/README.md +0 -84
  130. elspais/trace_view/specs/tv-d00001-template-architecture.md +0 -36
  131. elspais/trace_view/specs/tv-d00002-css-extraction.md +0 -37
  132. elspais/trace_view/specs/tv-d00003-js-extraction.md +0 -43
  133. elspais/trace_view/specs/tv-d00004-build-embedding.md +0 -40
  134. elspais/trace_view/specs/tv-d00005-test-format.md +0 -78
  135. elspais/trace_view/specs/tv-d00010-review-data-models.md +0 -33
  136. elspais/trace_view/specs/tv-d00011-review-storage.md +0 -33
  137. elspais/trace_view/specs/tv-d00012-position-resolution.md +0 -33
  138. elspais/trace_view/specs/tv-d00013-git-branches.md +0 -31
  139. elspais/trace_view/specs/tv-d00014-review-api-server.md +0 -31
  140. elspais/trace_view/specs/tv-d00015-status-modifier.md +0 -27
  141. elspais/trace_view/specs/tv-d00016-js-integration.md +0 -33
  142. elspais/trace_view/specs/tv-p00001-html-generator.md +0 -33
  143. elspais/trace_view/specs/tv-p00002-review-system.md +0 -29
  144. elspais-0.11.2.dist-info/RECORD +0 -101
  145. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/WHEEL +0 -0
  146. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/entry_points.txt +0 -0
  147. {elspais-0.11.2.dist-info → elspais-0.43.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,2151 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Requirements Traceability</title>
7
+ <style>
8
+ /* ═══════════════════════════════════════════════════════════════════════════
9
+ CSS STYLES - Trace View
10
+ ═══════════════════════════════════════════════════════════════════════════ */
11
+
12
+ * {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ body {
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
20
+ font-size: 14px;
21
+ line-height: 1.5;
22
+ color: #333;
23
+ background: #f8f9fa;
24
+ }
25
+
26
+ /* ─────────────────────────────────────────────────────────────────────────────
27
+ Header
28
+ ───────────────────────────────────────────────────────────────────────────── */
29
+
30
+ .header {
31
+ background: white;
32
+ border-bottom: 1px solid #e9ecef;
33
+ padding: 16px 24px;
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ flex-wrap: wrap;
38
+ gap: 16px;
39
+ }
40
+
41
+ .header-title {
42
+ font-size: 1.5rem;
43
+ font-weight: 600;
44
+ color: #212529;
45
+ }
46
+
47
+ .header-stats {
48
+ display: flex;
49
+ gap: 8px;
50
+ align-items: center;
51
+ flex-wrap: wrap;
52
+ }
53
+
54
+ .stat-badge {
55
+ padding: 4px 12px;
56
+ border-radius: 4px;
57
+ font-size: 0.875rem;
58
+ font-weight: 600;
59
+ cursor: pointer;
60
+ transition: opacity 0.15s, transform 0.1s;
61
+ user-select: none;
62
+ }
63
+
64
+ .stat-badge:hover {
65
+ opacity: 0.85;
66
+ transform: scale(1.02);
67
+ }
68
+
69
+ .stat-badge.active {
70
+ /* When filter is active (hiding that level), show hollow/outline appearance */
71
+ box-shadow: inset 0 0 0 2px currentColor;
72
+ background: transparent !important;
73
+ }
74
+
75
+ /* Level badges need dark text when active (background becomes transparent) */
76
+ .stat-badge.prd.active { color: #212529; }
77
+ .stat-badge.ops.active { color: #fd7e14; }
78
+ .stat-badge.dev.active { color: #20c997; }
79
+ .stat-badge.core.active { color: #0d6efd; }
80
+
81
+ .stat-badge.prd { background: #212529; color: white; }
82
+ .stat-badge.ops { background: #fd7e14; color: white; }
83
+ .stat-badge.dev { background: #20c997; color: white; }
84
+ .stat-badge.core { background: #0d6efd; color: white; }
85
+ .stat-badge.code {
86
+ background: #6c757d;
87
+ color: white;
88
+ }
89
+ .stat-badge.code.active {
90
+ color: #6c757d;
91
+ }
92
+ .stat-badge.tests {
93
+ background: #198754;
94
+ color: white;
95
+ }
96
+ .stat-badge.tests.active {
97
+ color: #198754;
98
+ }
99
+ .stat-badge.results {
100
+ background: #6610f2;
101
+ color: white;
102
+ }
103
+ .stat-badge.results.active {
104
+ color: #6610f2;
105
+ }
106
+ .stat-badge.associated {
107
+ background: #6f42c1;
108
+ color: white;
109
+ }
110
+ .stat-badge.associated.active {
111
+ color: #6f42c1;
112
+ }
113
+
114
+ .assertion-summary {
115
+ font-size: 0.85em;
116
+ color: #6c757d;
117
+ padding: 4px 0;
118
+ border-top: 1px solid #dee2e6;
119
+ margin-top: 8px;
120
+ }
121
+ .assertion-summary strong {
122
+ color: #212529;
123
+ }
124
+
125
+ .header-meta {
126
+ display: flex;
127
+ gap: 16px;
128
+ align-items: center;
129
+ }
130
+
131
+ .version-badge {
132
+ padding: 4px 8px;
133
+ background: #e9ecef;
134
+ border-radius: 4px;
135
+ font-size: 0.75rem;
136
+ color: #6c757d;
137
+ }
138
+
139
+ .legend-btn {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 4px;
143
+ padding: 6px 12px;
144
+ background: #6c757d;
145
+ color: white;
146
+ border: none;
147
+ border-radius: 4px;
148
+ cursor: pointer;
149
+ font-size: 0.875rem;
150
+ }
151
+
152
+ .legend-btn:hover {
153
+ background: #5a6268;
154
+ }
155
+
156
+ /* ─────────────────────────────────────────────────────────────────────────────
157
+ Toolbar
158
+ ───────────────────────────────────────────────────────────────────────────── */
159
+
160
+ .toolbar {
161
+ background: white;
162
+ border-bottom: 1px solid #e9ecef;
163
+ padding: 12px 24px;
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ gap: 12px;
167
+ align-items: center;
168
+ }
169
+
170
+ .btn-group {
171
+ display: flex;
172
+ border: 1px solid #dee2e6;
173
+ border-radius: 4px;
174
+ overflow: hidden;
175
+ }
176
+
177
+ .btn-group .btn {
178
+ border: none;
179
+ border-right: 1px solid #dee2e6;
180
+ background: white;
181
+ padding: 8px 16px;
182
+ cursor: pointer;
183
+ font-size: 0.875rem;
184
+ color: #495057;
185
+ transition: background 0.15s;
186
+ }
187
+
188
+ .btn-group .btn:last-child {
189
+ border-right: none;
190
+ }
191
+
192
+ .btn-group .btn:hover {
193
+ background: #f8f9fa;
194
+ }
195
+
196
+ .btn-group .btn.active {
197
+ background: #0d6efd;
198
+ color: white;
199
+ }
200
+
201
+ .toggle-group {
202
+ display: flex;
203
+ gap: 16px;
204
+ align-items: center;
205
+ }
206
+
207
+ .toggle-item {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 6px;
211
+ font-size: 0.875rem;
212
+ color: #495057;
213
+ cursor: pointer;
214
+ }
215
+
216
+ .toggle-item input[type="checkbox"] {
217
+ width: 16px;
218
+ height: 16px;
219
+ cursor: pointer;
220
+ }
221
+
222
+ .toolbar-actions {
223
+ display: flex;
224
+ gap: 8px;
225
+ margin-left: auto;
226
+ align-items: center;
227
+ }
228
+
229
+ .btn {
230
+ padding: 8px 16px;
231
+ border: 1px solid #dee2e6;
232
+ background: white;
233
+ border-radius: 4px;
234
+ cursor: pointer;
235
+ font-size: 0.875rem;
236
+ color: #495057;
237
+ transition: background 0.15s;
238
+ }
239
+
240
+ .btn:hover {
241
+ background: #f8f9fa;
242
+ }
243
+
244
+ .btn-icon {
245
+ padding: 8px 12px;
246
+ }
247
+
248
+ .counter {
249
+ font-size: 0.875rem;
250
+ color: #6c757d;
251
+ }
252
+
253
+ /* ─────────────────────────────────────────────────────────────────────────────
254
+ Tabs
255
+ ───────────────────────────────────────────────────────────────────────────── */
256
+
257
+ .tabs {
258
+ background: white;
259
+ border-bottom: 1px solid #e9ecef;
260
+ padding: 0 24px;
261
+ display: flex;
262
+ gap: 0;
263
+ }
264
+
265
+ .tab {
266
+ padding: 12px 24px;
267
+ border: none;
268
+ background: none;
269
+ cursor: pointer;
270
+ font-size: 0.875rem;
271
+ color: #6c757d;
272
+ border-bottom: 2px solid transparent;
273
+ transition: all 0.15s;
274
+ }
275
+
276
+ .tab:hover {
277
+ color: #495057;
278
+ }
279
+
280
+ .tab.active {
281
+ color: #0d6efd;
282
+ border-bottom-color: #0d6efd;
283
+ }
284
+
285
+ /* Tab Content */
286
+ .tab-content {
287
+ display: none;
288
+ }
289
+
290
+ .tab-content.active {
291
+ display: block;
292
+ }
293
+
294
+ /* Journey Cards */
295
+ .journey-toolbar {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 16px;
299
+ margin-bottom: 16px;
300
+ padding: 12px 16px;
301
+ background: #f8f9fa;
302
+ border-radius: 8px;
303
+ }
304
+
305
+ .journey-search {
306
+ flex: 1;
307
+ max-width: 400px;
308
+ }
309
+
310
+ .journey-search input {
311
+ width: 100%;
312
+ padding: 8px 12px;
313
+ border: 1px solid #dee2e6;
314
+ border-radius: 6px;
315
+ font-size: 0.875rem;
316
+ }
317
+
318
+ .journey-search input:focus {
319
+ outline: none;
320
+ border-color: #4299e1;
321
+ box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15);
322
+ }
323
+
324
+ .journey-count {
325
+ font-size: 0.875rem;
326
+ color: #6c757d;
327
+ }
328
+
329
+ .journey-list {
330
+ display: grid;
331
+ gap: 16px;
332
+ padding: 16px 0;
333
+ }
334
+
335
+ .journey-card {
336
+ background: white;
337
+ border: 1px solid #e9ecef;
338
+ border-radius: 8px;
339
+ padding: 20px;
340
+ transition: box-shadow 0.2s, border-color 0.2s;
341
+ }
342
+
343
+ .journey-card:hover {
344
+ border-color: #4299e1;
345
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
346
+ }
347
+
348
+ .journey-card.hidden {
349
+ display: none;
350
+ }
351
+
352
+ .journey-card-header {
353
+ display: flex;
354
+ align-items: flex-start;
355
+ gap: 12px;
356
+ margin-bottom: 12px;
357
+ }
358
+
359
+ .journey-card .journey-id {
360
+ font-family: 'SF Mono', Monaco, monospace;
361
+ font-size: 0.75rem;
362
+ color: white;
363
+ background: #6c757d;
364
+ padding: 4px 8px;
365
+ border-radius: 4px;
366
+ flex-shrink: 0;
367
+ }
368
+
369
+ .journey-card h3 {
370
+ font-size: 1.125rem;
371
+ font-weight: 600;
372
+ color: #212529;
373
+ margin: 0;
374
+ line-height: 1.4;
375
+ }
376
+
377
+ .journey-meta {
378
+ display: grid;
379
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
380
+ gap: 12px;
381
+ margin-bottom: 12px;
382
+ padding: 12px;
383
+ background: #f8f9fa;
384
+ border-radius: 6px;
385
+ }
386
+
387
+ .journey-meta-item {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 2px;
391
+ }
392
+
393
+ .journey-meta-label {
394
+ font-size: 0.7rem;
395
+ font-weight: 600;
396
+ text-transform: uppercase;
397
+ letter-spacing: 0.5px;
398
+ color: #6c757d;
399
+ }
400
+
401
+ .journey-meta-value {
402
+ font-size: 0.875rem;
403
+ color: #212529;
404
+ }
405
+
406
+ .journey-card .journey-description {
407
+ font-size: 0.875rem;
408
+ color: #495057;
409
+ line-height: 1.6;
410
+ white-space: pre-wrap;
411
+ }
412
+
413
+ .no-journeys {
414
+ text-align: center;
415
+ padding: 48px;
416
+ color: #6c757d;
417
+ }
418
+
419
+ /* Journey Grouping */
420
+ .journey-group-controls {
421
+ display: flex;
422
+ align-items: center;
423
+ gap: 8px;
424
+ }
425
+
426
+ .journey-group-controls label {
427
+ font-size: 0.8rem;
428
+ color: #6c757d;
429
+ font-weight: 500;
430
+ }
431
+
432
+ .journey-group-controls select {
433
+ padding: 6px 10px;
434
+ border: 1px solid #dee2e6;
435
+ border-radius: 6px;
436
+ font-size: 0.8rem;
437
+ background: white;
438
+ cursor: pointer;
439
+ }
440
+
441
+ .journey-group-controls select:focus {
442
+ outline: none;
443
+ border-color: #4299e1;
444
+ }
445
+
446
+ .journey-group {
447
+ margin-bottom: 24px;
448
+ }
449
+
450
+ .journey-group-header {
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 10px;
454
+ padding: 10px 14px;
455
+ background: linear-gradient(to right, #e9ecef, #f8f9fa);
456
+ border-radius: 8px;
457
+ margin-bottom: 12px;
458
+ cursor: pointer;
459
+ user-select: none;
460
+ transition: background 0.2s;
461
+ }
462
+
463
+ .journey-group-header:hover {
464
+ background: linear-gradient(to right, #dee2e6, #e9ecef);
465
+ }
466
+
467
+ .journey-group-header .expand-icon {
468
+ font-size: 0.75rem;
469
+ color: #6c757d;
470
+ transition: transform 0.2s;
471
+ width: 16px;
472
+ }
473
+
474
+ .journey-group.collapsed .expand-icon {
475
+ transform: rotate(-90deg);
476
+ }
477
+
478
+ .journey-group-header h4 {
479
+ margin: 0;
480
+ font-size: 0.9rem;
481
+ font-weight: 600;
482
+ color: #495057;
483
+ flex: 1;
484
+ }
485
+
486
+ .journey-group-count {
487
+ font-size: 0.75rem;
488
+ color: #6c757d;
489
+ background: white;
490
+ padding: 2px 8px;
491
+ border-radius: 10px;
492
+ border: 1px solid #dee2e6;
493
+ }
494
+
495
+ .journey-group-cards {
496
+ display: grid;
497
+ gap: 12px;
498
+ padding-left: 0;
499
+ }
500
+
501
+ .journey-group.collapsed .journey-group-cards {
502
+ display: none;
503
+ }
504
+
505
+ /* Compact card variant for grouped view */
506
+ .journey-card.compact {
507
+ padding: 14px 16px;
508
+ }
509
+
510
+ .journey-card.compact .journey-card-header {
511
+ margin-bottom: 8px;
512
+ }
513
+
514
+ .journey-card.compact h3 {
515
+ font-size: 1rem;
516
+ }
517
+
518
+ .journey-card.compact .journey-description {
519
+ font-size: 0.8rem;
520
+ max-height: 80px;
521
+ overflow: hidden;
522
+ position: relative;
523
+ }
524
+
525
+ .journey-card.compact .journey-description::after {
526
+ content: '';
527
+ position: absolute;
528
+ bottom: 0;
529
+ left: 0;
530
+ right: 0;
531
+ height: 30px;
532
+ background: linear-gradient(transparent, white);
533
+ }
534
+
535
+ .journey-card.compact.expanded .journey-description {
536
+ max-height: none;
537
+ }
538
+
539
+ .journey-card.compact.expanded .journey-description::after {
540
+ display: none;
541
+ }
542
+
543
+ /* ─────────────────────────────────────────────────────────────────────────────
544
+ Main Content
545
+ ───────────────────────────────────────────────────────────────────────────── */
546
+
547
+ .main-content {
548
+ padding: 24px;
549
+ }
550
+
551
+ .section-title {
552
+ font-size: 1rem;
553
+ font-weight: 600;
554
+ color: #495057;
555
+ margin-bottom: 16px;
556
+ }
557
+
558
+ .table-container {
559
+ background: white;
560
+ border-radius: 8px;
561
+ border: 1px solid #e9ecef;
562
+ overflow: hidden;
563
+ }
564
+
565
+ /* ─────────────────────────────────────────────────────────────────────────────
566
+ Traceability Table
567
+ ───────────────────────────────────────────────────────────────────────────── */
568
+
569
+ .trace-table {
570
+ width: 100%;
571
+ border-collapse: collapse;
572
+ }
573
+
574
+ .trace-table th {
575
+ background: #f8f9fa;
576
+ padding: 12px 16px;
577
+ text-align: left;
578
+ font-weight: 600;
579
+ color: #495057;
580
+ border-bottom: 1px solid #e9ecef;
581
+ position: sticky;
582
+ top: 0;
583
+ z-index: 10;
584
+ }
585
+
586
+ .trace-table td {
587
+ padding: 10px 16px;
588
+ border-bottom: 1px solid #f1f3f4;
589
+ vertical-align: middle;
590
+ }
591
+
592
+ .trace-table tr:hover {
593
+ background: #f8f9fa;
594
+ }
595
+
596
+ .trace-table tr.hidden {
597
+ display: none;
598
+ }
599
+
600
+ /* Column widths */
601
+ .col-id { width: 180px; }
602
+ .col-title { min-width: 200px; }
603
+ .col-level { width: 70px; }
604
+ .col-status { width: 100px; }
605
+ .col-cov { width: 60px; text-align: center; }
606
+ .col-topic { width: 180px; }
607
+
608
+ /* Filter inputs in header */
609
+ .filter-input {
610
+ width: 100%;
611
+ padding: 6px 8px;
612
+ border: 1px solid #dee2e6;
613
+ border-radius: 4px;
614
+ font-size: 0.75rem;
615
+ margin-top: 6px;
616
+ }
617
+
618
+ .filter-select {
619
+ width: 100%;
620
+ padding: 6px 8px;
621
+ border: 1px solid #dee2e6;
622
+ border-radius: 4px;
623
+ font-size: 0.75rem;
624
+ margin-top: 6px;
625
+ background: white;
626
+ }
627
+
628
+ /* ─────────────────────────────────────────────────────────────────────────────
629
+ Tree Structure
630
+ ───────────────────────────────────────────────────────────────────────────── */
631
+
632
+ .tree-cell {
633
+ display: flex;
634
+ align-items: center;
635
+ gap: 4px;
636
+ }
637
+
638
+ .tree-indent {
639
+ display: inline-block;
640
+ width: 20px;
641
+ flex-shrink: 0;
642
+ }
643
+
644
+ .tree-toggle {
645
+ width: 20px;
646
+ height: 20px;
647
+ display: flex;
648
+ align-items: center;
649
+ justify-content: center;
650
+ cursor: pointer;
651
+ color: #6c757d;
652
+ flex-shrink: 0;
653
+ border: none;
654
+ background: none;
655
+ font-size: 10px;
656
+ }
657
+
658
+ .tree-toggle:hover {
659
+ color: #495057;
660
+ }
661
+
662
+ .tree-toggle.collapsed::before {
663
+ content: "▶";
664
+ }
665
+
666
+ .tree-toggle.expanded::before {
667
+ content: "▼";
668
+ }
669
+
670
+ .tree-toggle.leaf {
671
+ visibility: hidden;
672
+ }
673
+
674
+ .req-id {
675
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
676
+ font-size: 0.875rem;
677
+ color: #0d6efd;
678
+ text-decoration: none;
679
+ }
680
+
681
+ .req-id:hover {
682
+ text-decoration: underline;
683
+ }
684
+
685
+ .file-link {
686
+ color: #198754;
687
+ text-decoration: none;
688
+ }
689
+
690
+ .file-link:hover {
691
+ text-decoration: underline;
692
+ }
693
+
694
+ .assertion-badges {
695
+ display: flex;
696
+ gap: 2px;
697
+ margin-left: 4px;
698
+ }
699
+
700
+ .assertion-badge {
701
+ display: inline-flex;
702
+ align-items: center;
703
+ justify-content: center;
704
+ width: 18px;
705
+ height: 18px;
706
+ background: #e7f1ff;
707
+ color: #0d6efd;
708
+ border-radius: 3px;
709
+ font-size: 0.7rem;
710
+ font-weight: 600;
711
+ }
712
+
713
+ /* ─────────────────────────────────────────────────────────────────────────────
714
+ Status Badges
715
+ ───────────────────────────────────────────────────────────────────────────── */
716
+
717
+ .status-badge {
718
+ display: inline-flex;
719
+ align-items: center;
720
+ gap: 4px;
721
+ padding: 2px 8px;
722
+ border-radius: 4px;
723
+ font-size: 0.75rem;
724
+ font-weight: 600;
725
+ text-transform: uppercase;
726
+ }
727
+
728
+ .status-badge.draft {
729
+ background: #cfe2ff;
730
+ color: #084298;
731
+ }
732
+
733
+ .status-badge.active {
734
+ background: #d1e7dd;
735
+ color: #0f5132;
736
+ }
737
+
738
+ .status-badge.deprecated {
739
+ background: #f8d7da;
740
+ color: #842029;
741
+ }
742
+
743
+ .status-badge.proposed {
744
+ background: #fff3cd;
745
+ color: #664d03;
746
+ }
747
+
748
+ .change-indicator {
749
+ color: #fd7e14;
750
+ font-size: 0.75rem;
751
+ }
752
+
753
+ /* ─────────────────────────────────────────────────────────────────────────────
754
+ Coverage Indicators
755
+ ───────────────────────────────────────────────────────────────────────────── */
756
+
757
+ .coverage-cell {
758
+ display: flex;
759
+ align-items: center;
760
+ justify-content: center;
761
+ gap: 4px;
762
+ }
763
+
764
+ .coverage-icon {
765
+ font-size: 1rem;
766
+ }
767
+
768
+ .coverage-icon.none { color: #adb5bd; }
769
+ .coverage-icon.partial { color: #6c757d; }
770
+ .coverage-icon.full { color: #212529; }
771
+
772
+ .warning-icon {
773
+ color: #ffc107;
774
+ font-size: 0.875rem;
775
+ }
776
+
777
+ /* ─────────────────────────────────────────────────────────────────────────────
778
+ Topic Tags
779
+ ───────────────────────────────────────────────────────────────────────────── */
780
+
781
+ .topic-tag {
782
+ font-size: 0.75rem;
783
+ color: #6c757d;
784
+ }
785
+
786
+ /* ─────────────────────────────────────────────────────────────────────────────
787
+ Code Node Rows
788
+ ───────────────────────────────────────────────────────────────────────────── */
789
+
790
+ .code-row {
791
+ background: #f8f9fa;
792
+ }
793
+
794
+ .code-row td {
795
+ font-size: 0.8rem;
796
+ color: #6c757d;
797
+ }
798
+
799
+ .code-icon {
800
+ color: #6c757d;
801
+ margin-right: 4px;
802
+ }
803
+
804
+ .test-row {
805
+ background: #f0fff4; /* Light green tint for tests */
806
+ }
807
+
808
+ .test-row td {
809
+ font-size: 0.8rem;
810
+ color: #198754;
811
+ }
812
+
813
+ .test-icon {
814
+ margin-right: 4px;
815
+ }
816
+
817
+ /* Test Result Rows */
818
+ .result-row {
819
+ background: #f8f9fa;
820
+ }
821
+
822
+ .result-row td {
823
+ font-size: 0.75rem;
824
+ color: #6c757d;
825
+ }
826
+
827
+ .result-row.passed {
828
+ background: #d1e7dd;
829
+ }
830
+
831
+ .result-row.passed td {
832
+ color: #0f5132;
833
+ }
834
+
835
+ .result-row.failed {
836
+ background: #f8d7da;
837
+ }
838
+
839
+ .result-row.failed td {
840
+ color: #842029;
841
+ }
842
+
843
+ .result-row.error {
844
+ background: #fff3cd;
845
+ }
846
+
847
+ .result-row.error td {
848
+ color: #664d03;
849
+ }
850
+
851
+ .result-row.skipped {
852
+ background: #e2e3e5;
853
+ }
854
+
855
+ .result-row.skipped td {
856
+ color: #41464b;
857
+ }
858
+
859
+ .result-icon {
860
+ margin-right: 4px;
861
+ }
862
+
863
+ .result-badge {
864
+ display: inline-flex;
865
+ align-items: center;
866
+ gap: 4px;
867
+ padding: 2px 6px;
868
+ border-radius: 3px;
869
+ font-size: 0.7rem;
870
+ font-weight: 600;
871
+ text-transform: uppercase;
872
+ }
873
+
874
+ .result-badge.passed {
875
+ background: #d1e7dd;
876
+ color: #0f5132;
877
+ }
878
+
879
+ .result-badge.failed {
880
+ background: #f8d7da;
881
+ color: #842029;
882
+ }
883
+
884
+ .result-badge.error {
885
+ background: #fff3cd;
886
+ color: #664d03;
887
+ }
888
+
889
+ .result-badge.skipped {
890
+ background: #e2e3e5;
891
+ color: #41464b;
892
+ }
893
+
894
+ /* ─────────────────────────────────────────────────────────────────────────────
895
+ Legend Modal
896
+ ───────────────────────────────────────────────────────────────────────────── */
897
+
898
+ .modal-overlay {
899
+ position: fixed;
900
+ top: 0;
901
+ left: 0;
902
+ right: 0;
903
+ bottom: 0;
904
+ background: rgba(0, 0, 0, 0.5);
905
+ display: flex;
906
+ align-items: center;
907
+ justify-content: center;
908
+ z-index: 1000;
909
+ opacity: 0;
910
+ visibility: hidden;
911
+ transition: opacity 0.2s, visibility 0.2s;
912
+ }
913
+
914
+ .modal-overlay.visible {
915
+ opacity: 1;
916
+ visibility: visible;
917
+ }
918
+
919
+ .modal {
920
+ background: white;
921
+ border-radius: 8px;
922
+ padding: 24px;
923
+ max-width: 500px;
924
+ width: 90%;
925
+ max-height: 80vh;
926
+ overflow-y: auto;
927
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
928
+ }
929
+
930
+ .modal-header {
931
+ display: flex;
932
+ justify-content: space-between;
933
+ align-items: center;
934
+ margin-bottom: 16px;
935
+ }
936
+
937
+ .modal-title {
938
+ font-size: 1.25rem;
939
+ font-weight: 600;
940
+ }
941
+
942
+ .modal-close {
943
+ background: none;
944
+ border: none;
945
+ font-size: 1.5rem;
946
+ cursor: pointer;
947
+ color: #6c757d;
948
+ padding: 0;
949
+ line-height: 1;
950
+ }
951
+
952
+ .modal-close:hover {
953
+ color: #333;
954
+ }
955
+
956
+ .legend-section {
957
+ margin-bottom: 20px;
958
+ }
959
+
960
+ .legend-section h4 {
961
+ font-size: 0.875rem;
962
+ font-weight: 600;
963
+ color: #495057;
964
+ margin-bottom: 8px;
965
+ border-bottom: 1px solid #e9ecef;
966
+ padding-bottom: 4px;
967
+ }
968
+
969
+ .legend-item {
970
+ display: flex;
971
+ align-items: center;
972
+ gap: 12px;
973
+ padding: 6px 0;
974
+ font-size: 0.875rem;
975
+ }
976
+
977
+ .legend-icon {
978
+ width: 24px;
979
+ text-align: center;
980
+ flex-shrink: 0;
981
+ }
982
+
983
+ /* ─────────────────────────────────────────────────────────────────────────────
984
+ Responsive
985
+ ───────────────────────────────────────────────────────────────────────────── */
986
+
987
+ @media (max-width: 768px) {
988
+ .header {
989
+ flex-direction: column;
990
+ align-items: flex-start;
991
+ }
992
+
993
+ .toolbar {
994
+ flex-direction: column;
995
+ align-items: flex-start;
996
+ }
997
+
998
+ .toolbar-actions {
999
+ margin-left: 0;
1000
+ width: 100%;
1001
+ justify-content: space-between;
1002
+ }
1003
+ }
1004
+
1005
+ </style>
1006
+ </head>
1007
+ <body>
1008
+
1009
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1010
+ HEADER
1011
+ ═══════════════════════════════════════════════════════════════════════════ -->
1012
+
1013
+ <header class="header">
1014
+ <h1 class="header-title">Requirements Traceability</h1>
1015
+
1016
+ <div class="header-stats">
1017
+ <span class="stat-badge prd" id="badge-prd" onclick="toggleLevelFilter('PRD')" title="Click to filter PRD only">PRD: {{ stats.prd_count }}</span>
1018
+ <span class="stat-badge ops" id="badge-ops" onclick="toggleLevelFilter('OPS')" title="Click to filter OPS only">OPS: {{ stats.ops_count }}</span>
1019
+ <span class="stat-badge dev" id="badge-dev" onclick="toggleLevelFilter('DEV')" title="Click to filter DEV only">DEV: {{ stats.dev_count }}</span>
1020
+ <span style="color: #adb5bd;">|</span>
1021
+ <span class="stat-badge core" id="badge-core" onclick="toggleFilter('core')" title="Click to hide core (non-associated) requirements">Core: {{ stats.total_count - stats.associated_count }}</span>
1022
+ <span style="color: #adb5bd;">|</span>
1023
+ <span class="stat-badge code" id="badge-code" onclick="toggleFilter('code')" title="Click to hide code nodes">Code: {{ stats.code_count }}</span>
1024
+ <span class="stat-badge tests" id="badge-tests" onclick="toggleFilter('tests')" title="Click to hide test nodes">Tests: {{ stats.test_count }}</span>
1025
+ {% if stats.test_result_count > 0 %}
1026
+ <span class="stat-badge results" id="badge-results" onclick="toggleFilter('results')" title="Click to show test results">Results: {{ stats.test_passed_count }}✓ {{ stats.test_failed_count }}✗</span>
1027
+ {% endif %}
1028
+ <span class="stat-badge associated" id="badge-associated" onclick="toggleFilter('associated')" title="Click to filter associated repos">Assoc: {{ stats.associated_count|default(0) }}</span>
1029
+ </div>
1030
+
1031
+ <div class="assertion-summary">
1032
+ Assertions: <strong>{{ stats.assertion_count }}</strong> unique |
1033
+ Implemented: <strong>{{ stats.assertions_implemented }}</strong> |
1034
+ Tested: <strong>{{ stats.assertions_tested }}</strong> |
1035
+ Validated: <strong>{{ stats.assertions_validated }}</strong>
1036
+ </div>
1037
+
1038
+ <div class="header-meta">
1039
+ <span class="version-badge">v{{ version }}</span>
1040
+ <button class="legend-btn" onclick="toggleLegend()">
1041
+ <span>ℹ</span> Legend
1042
+ </button>
1043
+ <button class="legend-btn" onclick="clearAllCookies()" title="Clear saved preferences">
1044
+ <span>🍪</span> Reset
1045
+ </button>
1046
+ </div>
1047
+ </header>
1048
+
1049
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1050
+ TOOLBAR
1051
+ ═══════════════════════════════════════════════════════════════════════════ -->
1052
+
1053
+ <div class="toolbar">
1054
+ <!-- View Mode -->
1055
+ <div class="btn-group" role="group">
1056
+ <button class="btn" data-view="flat" onclick="setViewMode('flat')">Flat View</button>
1057
+ <button class="btn active" data-view="hierarchical" onclick="setViewMode('hierarchical')">Hierarchical View</button>
1058
+ </div>
1059
+
1060
+ <!-- Git Filters -->
1061
+ <div class="btn-group" role="group">
1062
+ <button class="btn" id="btn-uncommitted" onclick="toggleFilter('uncommitted')">Uncommitted</button>
1063
+ <button class="btn" id="btn-changed" onclick="toggleFilter('changed')">Changed vs Main</button>
1064
+ </div>
1065
+
1066
+ <!-- Content Toggles -->
1067
+ <div class="toggle-group">
1068
+ <label class="toggle-item">
1069
+ <input type="checkbox" id="toggle-leaf" onchange="applyFilters()">
1070
+ <span>🌿 Leaf Only</span>
1071
+ </label>
1072
+ <label class="toggle-item">
1073
+ <input type="checkbox" id="toggle-deprecated" onchange="applyFilters()">
1074
+ <span>Include deprecated</span>
1075
+ </label>
1076
+ <label class="toggle-item">
1077
+ <input type="checkbox" id="toggle-roadmap" onchange="applyFilters()">
1078
+ <span>Include roadmap</span>
1079
+ </label>
1080
+ <label class="toggle-item">
1081
+ <input type="checkbox" id="toggle-code" onchange="applyFilters()">
1082
+ <span>Show code refs</span>
1083
+ </label>
1084
+ </div>
1085
+
1086
+ <!-- Actions -->
1087
+ <div class="toolbar-actions">
1088
+ <button class="btn btn-icon" onclick="expandAll()" title="Expand All">▼ Expand All</button>
1089
+ <button class="btn btn-icon" onclick="collapseAll()" title="Collapse All">▶ Collapse All</button>
1090
+ <button class="btn" onclick="clearFilters()">Clear</button>
1091
+ <span class="counter" id="counter">Showing {{ stats.total_count }} of {{ stats.total_count }} requirements</span>
1092
+ </div>
1093
+ </div>
1094
+
1095
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1096
+ TABS
1097
+ ═══════════════════════════════════════════════════════════════════════════ -->
1098
+
1099
+ <div class="tabs">
1100
+ <button class="tab active" data-tab="requirements" onclick="switchTab('requirements')">Requirements</button>
1101
+ <button class="tab" data-tab="journeys" onclick="switchTab('journeys')">User Journeys</button>
1102
+ </div>
1103
+
1104
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1105
+ MAIN CONTENT
1106
+ ═══════════════════════════════════════════════════════════════════════════ -->
1107
+
1108
+ <main class="main-content">
1109
+ <!-- Requirements Tab Content -->
1110
+ <div class="tab-content active" id="content-requirements">
1111
+ <h2 class="section-title">Traceability Tree - <span id="view-mode-label">Hierarchical View</span></h2>
1112
+
1113
+ <div class="table-container">
1114
+ <table class="trace-table" id="trace-table">
1115
+ <thead>
1116
+ <tr>
1117
+ <th class="col-id">
1118
+ ID
1119
+ <input type="text" class="filter-input" id="filter-id" placeholder="e.g. p00001" oninput="applyFilters()">
1120
+ </th>
1121
+ <th class="col-title">
1122
+ TITLE
1123
+ <input type="text" class="filter-input" id="filter-title" placeholder="Search title..." oninput="applyFilters()">
1124
+ </th>
1125
+ <th class="col-level">
1126
+ LEVEL
1127
+ <select class="filter-select" id="filter-level" onchange="applyFilters()">
1128
+ <option value="">All</option>
1129
+ <option value="PRD">PRD</option>
1130
+ <option value="OPS">OPS</option>
1131
+ <option value="DEV">DEV</option>
1132
+ </select>
1133
+ </th>
1134
+ <th class="col-status">
1135
+ STATUS
1136
+ <select class="filter-select" id="filter-status" onchange="applyFilters()">
1137
+ <option value="">All</option>
1138
+ {% for status in statuses %}
1139
+ <option value="{{ status }}">{{ status }}</option>
1140
+ {% endfor %}
1141
+ </select>
1142
+ </th>
1143
+ <th class="col-cov">
1144
+ COV
1145
+ <select class="filter-select" id="filter-cov" onchange="applyFilters()">
1146
+ <option value="">All</option>
1147
+ <option value="none">○ None</option>
1148
+ <option value="partial">◐ Partial</option>
1149
+ <option value="full">● Full</option>
1150
+ </select>
1151
+ </th>
1152
+ <th class="col-topic">
1153
+ TOPIC
1154
+ <select class="filter-select" id="filter-topic" onchange="applyFilters()">
1155
+ <option value="">All</option>
1156
+ {% for topic in topics %}
1157
+ <option value="{{ topic }}">{{ topic }}</option>
1158
+ {% endfor %}
1159
+ </select>
1160
+ </th>
1161
+ </tr>
1162
+ </thead>
1163
+ <tbody id="table-body">
1164
+ {% for row in rows %}
1165
+ <tr class="req-row {{ 'code-row' if row.is_code else '' }}{{ 'test-row' if row.is_test else '' }}{{ 'result-row ' + row.result_status if row.is_test_result else '' }}"
1166
+ data-id="{{ row.id }}"
1167
+ data-parent="{{ row.parent_id or '' }}"
1168
+ data-level="{{ row.level }}"
1169
+ data-status="{{ row.status }}"
1170
+ data-coverage="{{ row.coverage }}"
1171
+ data-topic="{{ row.topic }}"
1172
+ data-depth="{{ row.depth }}"
1173
+ data-is-leaf="{{ 'true' if row.is_leaf else 'false' }}"
1174
+ data-is-changed="{{ 'true' if row.is_changed else 'false' }}"
1175
+ data-is-uncommitted="{{ 'true' if row.is_uncommitted else 'false' }}"
1176
+ data-is-roadmap="{{ 'true' if row.is_roadmap else 'false' }}"
1177
+ data-is-code="{{ 'true' if row.is_code else 'false' }}"
1178
+ data-is-test="{{ 'true' if row.is_test else 'false' }}"
1179
+ data-is-test-result="{{ 'true' if row.is_test_result else 'false' }}"
1180
+ data-result-status="{{ row.result_status }}"
1181
+ data-is-associated="{{ 'true' if row.is_associated else 'false' }}"
1182
+ data-has-children="{{ 'true' if row.has_children else 'false' }}"
1183
+ data-source-file="{{ row.source_file }}"
1184
+ data-source-line="{{ row.source_line }}">
1185
+ <td class="col-id">
1186
+ <div class="tree-cell">
1187
+ {% for i in range(row.depth) %}
1188
+ <span class="tree-indent"></span>
1189
+ {% endfor %}
1190
+ <button class="tree-toggle {{ 'expanded' if row.has_children else 'leaf' }}"
1191
+ onclick="toggleNode('{{ row.id }}')"
1192
+ data-node-id="{{ row.id }}"></button>
1193
+ {% if row.is_code %}
1194
+ <span class="code-icon">📄</span>
1195
+ {% elif row.is_test %}
1196
+ <span class="test-icon">🧪</span>
1197
+ {% elif row.is_test_result %}
1198
+ <span class="result-icon">{% if row.result_status == 'passed' %}✅{% elif row.result_status in ['failed', 'failure'] %}❌{% elif row.result_status == 'error' %}⚠️{% elif row.result_status == 'skipped' %}⏭️{% else %}📋{% endif %}</span>
1199
+ {% endif %}
1200
+ {% if row.source_file %}
1201
+ <a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="req-id" title="Open in VS Code: {{ row.source_file }}:{{ row.source_line }}">{{ row.display_id | replace('REQ-', '') }}</a>
1202
+ {% else %}
1203
+ <span class="req-id">{{ row.display_id | replace('REQ-', '') }}</span>
1204
+ {% endif %}
1205
+ {% if row.assertions %}
1206
+ <span class="assertion-badges">
1207
+ {% for a in row.assertions %}
1208
+ <span class="assertion-badge">{{ a }}</span>
1209
+ {% endfor %}
1210
+ </span>
1211
+ {% endif %}
1212
+ </div>
1213
+ </td>
1214
+ <td class="col-title">
1215
+ {% if row.is_test_result %}
1216
+ {% if row.source_file %}
1217
+ <a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="file-link" title="Open in VS Code">{{ row.title }}</a>
1218
+ {% else %}
1219
+ {{ row.title }}
1220
+ {% endif %}
1221
+ <span class="result-badge {{ row.result_status }}">{{ row.result_status }}</span>
1222
+ {% elif (row.is_test or row.is_code) and row.source_file %}
1223
+ <a href="vscode://file/{{ row.source_file }}:{{ row.source_line }}:1" class="file-link" title="Open in VS Code">{{ row.title }}</a>
1224
+ {% else %}
1225
+ {{ row.title }}
1226
+ {% endif %}
1227
+ </td>
1228
+ <td class="col-level">{{ row.level }}</td>
1229
+ <td class="col-status">
1230
+ {% if row.status %}
1231
+ <span class="status-badge {{ row.status|lower }}">
1232
+ {{ row.status }}
1233
+ {% if row.is_changed %}<span class="change-indicator">◆</span>{% endif %}
1234
+ </span>
1235
+ {% endif %}
1236
+ </td>
1237
+ <td class="col-cov">
1238
+ <div class="coverage-cell">
1239
+ {% if row.coverage == 'full' %}
1240
+ <span class="coverage-icon full">●</span>
1241
+ {% elif row.coverage == 'partial' %}
1242
+ <span class="coverage-icon partial">◐</span>
1243
+ {% else %}
1244
+ <span class="coverage-icon none">○</span>
1245
+ {% endif %}
1246
+ {% if row.has_failures %}
1247
+ <span class="warning-icon">⚡</span>
1248
+ {% endif %}
1249
+ </div>
1250
+ </td>
1251
+ <td class="col-topic">
1252
+ <span class="topic-tag">{{ row.topic }}</span>
1253
+ </td>
1254
+ </tr>
1255
+ {% endfor %}
1256
+ </tbody>
1257
+ </table>
1258
+ </div>
1259
+ </div>
1260
+
1261
+ <!-- User Journeys Tab Content -->
1262
+ <div class="tab-content" id="content-journeys">
1263
+ <h2 class="section-title">User Journeys</h2>
1264
+ {% if journeys %}
1265
+ <div class="journey-toolbar">
1266
+ <div class="journey-search">
1267
+ <input type="text" id="journey-search" placeholder="Search journeys by ID, title, actor, or goal..." oninput="filterJourneys()">
1268
+ </div>
1269
+ <div class="journey-group-controls">
1270
+ <label for="journey-group-by">Group by:</label>
1271
+ <select id="journey-group-by" onchange="regroupJourneys()">
1272
+ <option value="none">None</option>
1273
+ <option value="descriptor">Descriptor</option>
1274
+ <option value="actor">Actor</option>
1275
+ <option value="file">File</option>
1276
+ </select>
1277
+ </div>
1278
+ <div class="journey-count">
1279
+ <span id="journey-visible-count">{{ journeys|length }}</span> of {{ journeys|length }} journeys
1280
+ </div>
1281
+ </div>
1282
+ <div class="journey-list" id="journey-list">
1283
+ {% for journey in journeys %}
1284
+ <div class="journey-card"
1285
+ data-id="{{ journey.id|lower }}"
1286
+ data-title="{{ journey.title|lower }}"
1287
+ data-actor="{{ (journey.actor or '')|lower }}"
1288
+ data-goal="{{ (journey.goal or '')|lower }}"
1289
+ data-descriptor="{{ journey.descriptor|lower }}"
1290
+ data-file="{{ journey.file|lower }}">
1291
+ <div class="journey-card-header">
1292
+ <span class="journey-id">{{ journey.id }}</span>
1293
+ <h3>{{ journey.title }}</h3>
1294
+ </div>
1295
+ {% if journey.actor or journey.goal or journey.descriptor or journey.file %}
1296
+ <div class="journey-meta">
1297
+ {% if journey.descriptor %}
1298
+ <div class="journey-meta-item">
1299
+ <span class="journey-meta-label">Topic</span>
1300
+ <span class="journey-meta-value">{{ journey.descriptor }}</span>
1301
+ </div>
1302
+ {% endif %}
1303
+ {% if journey.actor %}
1304
+ <div class="journey-meta-item">
1305
+ <span class="journey-meta-label">Actor</span>
1306
+ <span class="journey-meta-value">{{ journey.actor }}</span>
1307
+ </div>
1308
+ {% endif %}
1309
+ {% if journey.goal %}
1310
+ <div class="journey-meta-item">
1311
+ <span class="journey-meta-label">Goal</span>
1312
+ <span class="journey-meta-value">{{ journey.goal }}</span>
1313
+ </div>
1314
+ {% endif %}
1315
+ {% if journey.file %}
1316
+ <div class="journey-meta-item">
1317
+ <span class="journey-meta-label">Source</span>
1318
+ <span class="journey-meta-value">{{ journey.file }}</span>
1319
+ </div>
1320
+ {% endif %}
1321
+ </div>
1322
+ {% endif %}
1323
+ {% if journey.description %}
1324
+ <div class="journey-description">{{ journey.description }}</div>
1325
+ {% endif %}
1326
+ </div>
1327
+ {% endfor %}
1328
+ </div>
1329
+ {% else %}
1330
+ <div class="no-journeys">
1331
+ <p>No user journeys found in specification files.</p>
1332
+ <p style="font-size: 0.875rem; margin-top: 8px;">User journeys are defined with <code>## JNY-xxx: Title</code> headers in spec files.</p>
1333
+ <p style="font-size: 0.8rem; margin-top: 12px; color: #868e96;">
1334
+ Example format:<br>
1335
+ <code style="display: block; margin-top: 8px; padding: 12px; background: #f1f3f5; border-radius: 4px; text-align: left;">
1336
+ ## JNY-LOGIN-01: User Login<br>
1337
+ **Actor**: End User<br>
1338
+ **Goal**: Access the system securely<br><br>
1339
+ Steps...<br><br>
1340
+ *End* *JNY-LOGIN-01*
1341
+ </code>
1342
+ </p>
1343
+ </div>
1344
+ {% endif %}
1345
+ </div>
1346
+ </main>
1347
+
1348
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1349
+ LEGEND MODAL
1350
+ ═══════════════════════════════════════════════════════════════════════════ -->
1351
+
1352
+ <div class="modal-overlay" id="legend-modal" onclick="closeLegendOnOverlay(event)">
1353
+ <div class="modal" onclick="event.stopPropagation()">
1354
+ <div class="modal-header">
1355
+ <h3 class="modal-title">Legend</h3>
1356
+ <button class="modal-close" onclick="toggleLegend()">&times;</button>
1357
+ </div>
1358
+
1359
+ <div class="legend-section">
1360
+ <h4>Coverage Status</h4>
1361
+ <div class="legend-item">
1362
+ <span class="legend-icon">○</span>
1363
+ <span>No implementations - no code references this requirement</span>
1364
+ </div>
1365
+ <div class="legend-item">
1366
+ <span class="legend-icon">◐</span>
1367
+ <span>Partial - some assertions have code implementations</span>
1368
+ </div>
1369
+ <div class="legend-item">
1370
+ <span class="legend-icon">●</span>
1371
+ <span>Full - all assertions have code implementations</span>
1372
+ </div>
1373
+ <div class="legend-item">
1374
+ <span class="legend-icon" style="color: #ffc107;">⚡</span>
1375
+ <span>Test failures or warnings detected</span>
1376
+ </div>
1377
+ </div>
1378
+
1379
+ <div class="legend-section">
1380
+ <h4>Change Indicators</h4>
1381
+ <div class="legend-item">
1382
+ <span class="legend-icon" style="color: #fd7e14;">◆</span>
1383
+ <span>Changed vs remote main branch</span>
1384
+ </div>
1385
+ </div>
1386
+
1387
+ <div class="legend-section">
1388
+ <h4>Assertion Badges</h4>
1389
+ <div class="legend-item">
1390
+ <span class="legend-icon"><span class="assertion-badge">A</span></span>
1391
+ <span>Implements specific assertion(s) from parent requirement</span>
1392
+ </div>
1393
+ </div>
1394
+
1395
+ <div class="legend-section">
1396
+ <h4>Status Badges</h4>
1397
+ <div class="legend-item">
1398
+ <span class="status-badge draft">DRAFT</span>
1399
+ <span>Work in progress</span>
1400
+ </div>
1401
+ <div class="legend-item">
1402
+ <span class="status-badge active">ACTIVE</span>
1403
+ <span>Approved and active</span>
1404
+ </div>
1405
+ <div class="legend-item">
1406
+ <span class="status-badge deprecated">DEPRECATED</span>
1407
+ <span>No longer applicable</span>
1408
+ </div>
1409
+ </div>
1410
+
1411
+ <div class="legend-section">
1412
+ <h4>Levels</h4>
1413
+ <div class="legend-item">
1414
+ <span class="stat-badge prd" style="font-size: 0.75rem;">PRD</span>
1415
+ <span>Product Requirements - business-level needs</span>
1416
+ </div>
1417
+ <div class="legend-item">
1418
+ <span class="stat-badge ops" style="font-size: 0.75rem;">OPS</span>
1419
+ <span>Operations Requirements - operational constraints</span>
1420
+ </div>
1421
+ <div class="legend-item">
1422
+ <span class="stat-badge dev" style="font-size: 0.75rem;">DEV</span>
1423
+ <span>Development Requirements - technical specifications</span>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+ </div>
1428
+
1429
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1430
+ EMBEDDED DATA
1431
+ ═══════════════════════════════════════════════════════════════════════════ -->
1432
+
1433
+ <script type="application/json" id="tree-data">
1434
+ {{ tree_data | tojson }}
1435
+ </script>
1436
+
1437
+ <!-- ═══════════════════════════════════════════════════════════════════════════
1438
+ JAVASCRIPT
1439
+ ═══════════════════════════════════════════════════════════════════════════ -->
1440
+
1441
+ <script>
1442
+ // ─────────────────────────────────────────────────────────────────────────────
1443
+ // State
1444
+ // ─────────────────────────────────────────────────────────────────────────────
1445
+
1446
+ const state = {
1447
+ viewMode: 'hierarchical',
1448
+ activeFilters: {
1449
+ uncommitted: false,
1450
+ changed: false,
1451
+ code: false,
1452
+ tests: false,
1453
+ results: false,
1454
+ associated: false,
1455
+ core: false,
1456
+ levelPrd: false,
1457
+ levelOps: false,
1458
+ levelDev: false
1459
+ },
1460
+ collapsedNodes: new Set(),
1461
+ totalCount: {{ stats.total_count }},
1462
+ activeTab: 'requirements',
1463
+ treeData: JSON.parse(document.getElementById('tree-data').textContent)
1464
+ };
1465
+
1466
+ // ─────────────────────────────────────────────────────────────────────────────
1467
+ // Cookie Persistence
1468
+ // ─────────────────────────────────────────────────────────────────────────────
1469
+
1470
+ const COOKIE_NAME = 'elspais_trace_view_state';
1471
+ const COOKIE_DAYS = 30;
1472
+
1473
+ function saveStateToCookie() {
1474
+ const journeyGroupBy = document.getElementById('journey-group-by');
1475
+ const journeySearch = document.getElementById('journey-search');
1476
+
1477
+ const stateToSave = {
1478
+ viewMode: state.viewMode,
1479
+ activeFilters: state.activeFilters,
1480
+ collapsedNodes: Array.from(state.collapsedNodes), // Convert Set to Array for JSON
1481
+ toggleLeaf: document.getElementById('toggle-leaf').checked,
1482
+ toggleDeprecated: document.getElementById('toggle-deprecated').checked,
1483
+ toggleRoadmap: document.getElementById('toggle-roadmap').checked,
1484
+ toggleCode: document.getElementById('toggle-code').checked,
1485
+ filterLevel: document.getElementById('filter-level').value,
1486
+ filterStatus: document.getElementById('filter-status').value,
1487
+ filterCov: document.getElementById('filter-cov').value,
1488
+ filterTopic: document.getElementById('filter-topic').value,
1489
+ activeTab: state.activeTab,
1490
+ // Journey state
1491
+ journeyGroupBy: journeyGroupBy ? journeyGroupBy.value : 'none',
1492
+ journeySearch: journeySearch ? journeySearch.value : '',
1493
+ collapsedJourneyGroups: Array.from(collapsedJourneyGroups)
1494
+ };
1495
+ const expires = new Date(Date.now() + COOKIE_DAYS * 864e5).toUTCString();
1496
+ document.cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(stateToSave))}; expires=${expires}; path=/; SameSite=Lax`;
1497
+ }
1498
+
1499
+ function loadStateFromCookie() {
1500
+ const match = document.cookie.match(new RegExp('(^| )' + COOKIE_NAME + '=([^;]+)'));
1501
+ if (!match) return null;
1502
+ try {
1503
+ return JSON.parse(decodeURIComponent(match[2]));
1504
+ } catch {
1505
+ return null;
1506
+ }
1507
+ }
1508
+
1509
+ function restoreState() {
1510
+ const saved = loadStateFromCookie();
1511
+ if (!saved) return;
1512
+
1513
+ // Restore view mode
1514
+ if (saved.viewMode) {
1515
+ state.viewMode = saved.viewMode;
1516
+ document.querySelectorAll('[data-view]').forEach(btn => {
1517
+ btn.classList.toggle('active', btn.dataset.view === saved.viewMode);
1518
+ });
1519
+ document.getElementById('view-mode-label').textContent =
1520
+ saved.viewMode === 'flat' ? 'Flat View' : 'Hierarchical View';
1521
+ }
1522
+
1523
+ // Restore active filters
1524
+ if (saved.activeFilters) {
1525
+ Object.assign(state.activeFilters, saved.activeFilters);
1526
+ Object.keys(state.activeFilters).forEach(key => {
1527
+ const btn = document.getElementById('btn-' + key);
1528
+ if (btn) btn.classList.toggle('active', state.activeFilters[key]);
1529
+ });
1530
+ // Update level badges
1531
+ document.getElementById('badge-prd')?.classList.toggle('active', state.activeFilters.levelPrd);
1532
+ document.getElementById('badge-ops')?.classList.toggle('active', state.activeFilters.levelOps);
1533
+ document.getElementById('badge-dev')?.classList.toggle('active', state.activeFilters.levelDev);
1534
+ document.getElementById('badge-code')?.classList.toggle('active', state.activeFilters.code);
1535
+ document.getElementById('badge-tests')?.classList.toggle('active', state.activeFilters.tests);
1536
+ document.getElementById('badge-results')?.classList.toggle('active', state.activeFilters.results);
1537
+ document.getElementById('badge-associated')?.classList.toggle('active', state.activeFilters.associated);
1538
+ document.getElementById('badge-core')?.classList.toggle('active', state.activeFilters.core);
1539
+ }
1540
+
1541
+ // Restore checkboxes
1542
+ if (saved.toggleLeaf !== undefined) document.getElementById('toggle-leaf').checked = saved.toggleLeaf;
1543
+ if (saved.toggleDeprecated !== undefined) document.getElementById('toggle-deprecated').checked = saved.toggleDeprecated;
1544
+ if (saved.toggleRoadmap !== undefined) document.getElementById('toggle-roadmap').checked = saved.toggleRoadmap;
1545
+ if (saved.toggleCode !== undefined) document.getElementById('toggle-code').checked = saved.toggleCode;
1546
+
1547
+ // Restore dropdowns
1548
+ if (saved.filterLevel) document.getElementById('filter-level').value = saved.filterLevel;
1549
+ if (saved.filterStatus) document.getElementById('filter-status').value = saved.filterStatus;
1550
+ if (saved.filterCov) document.getElementById('filter-cov').value = saved.filterCov;
1551
+ if (saved.filterTopic) document.getElementById('filter-topic').value = saved.filterTopic;
1552
+
1553
+ // Restore tab
1554
+ if (saved.activeTab) {
1555
+ state.activeTab = saved.activeTab;
1556
+ switchTab(saved.activeTab, false);
1557
+ }
1558
+
1559
+ // Restore collapsed nodes (tree expand/collapse state)
1560
+ if (saved.collapsedNodes && Array.isArray(saved.collapsedNodes)) {
1561
+ state.collapsedNodes = new Set(saved.collapsedNodes);
1562
+ // Update toggle button visuals
1563
+ document.querySelectorAll('.tree-toggle').forEach(toggle => {
1564
+ const nodeId = toggle.dataset.nodeId;
1565
+ if (state.collapsedNodes.has(nodeId)) {
1566
+ toggle.classList.remove('expanded');
1567
+ toggle.classList.add('collapsed');
1568
+ } else if (!toggle.classList.contains('leaf')) {
1569
+ toggle.classList.remove('collapsed');
1570
+ toggle.classList.add('expanded');
1571
+ }
1572
+ });
1573
+ }
1574
+
1575
+ // Restore journey state
1576
+ if (saved.collapsedJourneyGroups && Array.isArray(saved.collapsedJourneyGroups)) {
1577
+ collapsedJourneyGroups = new Set(saved.collapsedJourneyGroups);
1578
+ }
1579
+ if (saved.journeySearch) {
1580
+ const journeySearch = document.getElementById('journey-search');
1581
+ if (journeySearch) journeySearch.value = saved.journeySearch;
1582
+ }
1583
+ if (saved.journeyGroupBy) {
1584
+ const journeyGroupBy = document.getElementById('journey-group-by');
1585
+ if (journeyGroupBy) {
1586
+ journeyGroupBy.value = saved.journeyGroupBy;
1587
+ // Apply grouping after DOM is ready
1588
+ setTimeout(() => regroupJourneys(), 0);
1589
+ }
1590
+ }
1591
+ }
1592
+
1593
+ function clearAllCookies() {
1594
+ document.cookie = `${COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
1595
+ // Reset to defaults
1596
+ state.viewMode = 'hierarchical';
1597
+ state.activeFilters = {
1598
+ uncommitted: false, changed: false, code: false, tests: false, results: false, associated: false, core: false,
1599
+ levelPrd: false, levelOps: false, levelDev: false
1600
+ };
1601
+ state.collapsedNodes.clear();
1602
+ state.activeTab = 'requirements';
1603
+
1604
+ // Reset UI
1605
+ document.querySelectorAll('[data-view]').forEach(btn => {
1606
+ btn.classList.toggle('active', btn.dataset.view === 'hierarchical');
1607
+ });
1608
+ document.querySelectorAll('.stat-badge').forEach(badge => badge.classList.remove('active'));
1609
+ document.querySelectorAll('.btn-group .btn').forEach(btn => {
1610
+ if (!btn.dataset.view) btn.classList.remove('active');
1611
+ });
1612
+ document.getElementById('toggle-leaf').checked = false;
1613
+ document.getElementById('toggle-deprecated').checked = false;
1614
+ document.getElementById('toggle-roadmap').checked = false;
1615
+ document.getElementById('toggle-code').checked = false;
1616
+ document.getElementById('filter-id').value = '';
1617
+ document.getElementById('filter-title').value = '';
1618
+ document.getElementById('filter-level').value = '';
1619
+ document.getElementById('filter-status').value = '';
1620
+ document.getElementById('filter-cov').value = '';
1621
+ document.getElementById('filter-topic').value = '';
1622
+
1623
+ switchTab('requirements', false);
1624
+ collapseAll();
1625
+ alert('Preferences cleared! Page reset to defaults.');
1626
+ }
1627
+
1628
+ // ─────────────────────────────────────────────────────────────────────────────
1629
+ // View Mode
1630
+ // ─────────────────────────────────────────────────────────────────────────────
1631
+
1632
+ function setViewMode(mode) {
1633
+ state.viewMode = mode;
1634
+
1635
+ // Update button states
1636
+ document.querySelectorAll('[data-view]').forEach(btn => {
1637
+ btn.classList.toggle('active', btn.dataset.view === mode);
1638
+ });
1639
+
1640
+ // Update label
1641
+ document.getElementById('view-mode-label').textContent =
1642
+ mode === 'flat' ? 'Flat View' : 'Hierarchical View';
1643
+
1644
+ applyFilters();
1645
+ }
1646
+
1647
+ // ─────────────────────────────────────────────────────────────────────────────
1648
+ // Filter Toggles
1649
+ // ─────────────────────────────────────────────────────────────────────────────
1650
+
1651
+ function toggleFilter(filter) {
1652
+ state.activeFilters[filter] = !state.activeFilters[filter];
1653
+
1654
+ const btn = document.getElementById('btn-' + filter);
1655
+ if (btn) btn.classList.toggle('active', state.activeFilters[filter]);
1656
+
1657
+ // Also update badges for files/associated
1658
+ const badge = document.getElementById('badge-' + filter);
1659
+ if (badge) badge.classList.toggle('active', state.activeFilters[filter]);
1660
+
1661
+ applyFilters();
1662
+ saveStateToCookie();
1663
+ }
1664
+
1665
+ function toggleLevelFilter(level) {
1666
+ const key = 'level' + level.charAt(0).toUpperCase() + level.slice(1).toLowerCase();
1667
+ state.activeFilters[key] = !state.activeFilters[key];
1668
+
1669
+ // Update badge visual
1670
+ const badge = document.getElementById('badge-' + level.toLowerCase());
1671
+ if (badge) badge.classList.toggle('active', state.activeFilters[key]);
1672
+
1673
+ // Clear dropdown when badge filters are used (badges hide, dropdown shows - they conflict)
1674
+ const anyBadgeActive = state.activeFilters.levelPrd || state.activeFilters.levelOps || state.activeFilters.levelDev;
1675
+ if (anyBadgeActive) {
1676
+ document.getElementById('filter-level').value = '';
1677
+ }
1678
+
1679
+ applyFilters();
1680
+ saveStateToCookie();
1681
+ }
1682
+
1683
+ function clearFilters() {
1684
+ // Reset all filters
1685
+ state.activeFilters = {
1686
+ uncommitted: false, changed: false, code: false, tests: false, results: false, associated: false, core: false,
1687
+ levelPrd: false, levelOps: false, levelDev: false
1688
+ };
1689
+
1690
+ // Remove active class from buttons and badges
1691
+ document.querySelectorAll('.btn-group .btn').forEach(btn => {
1692
+ if (!btn.dataset.view) btn.classList.remove('active');
1693
+ });
1694
+ document.querySelectorAll('.stat-badge').forEach(badge => badge.classList.remove('active'));
1695
+
1696
+ document.getElementById('filter-id').value = '';
1697
+ document.getElementById('filter-title').value = '';
1698
+ document.getElementById('filter-level').value = '';
1699
+ document.getElementById('filter-status').value = '';
1700
+ document.getElementById('filter-cov').value = '';
1701
+ document.getElementById('filter-topic').value = '';
1702
+
1703
+ document.getElementById('toggle-leaf').checked = false;
1704
+ document.getElementById('toggle-deprecated').checked = false;
1705
+ document.getElementById('toggle-roadmap').checked = false;
1706
+ document.getElementById('toggle-code').checked = false;
1707
+
1708
+ applyFilters();
1709
+ saveStateToCookie();
1710
+ }
1711
+
1712
+ // ─────────────────────────────────────────────────────────────────────────────
1713
+ // Apply Filters
1714
+ // ─────────────────────────────────────────────────────────────────────────────
1715
+
1716
+ function applyFilters() {
1717
+ const rows = document.querySelectorAll('#table-body .req-row');
1718
+ const filterIdValue = document.getElementById('filter-id').value.toLowerCase();
1719
+ const filterTitleValue = document.getElementById('filter-title').value.toLowerCase();
1720
+ const filterLevelValue = document.getElementById('filter-level').value;
1721
+ const filterStatusValue = document.getElementById('filter-status').value;
1722
+ const filterCovValue = document.getElementById('filter-cov').value;
1723
+ const filterTopicValue = document.getElementById('filter-topic').value;
1724
+
1725
+ const leafOnly = document.getElementById('toggle-leaf').checked;
1726
+ const includeDeprecated = document.getElementById('toggle-deprecated').checked;
1727
+ const includeRoadmap = document.getElementById('toggle-roadmap').checked;
1728
+ const showCode = document.getElementById('toggle-code').checked;
1729
+
1730
+ // Check if any level badge filters are active
1731
+ const levelBadgeFiltersActive = state.activeFilters.levelPrd ||
1732
+ state.activeFilters.levelOps ||
1733
+ state.activeFilters.levelDev;
1734
+
1735
+ let visibleCount = 0;
1736
+
1737
+ rows.forEach(row => {
1738
+ const id = row.dataset.id.toLowerCase();
1739
+ const level = row.dataset.level;
1740
+ const status = row.dataset.status;
1741
+ const coverage = row.dataset.coverage;
1742
+ const topic = row.dataset.topic;
1743
+ const title = row.querySelector('.col-title')?.textContent.toLowerCase() || '';
1744
+ const isLeaf = row.dataset.isLeaf === 'true';
1745
+ const isChanged = row.dataset.isChanged === 'true';
1746
+ const isUncommitted = row.dataset.isUncommitted === 'true';
1747
+ const isRoadmap = row.dataset.isRoadmap === 'true';
1748
+ const isCode = row.dataset.isCode === 'true';
1749
+ const isTest = row.dataset.isTest === 'true';
1750
+ const isTestResult = row.dataset.isTestResult === 'true';
1751
+ const isAssociated = row.dataset.isAssociated === 'true';
1752
+ const depth = parseInt(row.dataset.depth);
1753
+ const parentId = row.dataset.parent;
1754
+ const isImplNode = isCode || isTest || isTestResult; // Implementation/evidence nodes
1755
+
1756
+ let visible = true;
1757
+
1758
+ // Code badge filter - HIDE logic: code visible by default, hide when badge is active
1759
+ if (isCode && state.activeFilters.code) {
1760
+ visible = false;
1761
+ }
1762
+
1763
+ // Tests badge filter - HIDE logic: tests visible by default, hide when badge is active
1764
+ if (isTest && state.activeFilters.tests) {
1765
+ visible = false;
1766
+ }
1767
+
1768
+ // Results badge filter - SHOW logic: results hidden by default, show when badge is active
1769
+ if (isTestResult && !state.activeFilters.results) {
1770
+ visible = false;
1771
+ }
1772
+
1773
+ // Text filters
1774
+ if (filterIdValue && !id.includes(filterIdValue)) visible = false;
1775
+ if (filterTitleValue && !title.includes(filterTitleValue)) visible = false;
1776
+
1777
+ // Dropdown filters (level dropdown is separate from badge filters)
1778
+ if (filterLevelValue && !levelBadgeFiltersActive && level !== filterLevelValue) visible = false;
1779
+ if (filterStatusValue && status !== filterStatusValue) visible = false;
1780
+ if (filterCovValue && coverage !== filterCovValue) visible = false;
1781
+ if (filterTopicValue && topic !== filterTopicValue) visible = false;
1782
+
1783
+ // Level badge filters (HIDE logic - hide if that level's filter is active)
1784
+ if (!isImplNode) {
1785
+ if (state.activeFilters.levelPrd && level === 'PRD') visible = false;
1786
+ if (state.activeFilters.levelOps && level === 'OPS') visible = false;
1787
+ if (state.activeFilters.levelDev && level === 'DEV') visible = false;
1788
+ // Associated filter - same HIDE logic as level filters
1789
+ if (state.activeFilters.associated && isAssociated) visible = false;
1790
+ // Core filter - HIDE logic: hide non-associated (core) requirements
1791
+ if (state.activeFilters.core && !isAssociated) visible = false;
1792
+ }
1793
+
1794
+ // Toggle filters
1795
+ if (leafOnly && !isLeaf && !isImplNode) visible = false;
1796
+ if (!includeDeprecated && status === 'DEPRECATED') visible = false;
1797
+ if (!includeRoadmap && isRoadmap) visible = false;
1798
+
1799
+ // Git filters
1800
+ if (state.activeFilters.uncommitted && !isUncommitted) visible = false;
1801
+ if (state.activeFilters.changed && !isChanged) visible = false;
1802
+
1803
+ // Hierarchical mode: hide if parent is collapsed
1804
+ if (state.viewMode === 'hierarchical' && parentId && depth > 0) {
1805
+ if (isParentCollapsed(parentId)) {
1806
+ visible = false;
1807
+ }
1808
+ }
1809
+
1810
+ // Flat mode: reset indentation visually
1811
+ if (state.viewMode === 'flat') {
1812
+ // In flat mode, all rows are at depth 0 visually
1813
+ // We keep the data but hide tree structure
1814
+ row.querySelectorAll('.tree-indent').forEach(indent => {
1815
+ indent.style.display = 'none';
1816
+ });
1817
+ row.querySelector('.tree-toggle')?.classList.add('leaf');
1818
+ } else {
1819
+ row.querySelectorAll('.tree-indent').forEach(indent => {
1820
+ indent.style.display = '';
1821
+ });
1822
+ const toggle = row.querySelector('.tree-toggle');
1823
+ if (toggle && row.dataset.hasChildren === 'true') {
1824
+ toggle.classList.remove('leaf');
1825
+ }
1826
+ }
1827
+
1828
+ row.classList.toggle('hidden', !visible);
1829
+ if (visible && !isImplNode) visibleCount++; // Only count requirements, not code/test
1830
+ });
1831
+
1832
+ updateCounter(visibleCount);
1833
+ saveStateToCookie();
1834
+ }
1835
+
1836
+ function isParentCollapsed(parentId) {
1837
+ if (!parentId) return false;
1838
+ if (state.collapsedNodes.has(parentId)) return true;
1839
+
1840
+ // Check ancestors
1841
+ const parentRow = document.querySelector(`[data-id="${parentId}"]`);
1842
+ if (parentRow && parentRow.dataset.parent) {
1843
+ return isParentCollapsed(parentRow.dataset.parent);
1844
+ }
1845
+ return false;
1846
+ }
1847
+
1848
+ function updateCounter(count) {
1849
+ document.getElementById('counter').textContent =
1850
+ `Showing ${count} of ${state.totalCount} requirements`;
1851
+ }
1852
+
1853
+ // ─────────────────────────────────────────────────────────────────────────────
1854
+ // Tree Expand/Collapse
1855
+ // ─────────────────────────────────────────────────────────────────────────────
1856
+
1857
+ function toggleNode(nodeId) {
1858
+ const toggle = document.querySelector(`[data-node-id="${nodeId}"]`);
1859
+ if (!toggle) return;
1860
+
1861
+ if (state.collapsedNodes.has(nodeId)) {
1862
+ state.collapsedNodes.delete(nodeId);
1863
+ toggle.classList.remove('collapsed');
1864
+ toggle.classList.add('expanded');
1865
+ } else {
1866
+ state.collapsedNodes.add(nodeId);
1867
+ toggle.classList.remove('expanded');
1868
+ toggle.classList.add('collapsed');
1869
+ }
1870
+
1871
+ applyFilters();
1872
+ }
1873
+
1874
+ function expandAll() {
1875
+ state.collapsedNodes.clear();
1876
+ document.querySelectorAll('.tree-toggle').forEach(toggle => {
1877
+ if (!toggle.classList.contains('leaf')) {
1878
+ toggle.classList.remove('collapsed');
1879
+ toggle.classList.add('expanded');
1880
+ }
1881
+ });
1882
+ applyFilters();
1883
+ }
1884
+
1885
+ function collapseAll() {
1886
+ document.querySelectorAll('.tree-toggle').forEach(toggle => {
1887
+ if (!toggle.classList.contains('leaf')) {
1888
+ const nodeId = toggle.dataset.nodeId;
1889
+ state.collapsedNodes.add(nodeId);
1890
+ toggle.classList.remove('expanded');
1891
+ toggle.classList.add('collapsed');
1892
+ }
1893
+ });
1894
+ applyFilters();
1895
+ }
1896
+
1897
+ // ─────────────────────────────────────────────────────────────────────────────
1898
+ // Tabs
1899
+ // ─────────────────────────────────────────────────────────────────────────────
1900
+
1901
+ function switchTab(tabName, save = true) {
1902
+ state.activeTab = tabName;
1903
+
1904
+ // Update tab buttons
1905
+ document.querySelectorAll('.tab').forEach(tab => {
1906
+ tab.classList.toggle('active', tab.dataset.tab === tabName);
1907
+ });
1908
+
1909
+ // Switch tab content visibility
1910
+ document.querySelectorAll('.tab-content').forEach(content => {
1911
+ content.classList.remove('active');
1912
+ });
1913
+ const activeContent = document.getElementById('content-' + tabName);
1914
+ if (activeContent) {
1915
+ activeContent.classList.add('active');
1916
+ }
1917
+
1918
+ if (save) {
1919
+ saveStateToCookie();
1920
+ }
1921
+ }
1922
+
1923
+ // ─────────────────────────────────────────────────────────────────────────────
1924
+ // Journey Filtering and Grouping
1925
+ // ─────────────────────────────────────────────────────────────────────────────
1926
+
1927
+ // Store original card order for ungrouping
1928
+ let originalJourneyCards = null;
1929
+ let collapsedJourneyGroups = new Set();
1930
+
1931
+ function filterJourneys() {
1932
+ const searchInput = document.getElementById('journey-search');
1933
+ const query = searchInput.value.toLowerCase().trim();
1934
+
1935
+ // Filter both ungrouped cards and cards within groups
1936
+ const allCards = document.querySelectorAll('.journey-card');
1937
+ let visibleCount = 0;
1938
+
1939
+ allCards.forEach(card => {
1940
+ if (!query) {
1941
+ card.classList.remove('hidden');
1942
+ visibleCount++;
1943
+ return;
1944
+ }
1945
+
1946
+ const id = card.dataset.id || '';
1947
+ const title = card.dataset.title || '';
1948
+ const actor = card.dataset.actor || '';
1949
+ const goal = card.dataset.goal || '';
1950
+ const descriptor = card.dataset.descriptor || '';
1951
+ const file = card.dataset.file || '';
1952
+
1953
+ const matches = id.includes(query) ||
1954
+ title.includes(query) ||
1955
+ actor.includes(query) ||
1956
+ goal.includes(query) ||
1957
+ descriptor.includes(query) ||
1958
+ file.includes(query);
1959
+
1960
+ card.classList.toggle('hidden', !matches);
1961
+ if (matches) visibleCount++;
1962
+ });
1963
+
1964
+ // Update group counts if grouped
1965
+ updateGroupCounts();
1966
+
1967
+ // Update visible count
1968
+ const countEl = document.getElementById('journey-visible-count');
1969
+ if (countEl) {
1970
+ countEl.textContent = visibleCount;
1971
+ }
1972
+
1973
+ saveStateToCookie();
1974
+ }
1975
+
1976
+ function updateGroupCounts() {
1977
+ const groups = document.querySelectorAll('.journey-group');
1978
+ groups.forEach(group => {
1979
+ const visibleCards = group.querySelectorAll('.journey-card:not(.hidden)');
1980
+ const countEl = group.querySelector('.journey-group-count');
1981
+ if (countEl) {
1982
+ countEl.textContent = visibleCards.length;
1983
+ }
1984
+ // Hide group if no visible cards
1985
+ group.style.display = visibleCards.length === 0 ? 'none' : '';
1986
+ });
1987
+ }
1988
+
1989
+ function regroupJourneys() {
1990
+ const groupBy = document.getElementById('journey-group-by').value;
1991
+ const journeyList = document.getElementById('journey-list');
1992
+
1993
+ // Store original cards on first grouping
1994
+ if (!originalJourneyCards) {
1995
+ originalJourneyCards = Array.from(journeyList.querySelectorAll('.journey-card'));
1996
+ }
1997
+
1998
+ // Clear current content
1999
+ journeyList.innerHTML = '';
2000
+
2001
+ if (groupBy === 'none') {
2002
+ // Restore ungrouped view
2003
+ originalJourneyCards.forEach(card => {
2004
+ card.classList.remove('compact');
2005
+ journeyList.appendChild(card);
2006
+ });
2007
+ } else {
2008
+ // Group cards by selected attribute
2009
+ const groups = new Map();
2010
+
2011
+ originalJourneyCards.forEach(card => {
2012
+ let key = card.dataset[groupBy] || '(none)';
2013
+ // Capitalize first letter
2014
+ if (key !== '(none)') {
2015
+ key = key.charAt(0).toUpperCase() + key.slice(1);
2016
+ }
2017
+
2018
+ if (!groups.has(key)) {
2019
+ groups.set(key, []);
2020
+ }
2021
+ groups.get(key).push(card);
2022
+ });
2023
+
2024
+ // Sort groups alphabetically, but put (none) last
2025
+ const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
2026
+ if (a === '(none)') return 1;
2027
+ if (b === '(none)') return -1;
2028
+ return a.localeCompare(b);
2029
+ });
2030
+
2031
+ // Create group elements
2032
+ sortedKeys.forEach(key => {
2033
+ const cards = groups.get(key);
2034
+ const groupEl = document.createElement('div');
2035
+ groupEl.className = 'journey-group';
2036
+ groupEl.dataset.groupKey = key.toLowerCase();
2037
+
2038
+ // Check if this group was previously collapsed
2039
+ if (collapsedJourneyGroups.has(key.toLowerCase())) {
2040
+ groupEl.classList.add('collapsed');
2041
+ }
2042
+
2043
+ const header = document.createElement('div');
2044
+ header.className = 'journey-group-header';
2045
+ header.onclick = () => toggleJourneyGroup(groupEl);
2046
+ header.innerHTML = `
2047
+ <span class="expand-icon">▼</span>
2048
+ <h4>${key}</h4>
2049
+ <span class="journey-group-count">${cards.length}</span>
2050
+ `;
2051
+
2052
+ const cardsContainer = document.createElement('div');
2053
+ cardsContainer.className = 'journey-group-cards';
2054
+
2055
+ cards.forEach(card => {
2056
+ card.classList.add('compact');
2057
+ cardsContainer.appendChild(card);
2058
+ });
2059
+
2060
+ groupEl.appendChild(header);
2061
+ groupEl.appendChild(cardsContainer);
2062
+ journeyList.appendChild(groupEl);
2063
+ });
2064
+ }
2065
+
2066
+ // Re-apply search filter
2067
+ filterJourneys();
2068
+ saveStateToCookie();
2069
+ }
2070
+
2071
+ function toggleJourneyGroup(groupEl) {
2072
+ groupEl.classList.toggle('collapsed');
2073
+ const key = groupEl.dataset.groupKey;
2074
+
2075
+ if (groupEl.classList.contains('collapsed')) {
2076
+ collapsedJourneyGroups.add(key);
2077
+ } else {
2078
+ collapsedJourneyGroups.delete(key);
2079
+ }
2080
+
2081
+ saveStateToCookie();
2082
+ }
2083
+
2084
+ function expandAllJourneyGroups() {
2085
+ document.querySelectorAll('.journey-group').forEach(group => {
2086
+ group.classList.remove('collapsed');
2087
+ });
2088
+ collapsedJourneyGroups.clear();
2089
+ saveStateToCookie();
2090
+ }
2091
+
2092
+ function collapseAllJourneyGroups() {
2093
+ document.querySelectorAll('.journey-group').forEach(group => {
2094
+ group.classList.add('collapsed');
2095
+ collapsedJourneyGroups.add(group.dataset.groupKey);
2096
+ });
2097
+ saveStateToCookie();
2098
+ }
2099
+
2100
+ // ─────────────────────────────────────────────────────────────────────────────
2101
+ // Legend Modal
2102
+ // ─────────────────────────────────────────────────────────────────────────────
2103
+
2104
+ function toggleLegend() {
2105
+ const modal = document.getElementById('legend-modal');
2106
+ modal.classList.toggle('visible');
2107
+ }
2108
+
2109
+ function closeLegendOnOverlay(event) {
2110
+ if (event.target === event.currentTarget) {
2111
+ toggleLegend();
2112
+ }
2113
+ }
2114
+
2115
+ // ─────────────────────────────────────────────────────────────────────────────
2116
+ // Keyboard Shortcuts
2117
+ // ─────────────────────────────────────────────────────────────────────────────
2118
+
2119
+ document.addEventListener('keydown', function(e) {
2120
+ // Escape closes modal
2121
+ if (e.key === 'Escape') {
2122
+ const modal = document.getElementById('legend-modal');
2123
+ if (modal.classList.contains('visible')) {
2124
+ toggleLegend();
2125
+ }
2126
+ }
2127
+ });
2128
+
2129
+ // ─────────────────────────────────────────────────────────────────────────────
2130
+ // Initialize
2131
+ // ─────────────────────────────────────────────────────────────────────────────
2132
+
2133
+ document.addEventListener('DOMContentLoaded', function() {
2134
+ // Check if there's saved state to restore
2135
+ const hasSavedState = loadStateFromCookie();
2136
+
2137
+ if (hasSavedState) {
2138
+ // Restore saved state from cookie (includes collapsed nodes)
2139
+ restoreState();
2140
+ } else {
2141
+ // No saved state - start with tree collapsed (default state)
2142
+ collapseAll();
2143
+ }
2144
+
2145
+ // Apply filters (this will also save state)
2146
+ applyFilters();
2147
+ });
2148
+ </script>
2149
+
2150
+ </body>
2151
+ </html>