elspais 0.9.3__py3-none-any.whl → 0.11.0__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 (73) hide show
  1. elspais/cli.py +99 -1
  2. elspais/commands/hash_cmd.py +72 -26
  3. elspais/commands/reformat_cmd.py +458 -0
  4. elspais/commands/trace.py +157 -3
  5. elspais/commands/validate.py +44 -16
  6. elspais/core/models.py +2 -0
  7. elspais/core/parser.py +68 -24
  8. elspais/reformat/__init__.py +50 -0
  9. elspais/reformat/detector.py +119 -0
  10. elspais/reformat/hierarchy.py +246 -0
  11. elspais/reformat/line_breaks.py +220 -0
  12. elspais/reformat/prompts.py +123 -0
  13. elspais/reformat/transformer.py +264 -0
  14. elspais/sponsors/__init__.py +432 -0
  15. elspais/trace_view/__init__.py +54 -0
  16. elspais/trace_view/coverage.py +183 -0
  17. elspais/trace_view/generators/__init__.py +12 -0
  18. elspais/trace_view/generators/base.py +329 -0
  19. elspais/trace_view/generators/csv.py +122 -0
  20. elspais/trace_view/generators/markdown.py +175 -0
  21. elspais/trace_view/html/__init__.py +31 -0
  22. elspais/trace_view/html/generator.py +1006 -0
  23. elspais/trace_view/html/templates/base.html +283 -0
  24. elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
  25. elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
  26. elspais/trace_view/html/templates/components/legend_modal.html +69 -0
  27. elspais/trace_view/html/templates/components/review_panel.html +118 -0
  28. elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
  29. elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
  30. elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
  31. elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
  32. elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
  33. elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
  34. elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
  35. elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
  36. elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
  37. elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
  38. elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
  39. elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
  40. elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
  41. elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
  42. elspais/trace_view/html/templates/partials/scripts.js +1741 -0
  43. elspais/trace_view/html/templates/partials/styles.css +1756 -0
  44. elspais/trace_view/models.py +353 -0
  45. elspais/trace_view/review/__init__.py +60 -0
  46. elspais/trace_view/review/branches.py +1149 -0
  47. elspais/trace_view/review/models.py +1205 -0
  48. elspais/trace_view/review/position.py +609 -0
  49. elspais/trace_view/review/server.py +1056 -0
  50. elspais/trace_view/review/status.py +470 -0
  51. elspais/trace_view/review/storage.py +1367 -0
  52. elspais/trace_view/scanning.py +213 -0
  53. elspais/trace_view/specs/README.md +84 -0
  54. elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
  55. elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
  56. elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
  57. elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
  58. elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
  59. elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
  60. elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
  61. elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
  62. elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
  63. elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
  64. elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
  65. elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
  66. elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
  67. elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
  68. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/METADATA +33 -18
  69. elspais-0.11.0.dist-info/RECORD +101 -0
  70. elspais-0.9.3.dist-info/RECORD +0 -40
  71. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/WHEEL +0 -0
  72. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/entry_points.txt +0 -0
  73. {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1741 @@
1
+ /**
2
+ * TraceView Interactive Traceability Matrix JavaScript
3
+ *
4
+ * This module provides all interactive functionality for the trace-view HTML report.
5
+ * Organized using the module pattern with logical sub-objects for maintainability.
6
+ *
7
+ * IMPLEMENTS REQUIREMENTS:
8
+ * REQ-tv-d00003: JavaScript Extraction
9
+ */
10
+
11
+ const TraceView = (function() {
12
+ 'use strict';
13
+
14
+ // ==========================================================================
15
+ // State Management (REQ-tv-d00003-I: Global state encapsulated)
16
+ // ==========================================================================
17
+
18
+ /**
19
+ * Internal state object - encapsulates all global state variables
20
+ */
21
+ const state = {
22
+ reqCardStack: [],
23
+ pendingMoves: [],
24
+ movedRequirements: new Map(), // Track moved reqs: reqId -> {from, to}
25
+ editModeActive: false,
26
+ leafOnlyActive: false,
27
+ pendingMovesCollapsed: false,
28
+ filePickerState: { reqId: null, sourceFile: null },
29
+ allSpecFiles: [],
30
+ userAddedFiles: new Set(),
31
+ originalStatusSuffixes: new Map(),
32
+ // Navigation state
33
+ collapsedInstances: new Set(),
34
+ currentView: 'flat',
35
+ // Review mode state
36
+ reviewModeActive: false
37
+ };
38
+
39
+ // ==========================================================================
40
+ // Helper Functions
41
+ // ==========================================================================
42
+
43
+ /**
44
+ * Escape HTML special characters
45
+ * @param {string} text - Text to escape
46
+ * @returns {string} Escaped text
47
+ */
48
+ function escapeHtml(text) {
49
+ const div = document.createElement('div');
50
+ div.textContent = text;
51
+ return div.innerHTML;
52
+ }
53
+
54
+ /**
55
+ * Show a toast notification
56
+ * @param {string} message - Message to display
57
+ * @param {string} type - Type: 'success', 'error', 'warning', 'info'
58
+ */
59
+ function showToast(message, type = 'info') {
60
+ // Create toast container if it doesn't exist
61
+ let container = document.getElementById('toast-container');
62
+ if (!container) {
63
+ container = document.createElement('div');
64
+ container.id = 'toast-container';
65
+ container.className = 'toast-container';
66
+ document.body.appendChild(container);
67
+ }
68
+
69
+ // Create toast element
70
+ const toast = document.createElement('div');
71
+ toast.className = `toast toast-${type}`;
72
+ toast.textContent = message;
73
+
74
+ // Add to container
75
+ container.appendChild(toast);
76
+
77
+ // Trigger animation
78
+ requestAnimationFrame(() => {
79
+ toast.classList.add('show');
80
+ });
81
+
82
+ // Auto-remove after 4 seconds
83
+ setTimeout(() => {
84
+ toast.classList.remove('show');
85
+ setTimeout(() => toast.remove(), 300);
86
+ }, 4000);
87
+ }
88
+
89
+ /**
90
+ * Render markdown body with line numbers (table-based layout for alignment)
91
+ * Line numbers are file-relative (starting from req.line)
92
+ * @param {string} body - Markdown body text
93
+ * @param {number} startLine - Starting line number in source file
94
+ * @returns {string} HTML with line numbers
95
+ */
96
+ function renderMarkdownWithLines(body, startLine) {
97
+ const lines = body.split('\n');
98
+ const tableRowsHtml = lines.map((line, i) => {
99
+ const lineNum = startLine + i;
100
+ // Render each line as markdown (basic inline formatting)
101
+ let content = escapeHtml(line);
102
+ // Apply basic markdown formatting
103
+ content = content
104
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') // Bold
105
+ .replace(/\*(.+?)\*/g, '<em>$1</em>') // Italic
106
+ .replace(/`([^`]+)`/g, '<code>$1</code>') // Inline code
107
+ .replace(/^(#{1,6})\s+(.+)$/, (m, h, t) => // Headers
108
+ `<span class="md-heading md-h${h.length}">${t}</span>`)
109
+ .replace(/^[-*+]\s+(.+)$/, '<span class="md-list-item">• $1</span>') // Bullet list items
110
+ .replace(/^\d+\.\s+(.+)$/, '<span class="md-list-item">$1</span>') // Numbered list (1. 2. 3.)
111
+ .replace(/^([A-Za-z])\.\s+(.+)$/, '<span class="md-list-item">$1. $2</span>'); // Lettered list (A. B. C.)
112
+
113
+ return `<div class="rs-line-row" data-line="${lineNum}">
114
+ <span class="rs-line-number">${lineNum}</span>
115
+ <span class="rs-line-text">${content || ' '}</span>
116
+ </div>`;
117
+ }).join('');
118
+
119
+ return `<div class="rs-lines-table">${tableRowsHtml}</div>`;
120
+ }
121
+
122
+ // ==========================================================================
123
+ // Panel Management (REQ-tv-d00003-F: Logical sub-objects)
124
+ // ==========================================================================
125
+
126
+ /**
127
+ * Side panel operations for requirement details
128
+ */
129
+ const panel = {
130
+ /**
131
+ * Open a requirement in the side panel
132
+ * @param {string} reqId - The requirement ID to display
133
+ */
134
+ open: function(reqId) {
135
+ const panelEl = document.getElementById('req-panel');
136
+ const cardStack = document.getElementById('req-card-stack');
137
+ const reqData = window.REQ_CONTENT_DATA;
138
+
139
+ if (!reqData || !reqData[reqId]) {
140
+ console.error('Requirement data not found:', reqId);
141
+ return;
142
+ }
143
+
144
+ // Show panel if hidden
145
+ panelEl.classList.remove('hidden');
146
+
147
+ // Check if card already exists - if so, move it to top
148
+ if (state.reqCardStack.includes(reqId)) {
149
+ // Remove existing card and re-add at top
150
+ const existingCard = document.getElementById(`req-card-${reqId}`);
151
+ if (existingCard) {
152
+ existingCard.remove();
153
+ }
154
+ const index = state.reqCardStack.indexOf(reqId);
155
+ if (index > -1) {
156
+ state.reqCardStack.splice(index, 1);
157
+ }
158
+ // Continue to create new card at top
159
+ }
160
+
161
+ // Add to stack
162
+ state.reqCardStack.unshift(reqId);
163
+
164
+ // Create card element
165
+ const req = reqData[reqId];
166
+ const card = document.createElement('div');
167
+ card.className = 'req-card';
168
+ card.id = `req-card-${reqId}`;
169
+
170
+ // Render markdown content with line numbers
171
+ // Line numbers are file-relative (starting from req.line)
172
+ const bodyHtml = renderMarkdownWithLines(req.body, req.line);
173
+ // Rationale starts after body - calculate line offset
174
+ const rationaleStartLine = req.line + req.body.split('\n').length + 2; // +2 for "Rationale:" header
175
+ const rationaleHtml = req.rationale
176
+ ? renderMarkdownWithLines(req.rationale, rationaleStartLine)
177
+ : '';
178
+
179
+ // Build implements links
180
+ let implementsHtml = '';
181
+ if (req.implements && req.implements.length > 0) {
182
+ const implLinks = req.implements.sort().map(parentId =>
183
+ `<a href="#" onclick="TraceView.panel.open('${parentId}'); return false;" class="implements-link">${parentId}</a>`
184
+ ).join(', ');
185
+ implementsHtml = `<div class="req-card-implements">Implements: ${implLinks}</div>`;
186
+ }
187
+
188
+ // Determine if in roadmap based on file path
189
+ const isInRoadmap = req.filePath.includes('roadmap/');
190
+ const moveButtons = isInRoadmap
191
+ ? `<button class="edit-btn from-roadmap panel-edit-btn" onclick="TraceView.editMode.addMove('${reqId}', '${req.file}', 'from-roadmap')" title="Move out of roadmap">↩ From Roadmap</button>
192
+ <button class="edit-btn move-file panel-edit-btn" onclick="TraceView.filePicker.show('${reqId}', '${req.file}')" title="Move to different file">📁 Move</button>`
193
+ : `<button class="edit-btn to-roadmap panel-edit-btn" onclick="TraceView.editMode.addMove('${reqId}', '${req.file}', 'to-roadmap')" title="Move to roadmap">🗺️ To Roadmap</button>
194
+ <button class="edit-btn move-file panel-edit-btn" onclick="TraceView.filePicker.show('${reqId}', '${req.file}')" title="Move to different file">📁 Move</button>`;
195
+
196
+ // Generate VS Code link - use relative path when REPO_ROOT is empty (portable mode)
197
+ // Strip ALL leading ../ components to get path relative to repo root
198
+ const repoRelPath = req.filePath.replace(/^(\.\.\/)+/, '');
199
+ const vscodeHref = window.REPO_ROOT
200
+ ? `vscode://file/${window.REPO_ROOT}/${repoRelPath}:${req.line}`
201
+ : `${req.filePath}`; // Relative link for portable mode
202
+ const vscodeTitle = window.REPO_ROOT
203
+ ? 'Open in VS Code'
204
+ : `Open file (${repoRelPath}:${req.line})`;
205
+
206
+ card.innerHTML = `
207
+ <div class="req-card-header">
208
+ <span class="req-card-title">REQ-${reqId}: ${req.title}</span>
209
+ <button class="close-btn" onclick="TraceView.panel.close('${reqId}')">×</button>
210
+ </div>
211
+ <div class="req-card-body">
212
+ <div class="req-card-meta">
213
+ <span class="badge">${req.level}</span>
214
+ <span class="badge">${req.status}</span>
215
+ <a href="#" onclick="TraceView.codeViewer.open('${req.filePath}', ${req.line}); return false;" class="file-ref-link">${req.file}:${req.line}</a>
216
+ <a href="${vscodeHref}" title="${vscodeTitle}" class="vscode-link">🔧</a>
217
+ </div>
218
+ <div class="req-card-actions edit-actions">
219
+ ${moveButtons}
220
+ </div>
221
+ ${implementsHtml}
222
+ <div class="req-card-content rs-lined-content">
223
+ <div class="req-body-section">
224
+ <h5 class="rs-section-label">Body</h5>
225
+ ${bodyHtml}
226
+ </div>
227
+ ${rationaleHtml ? `
228
+ <div class="req-rationale-section">
229
+ <h5 class="rs-section-label">Rationale</h5>
230
+ ${rationaleHtml}
231
+ </div>` : ''}
232
+ </div>
233
+ </div>
234
+ `;
235
+
236
+ // Add to top of stack
237
+ cardStack.insertBefore(card, cardStack.firstChild);
238
+
239
+ // Add click handler to update Review Panel when card is clicked
240
+ card.addEventListener('click', function(e) {
241
+ // Don't trigger if clicking on buttons, links, or inputs
242
+ if (e.target.closest('button, a, input, textarea')) return;
243
+
244
+ const reviewActive = (window.ReviewSystem && window.ReviewSystem.isReviewModeActive && window.ReviewSystem.isReviewModeActive()) ||
245
+ state.reviewModeActive;
246
+ if (reviewActive) {
247
+ document.dispatchEvent(new CustomEvent('traceview:req-selected', {
248
+ detail: { reqId: reqId, req: req }
249
+ }));
250
+ }
251
+ });
252
+
253
+ // Notify review system if in review mode (initial open)
254
+ // Check ReviewSystem.isReviewModeActive() directly since it's the source of truth
255
+ const reviewActive = (window.ReviewSystem && window.ReviewSystem.isReviewModeActive && window.ReviewSystem.isReviewModeActive()) ||
256
+ state.reviewModeActive;
257
+ if (reviewActive) {
258
+ // Apply interactive line numbers with click handlers (REQ-d00092)
259
+ if (window.ReviewSystem && window.ReviewSystem.applyLineNumbersToCard) {
260
+ window.ReviewSystem.applyLineNumbersToCard(card, reqId);
261
+ } else if (window.TraceView && window.TraceView.review && window.TraceView.review.applyLineNumbersToCard) {
262
+ window.TraceView.review.applyLineNumbersToCard(card, reqId);
263
+ }
264
+
265
+ document.dispatchEvent(new CustomEvent('traceview:req-selected', {
266
+ detail: { reqId: reqId, req: req }
267
+ }));
268
+ }
269
+ },
270
+
271
+ /**
272
+ * Close a specific requirement card
273
+ * @param {string} reqId - The requirement ID to close
274
+ */
275
+ close: function(reqId) {
276
+ const card = document.getElementById(`req-card-${reqId}`);
277
+ if (card) {
278
+ card.remove();
279
+ }
280
+ const index = state.reqCardStack.indexOf(reqId);
281
+ if (index > -1) {
282
+ state.reqCardStack.splice(index, 1);
283
+ }
284
+
285
+ // Hide panel if empty
286
+ if (state.reqCardStack.length === 0) {
287
+ document.getElementById('req-panel').classList.add('hidden');
288
+ }
289
+ },
290
+
291
+ /**
292
+ * Close all requirement cards
293
+ */
294
+ closeAll: function() {
295
+ const cardStack = document.getElementById('req-card-stack');
296
+ cardStack.innerHTML = '';
297
+ state.reqCardStack.length = 0;
298
+ document.getElementById('req-panel').classList.add('hidden');
299
+ },
300
+
301
+ /**
302
+ * Initialize panel resize functionality
303
+ */
304
+ initResize: function() {
305
+ const panelEl = document.getElementById('req-panel');
306
+ const handle = document.getElementById('resizeHandle');
307
+ if (!panelEl || !handle) return;
308
+
309
+ let isResizing = false;
310
+ let startX, startWidth;
311
+
312
+ handle.addEventListener('mousedown', function(e) {
313
+ isResizing = true;
314
+ startX = e.clientX;
315
+ startWidth = panelEl.offsetWidth;
316
+ handle.classList.add('dragging');
317
+ document.body.style.cursor = 'col-resize';
318
+ document.body.style.userSelect = 'none';
319
+ e.preventDefault();
320
+ });
321
+
322
+ document.addEventListener('mousemove', function(e) {
323
+ if (!isResizing) return;
324
+ const diff = startX - e.clientX;
325
+ const newWidth = Math.min(Math.max(startWidth + diff, 250), window.innerWidth * 0.7);
326
+ panelEl.style.width = newWidth + 'px';
327
+ });
328
+
329
+ document.addEventListener('mouseup', function() {
330
+ if (isResizing) {
331
+ isResizing = false;
332
+ handle.classList.remove('dragging');
333
+ document.body.style.cursor = '';
334
+ document.body.style.userSelect = '';
335
+ }
336
+ });
337
+ }
338
+ };
339
+
340
+ // ==========================================================================
341
+ // Code Viewer (REQ-tv-d00003-F: Logical sub-objects)
342
+ // ==========================================================================
343
+
344
+ /**
345
+ * Code viewer modal operations
346
+ */
347
+ const codeViewer = {
348
+ /**
349
+ * Get language class for syntax highlighting
350
+ * @param {string} ext - File extension
351
+ * @returns {string} Language class for highlight.js
352
+ */
353
+ getLangClass: function(ext) {
354
+ const langMap = {
355
+ 'dart': 'language-dart',
356
+ 'sql': 'language-sql',
357
+ 'py': 'language-python',
358
+ 'js': 'language-javascript',
359
+ 'ts': 'language-typescript',
360
+ 'json': 'language-json',
361
+ 'md': 'language-markdown',
362
+ 'yaml': 'language-yaml',
363
+ 'yml': 'language-yaml',
364
+ 'sh': 'language-bash',
365
+ 'bash': 'language-bash'
366
+ };
367
+ return langMap[ext] || 'language-plaintext';
368
+ },
369
+
370
+ /**
371
+ * Open the code viewer modal with file content
372
+ * @param {string} filePath - Path to the file
373
+ * @param {number} lineNum - Line number to highlight
374
+ */
375
+ open: async function(filePath, lineNum) {
376
+ const modal = document.getElementById('code-viewer-modal');
377
+ const content = document.getElementById('code-viewer-content');
378
+ const title = document.getElementById('code-viewer-title');
379
+ const lineInfo = document.getElementById('code-viewer-line');
380
+ const vscodeLink = document.getElementById('code-viewer-vscode');
381
+
382
+ title.textContent = filePath;
383
+ lineInfo.textContent = `Line ${lineNum}`;
384
+ content.innerHTML = '<div class="loading">Loading...</div>';
385
+ modal.classList.remove('hidden');
386
+
387
+ // Set VS Code link
388
+ if (vscodeLink) {
389
+ // Strip ALL leading ../ components to get path relative to repo root
390
+ const repoRelPath = filePath.replace(/^(\.\.\/)+/, '');
391
+ if (window.REPO_ROOT) {
392
+ const absPath = window.REPO_ROOT + '/' + repoRelPath;
393
+ vscodeLink.href = `vscode://file/${absPath}:${lineNum}`;
394
+ vscodeLink.title = 'Open in VS Code';
395
+ } else {
396
+ vscodeLink.href = filePath;
397
+ vscodeLink.title = `Open file (${repoRelPath}:${lineNum})`;
398
+ }
399
+ }
400
+
401
+ try {
402
+ // Handle file:// URLs by using the server API (browsers block file:// fetches)
403
+ let fetchUrl = filePath;
404
+ if (filePath.startsWith('file://')) {
405
+ const absPath = filePath.replace('file://', '');
406
+ fetchUrl = `/api/files?path=${encodeURIComponent(absPath)}`;
407
+ }
408
+ const response = await fetch(fetchUrl);
409
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
410
+ const text = await response.text();
411
+ const ext = filePath.split('.').pop().toLowerCase();
412
+
413
+ // For markdown files, render as formatted markdown
414
+ if (ext === 'md' && window.marked) {
415
+ // Pre-process: convert lettered lists (A. B. C.) to proper markdown lists
416
+ // Standard markdown only recognizes numbered (1. 2.) and bullet (- *) lists
417
+ const processedText = this._preprocessMarkdown(text);
418
+ const renderedHtml = marked.parse(processedText);
419
+ content.innerHTML = `<div class="markdown-viewer markdown-body">${renderedHtml}</div>`;
420
+ content.classList.add('markdown-mode');
421
+ this._scrollToMarkdownLine(content, text, lineNum);
422
+ } else {
423
+ // For code files, show with line numbers
424
+ content.classList.remove('markdown-mode');
425
+ this._renderCodeWithLines(content, text, lineNum, ext);
426
+ }
427
+ } catch (err) {
428
+ content.innerHTML = `<div class="error">Failed to load file: ${err.message}</div>`;
429
+ }
430
+ },
431
+
432
+ /**
433
+ * Render code with line numbers and highlighting
434
+ * @private
435
+ */
436
+ _renderCodeWithLines: function(content, text, lineNum, ext) {
437
+ const lines = text.split('\n');
438
+ const langClass = this.getLangClass(ext);
439
+
440
+ let html = '<table class="code-table"><tbody>';
441
+ lines.forEach((line, idx) => {
442
+ const lineNumber = idx + 1;
443
+ const isHighlighted = lineNumber === lineNum;
444
+ const highlightClass = isHighlighted ? 'highlighted-line' : '';
445
+ const lineId = `L${lineNumber}`;
446
+ const escapedLine = line
447
+ .replace(/&/g, '&amp;')
448
+ .replace(/</g, '&lt;')
449
+ .replace(/>/g, '&gt;');
450
+ html += `<tr id="${lineId}" class="${highlightClass}">`;
451
+ html += `<td class="line-num">${lineNumber}</td>`;
452
+ html += `<td class="line-code"><pre><code class="${langClass}">${escapedLine || ' '}</code></pre></td>`;
453
+ html += '</tr>';
454
+ });
455
+ html += '</tbody></table>';
456
+
457
+ content.innerHTML = html;
458
+
459
+ // Scroll to highlighted line
460
+ setTimeout(() => {
461
+ const highlightedRow = content.querySelector('.highlighted-line');
462
+ if (highlightedRow) {
463
+ highlightedRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
464
+ }
465
+ }, 100);
466
+
467
+ // Apply syntax highlighting if hljs is available
468
+ if (window.hljs) {
469
+ content.querySelectorAll('code').forEach(block => {
470
+ hljs.highlightElement(block);
471
+ });
472
+ }
473
+ },
474
+
475
+ /**
476
+ * Scroll to approximate line in markdown view
477
+ * @private
478
+ */
479
+ _scrollToMarkdownLine: function(content, text, lineNum) {
480
+ const lines = text.split('\n');
481
+ setTimeout(() => {
482
+ let targetElement = null;
483
+
484
+ // Find the nearest heading at or before the target line
485
+ const headings = content.querySelectorAll('h1, h2, h3, h4');
486
+ for (const heading of headings) {
487
+ const headingText = heading.textContent.trim();
488
+ for (let i = 0; i < lines.length; i++) {
489
+ const line = lines[i].trim();
490
+ if (line.startsWith('#') && line.includes(headingText)) {
491
+ if (i + 1 <= lineNum) {
492
+ targetElement = heading;
493
+ }
494
+ break;
495
+ }
496
+ }
497
+ }
498
+
499
+ // Fallback to first heading
500
+ if (!targetElement) {
501
+ targetElement = content.querySelector('h1, h2, h3');
502
+ }
503
+
504
+ if (targetElement) {
505
+ targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
506
+ targetElement.classList.add('highlight-target');
507
+ setTimeout(() => targetElement.classList.remove('highlight-target'), 2000);
508
+ }
509
+ }, 100);
510
+ },
511
+
512
+ /**
513
+ * Pre-process markdown to handle non-standard list formats
514
+ * Converts lettered lists (A. B. C.) to standard bullet lists
515
+ * @private
516
+ */
517
+ _preprocessMarkdown: function(text) {
518
+ const lines = text.split('\n');
519
+ const result = [];
520
+
521
+ for (let i = 0; i < lines.length; i++) {
522
+ let line = lines[i];
523
+ // Match lines starting with a single uppercase letter followed by period and space
524
+ // e.g., "A. The system SHALL..." -> "- **A.** The system SHALL..."
525
+ const letteredMatch = line.match(/^([A-Z])\.\s+(.+)$/);
526
+ if (letteredMatch) {
527
+ // Convert to bullet list with bold letter prefix
528
+ line = `- **${letteredMatch[1]}.** ${letteredMatch[2]}`;
529
+ }
530
+ result.push(line);
531
+ }
532
+
533
+ return result.join('\n');
534
+ },
535
+
536
+ /**
537
+ * Close the code viewer modal
538
+ */
539
+ close: function() {
540
+ document.getElementById('code-viewer-modal').classList.add('hidden');
541
+ }
542
+ };
543
+
544
+ // ==========================================================================
545
+ // Edit Mode (REQ-tv-d00003-F: Logical sub-objects)
546
+ // ==========================================================================
547
+
548
+ /**
549
+ * Edit mode operations for batch requirement moves
550
+ */
551
+ const editMode = {
552
+ /**
553
+ * Toggle edit mode on/off
554
+ */
555
+ toggle: function() {
556
+ state.editModeActive = !state.editModeActive;
557
+ const btn = document.getElementById('btnEditMode');
558
+ const panel = document.getElementById('editModePanel');
559
+
560
+ if (state.editModeActive) {
561
+ document.body.classList.add('edit-mode-active');
562
+ btn.classList.add('active');
563
+ panel.style.display = 'block';
564
+ document.getElementById('chkIncludeRoadmap').checked = true;
565
+ applyFilters();
566
+ } else {
567
+ document.body.classList.remove('edit-mode-active');
568
+ btn.classList.remove('active');
569
+ panel.style.display = 'none';
570
+ }
571
+ },
572
+
573
+ /**
574
+ * Add a pending move operation
575
+ * @param {string} reqId - Requirement ID
576
+ * @param {string} sourceFile - Source file path
577
+ * @param {string} moveType - Type of move ('to-roadmap', 'from-roadmap', 'move-file')
578
+ */
579
+ addMove: function(reqId, sourceFile, moveType) {
580
+ const existing = state.pendingMoves.find(m => m.reqId === reqId);
581
+ if (existing) {
582
+ alert('This requirement already has a pending move. Clear selection first.');
583
+ return;
584
+ }
585
+
586
+ const reqItem = document.querySelector(`.req-item[data-req-id="${reqId}"]`);
587
+ const title = reqItem ? reqItem.dataset.title : '';
588
+
589
+ const move = {
590
+ reqId: reqId,
591
+ sourceFile: sourceFile,
592
+ moveType: moveType,
593
+ title: title,
594
+ targetFile: moveType === 'to-roadmap' ? `roadmap/${sourceFile}` :
595
+ moveType === 'from-roadmap' ? sourceFile.replace('roadmap/', '') :
596
+ null
597
+ };
598
+ state.pendingMoves.push(move);
599
+ this._updateUI();
600
+ this._updateDestinationColumns();
601
+ },
602
+
603
+ /**
604
+ * Remove a pending move by index
605
+ * @param {number} index - Index in pendingMoves array
606
+ */
607
+ removeMove: function(index) {
608
+ state.pendingMoves.splice(index, 1);
609
+ this._updateUI();
610
+ this._updateDestinationColumns();
611
+ },
612
+
613
+ /**
614
+ * Clear all pending moves
615
+ */
616
+ clearMoves: function() {
617
+ state.pendingMoves.length = 0;
618
+ this._updateUI();
619
+ this._updateDestinationColumns();
620
+ },
621
+
622
+ /**
623
+ * Apply moves via server API
624
+ */
625
+ applyMoves: async function() {
626
+ if (state.pendingMoves.length === 0) {
627
+ showToast('No pending moves to apply.', 'warning');
628
+ return;
629
+ }
630
+
631
+ const moves = state.pendingMoves
632
+ .filter(m => m.targetFile)
633
+ .map(m => ({
634
+ reqId: m.reqId,
635
+ source: m.sourceFile,
636
+ target: m.targetFile
637
+ }));
638
+
639
+ if (moves.length === 0) {
640
+ showToast('No valid moves (all moves need target files).', 'warning');
641
+ return;
642
+ }
643
+
644
+ // Disable button during operation
645
+ const btn = document.getElementById('btnApplyMoves');
646
+ if (btn) {
647
+ btn.disabled = true;
648
+ btn.textContent = 'Applying...';
649
+ }
650
+
651
+ try {
652
+ const response = await fetch('/api/apply-moves', {
653
+ method: 'POST',
654
+ headers: { 'Content-Type': 'application/json' },
655
+ body: JSON.stringify(moves)
656
+ });
657
+
658
+ const result = await response.json();
659
+
660
+ if (result.success) {
661
+ // Track moved requirements for visual indicator
662
+ moves.forEach(move => {
663
+ state.movedRequirements.set(move.reqId, {
664
+ from: move.source,
665
+ to: move.target
666
+ });
667
+ // Update the file in REQ_CONTENT_DATA
668
+ if (window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[move.reqId]) {
669
+ window.REQ_CONTENT_DATA[move.reqId].file = move.target;
670
+ }
671
+ });
672
+
673
+ // Clear pending moves
674
+ this.clearMoves();
675
+
676
+ // Update tree to show moved indicators
677
+ this._updateMovedIndicators();
678
+
679
+ showToast(`Successfully moved ${moves.length} requirement(s)`, 'success');
680
+ } else {
681
+ showToast(`Move failed: ${result.error}`, 'error');
682
+ }
683
+ } catch (err) {
684
+ showToast(`Move failed: ${err.message}`, 'error');
685
+ } finally {
686
+ if (btn) {
687
+ btn.disabled = state.pendingMoves.length === 0;
688
+ btn.textContent = 'Apply Moves';
689
+ }
690
+ }
691
+ },
692
+
693
+ /**
694
+ * Update tree nodes to show moved indicators
695
+ * @private
696
+ */
697
+ _updateMovedIndicators: function() {
698
+ state.movedRequirements.forEach((info, reqId) => {
699
+ // Find tree nodes with this reqId
700
+ const nodes = document.querySelectorAll(`[data-req-id="${reqId}"]`);
701
+ nodes.forEach(node => {
702
+ // Add moved indicator if not already present
703
+ if (!node.querySelector('.moved-indicator')) {
704
+ const indicator = document.createElement('span');
705
+ indicator.className = 'moved-indicator';
706
+ indicator.textContent = ' 📦';
707
+ indicator.title = `Moved from: ${info.from} → ${info.to}`;
708
+ // Insert inline with REQ ID in hierarchy view
709
+ const reqIdEl = node.querySelector('.req-id');
710
+ if (reqIdEl) {
711
+ reqIdEl.appendChild(indicator);
712
+ } else {
713
+ // Fallback for other node types
714
+ node.appendChild(indicator);
715
+ }
716
+ }
717
+ });
718
+
719
+ // Also update the Requirements panel if this REQ is open
720
+ const card = document.getElementById(`req-card-${reqId}`);
721
+ if (card) {
722
+ this._updateCardFileInfo(card, reqId, info);
723
+ }
724
+ });
725
+ },
726
+
727
+ /**
728
+ * Update file info in a requirement card after move
729
+ * @private
730
+ */
731
+ _updateCardFileInfo: function(card, reqId, moveInfo) {
732
+ // Find the file link in the card header
733
+ const fileLink = card.querySelector('.req-card-file');
734
+ if (fileLink) {
735
+ // Update the file path display
736
+ const oldText = fileLink.textContent;
737
+ fileLink.innerHTML = `📦 ${moveInfo.to} <span class="moved-from">(was: ${moveInfo.from})</span>`;
738
+ fileLink.classList.add('file-moved');
739
+ }
740
+ },
741
+
742
+ /**
743
+ * Toggle pending moves list visibility
744
+ */
745
+ togglePendingMoves: function() {
746
+ state.pendingMovesCollapsed = !state.pendingMovesCollapsed;
747
+ const list = document.getElementById('pendingMovesList');
748
+ const toggleBtn = document.getElementById('pendingMovesToggle');
749
+ if (state.pendingMovesCollapsed) {
750
+ list.style.display = 'none';
751
+ toggleBtn.textContent = '▶';
752
+ } else {
753
+ list.style.display = 'block';
754
+ toggleBtn.textContent = '▼';
755
+ }
756
+ },
757
+
758
+ /**
759
+ * Update the pending moves UI
760
+ * @private
761
+ */
762
+ _updateUI: function() {
763
+ const list = document.getElementById('pendingMovesList');
764
+ const count = document.getElementById('pendingChangesCount');
765
+ const btn = document.getElementById('btnApplyMoves');
766
+
767
+ count.textContent = state.pendingMoves.length + ' pending';
768
+ btn.disabled = state.pendingMoves.length === 0;
769
+
770
+ if (state.pendingMoves.length === 0) {
771
+ list.innerHTML = '<div style="color: #666; padding: 10px;">No pending moves. Click edit buttons on requirements to select them.</div>';
772
+ return;
773
+ }
774
+
775
+ list.innerHTML = state.pendingMoves.map((m, i) => {
776
+ const displayTarget = m.targetFile ?
777
+ (m.moveType === 'to-roadmap' ? 'Roadmap' :
778
+ m.moveType === 'from-roadmap' ? m.targetFile :
779
+ m.targetFile) :
780
+ '(select target)';
781
+ const titleDisplay = m.title ? ` - ${m.title}` : '';
782
+ return `
783
+ <div class="pending-move-item">
784
+ <span><strong>REQ-${m.reqId}</strong>${titleDisplay}</span>
785
+ <span style="color: #666; margin-left: 8px;">→ ${displayTarget}</span>
786
+ <button onclick="TraceView.editMode.removeMove(${i})" style="background: none; border: none; cursor: pointer; margin-left: auto;">✕</button>
787
+ </div>
788
+ `}).join('');
789
+ },
790
+
791
+ /**
792
+ * Update destination columns in the requirement tree
793
+ * @private
794
+ */
795
+ _updateDestinationColumns: function() {
796
+ // Reset all destination columns
797
+ document.querySelectorAll('.req-destination').forEach(el => {
798
+ const editActions = el.querySelector('.edit-actions');
799
+ const destText = el.querySelector('.dest-text');
800
+ if (editActions) editActions.style.display = '';
801
+ if (destText) {
802
+ destText.textContent = '';
803
+ destText.style.display = 'none';
804
+ }
805
+ el.className = 'req-destination edit-mode-column';
806
+ });
807
+
808
+ // Restore original status suffixes for items not in pending moves
809
+ document.querySelectorAll('.req-item[data-req-id]').forEach(item => {
810
+ const reqId = item.dataset.reqId;
811
+ const suffixEl = item.querySelector('.status-suffix');
812
+ if (suffixEl && state.originalStatusSuffixes.has(reqId)) {
813
+ const original = state.originalStatusSuffixes.get(reqId);
814
+ if (!state.pendingMoves.some(m => m.reqId === reqId)) {
815
+ suffixEl.textContent = original.text;
816
+ suffixEl.className = original.className;
817
+ suffixEl.title = original.title;
818
+ }
819
+ }
820
+ });
821
+
822
+ // Update destination columns and status suffixes for pending moves
823
+ state.pendingMoves.forEach(m => {
824
+ const reqItem = document.querySelector(`.req-item[data-req-id="${m.reqId}"]`);
825
+ if (!reqItem) return;
826
+
827
+ const destEl = reqItem.querySelector('.req-destination');
828
+ const suffixEl = reqItem.querySelector('.status-suffix');
829
+
830
+ // Save original status suffix if not already saved
831
+ if (suffixEl && !state.originalStatusSuffixes.has(m.reqId)) {
832
+ state.originalStatusSuffixes.set(m.reqId, {
833
+ text: suffixEl.textContent,
834
+ className: suffixEl.className,
835
+ title: suffixEl.title
836
+ });
837
+ }
838
+
839
+ // Update destination column
840
+ if (destEl) {
841
+ const editActions = destEl.querySelector('.edit-actions');
842
+ const destText = destEl.querySelector('.dest-text');
843
+
844
+ if (editActions) editActions.style.display = 'none';
845
+ if (destText) {
846
+ destText.style.display = '';
847
+ if (m.moveType === 'to-roadmap') {
848
+ destText.textContent = '→ Roadmap';
849
+ destEl.className = 'req-destination edit-mode-column to-roadmap';
850
+ } else if (m.moveType === 'from-roadmap') {
851
+ destText.textContent = '← From Roadmap';
852
+ destEl.className = 'req-destination edit-mode-column from-roadmap';
853
+ } else if (m.targetFile) {
854
+ const displayName = m.targetFile.replace('roadmap/', '').replace(/\.md$/, '');
855
+ destText.textContent = '→ ' + displayName;
856
+ }
857
+ }
858
+ }
859
+
860
+ // Update status suffix
861
+ if (suffixEl) {
862
+ const originalText = state.originalStatusSuffixes.get(m.reqId)?.text || '';
863
+ if (originalText && originalText !== '↝' && originalText !== '⇢') {
864
+ suffixEl.textContent = '⇢' + originalText;
865
+ suffixEl.className = 'status-suffix status-pending-move';
866
+ suffixEl.title = 'PENDING MOVE + ' + (state.originalStatusSuffixes.get(m.reqId)?.title || '');
867
+ } else {
868
+ suffixEl.textContent = '⇢';
869
+ suffixEl.className = 'status-suffix status-pending-move';
870
+ suffixEl.title = 'PENDING MOVE (not yet executed)';
871
+ }
872
+ }
873
+ });
874
+ }
875
+ };
876
+
877
+ // ==========================================================================
878
+ // File Picker (REQ-tv-d00003-F: Logical sub-objects)
879
+ // ==========================================================================
880
+
881
+ /**
882
+ * File picker modal operations
883
+ */
884
+ const filePicker = {
885
+ /**
886
+ * Show the file picker modal
887
+ * @param {string} reqId - Requirement ID
888
+ * @param {string} sourceFile - Source file path
889
+ */
890
+ show: function(reqId, sourceFile) {
891
+ state.filePickerState = { reqId, sourceFile };
892
+ state.allSpecFiles = this._getAvailableFiles();
893
+
894
+ const modal = document.getElementById('file-picker-modal');
895
+ const input = document.getElementById('filePickerInput');
896
+ const error = document.getElementById('filePickerError');
897
+
898
+ input.value = '';
899
+ error.textContent = '';
900
+ error.style.display = 'none';
901
+
902
+ this._renderList('');
903
+ modal.classList.remove('hidden');
904
+ input.focus();
905
+ },
906
+
907
+ /**
908
+ * Close the file picker modal
909
+ */
910
+ close: function() {
911
+ document.getElementById('file-picker-modal').classList.add('hidden');
912
+ state.filePickerState = { reqId: null, sourceFile: null };
913
+ },
914
+
915
+ /**
916
+ * Filter the file list based on input
917
+ * @param {string} value - Filter value
918
+ */
919
+ filter: function(value) {
920
+ this._renderList(value);
921
+ this._validate(value);
922
+ },
923
+
924
+ /**
925
+ * Select a file from the list
926
+ * @param {string} filename - Selected filename
927
+ */
928
+ select: function(filename) {
929
+ document.getElementById('filePickerInput').value = filename;
930
+ this._validate(filename);
931
+ },
932
+
933
+ /**
934
+ * Confirm the file picker selection
935
+ */
936
+ confirm: function() {
937
+ const input = document.getElementById('filePickerInput');
938
+ const filename = input.value.trim();
939
+
940
+ if (!this._validate(filename)) {
941
+ return;
942
+ }
943
+
944
+ state.userAddedFiles.add(filename);
945
+ editMode.addMove(state.filePickerState.reqId, state.filePickerState.sourceFile, 'move-file');
946
+ state.pendingMoves[state.pendingMoves.length - 1].targetFile = filename;
947
+ editMode._updateUI();
948
+ editMode._updateDestinationColumns();
949
+
950
+ this.close();
951
+ },
952
+
953
+ /**
954
+ * Render the file list
955
+ * @private
956
+ */
957
+ _renderList: function(filter) {
958
+ const list = document.getElementById('filePickerList');
959
+ const filterLower = filter.toLowerCase();
960
+
961
+ const filtered = state.allSpecFiles.filter(f =>
962
+ f.toLowerCase().includes(filterLower)
963
+ );
964
+
965
+ if (filtered.length === 0 && filter) {
966
+ list.innerHTML = '<div class="file-picker-empty">No matching files. You can enter a new filename.</div>';
967
+ } else {
968
+ list.innerHTML = filtered.map(f =>
969
+ `<div class="file-picker-item" onclick="TraceView.filePicker.select('${f}')">${f}</div>`
970
+ ).join('');
971
+ }
972
+ },
973
+
974
+ /**
975
+ * Validate the filename
976
+ * @private
977
+ */
978
+ _validate: function(filename) {
979
+ const error = document.getElementById('filePickerError');
980
+
981
+ if (!filename || !filename.trim()) {
982
+ error.style.display = 'none';
983
+ return false;
984
+ }
985
+
986
+ filename = filename.trim();
987
+
988
+ if (!filename.endsWith('.md')) {
989
+ error.textContent = 'Filename must end with .md';
990
+ error.style.display = 'block';
991
+ return false;
992
+ }
993
+
994
+ const illegalChars = /[<>:"|?*\x00-\x1f]/;
995
+ if (illegalChars.test(filename)) {
996
+ error.textContent = 'Filename contains illegal characters';
997
+ error.style.display = 'block';
998
+ return false;
999
+ }
1000
+
1001
+ if (filename.includes(' ')) {
1002
+ error.textContent = 'Use dashes instead of spaces';
1003
+ error.style.display = 'block';
1004
+ return false;
1005
+ }
1006
+
1007
+ if (/^[.\-\/]/.test(filename)) {
1008
+ error.textContent = 'Filename cannot start with . - or /';
1009
+ error.style.display = 'block';
1010
+ return false;
1011
+ }
1012
+
1013
+ error.style.display = 'none';
1014
+ return true;
1015
+ },
1016
+
1017
+ /**
1018
+ * Get available target files
1019
+ * @private
1020
+ */
1021
+ _getAvailableFiles: function() {
1022
+ const files = new Set();
1023
+ document.querySelectorAll('.req-item[data-file]').forEach(item => {
1024
+ files.add(item.dataset.file);
1025
+ });
1026
+ state.userAddedFiles.forEach(f => files.add(f));
1027
+ return Array.from(files).sort();
1028
+ }
1029
+ };
1030
+
1031
+ // ==========================================================================
1032
+ // Legend Modal
1033
+ // ==========================================================================
1034
+
1035
+ /**
1036
+ * Legend modal operations
1037
+ */
1038
+ const legend = {
1039
+ /**
1040
+ * Open the legend modal
1041
+ */
1042
+ open: function() {
1043
+ document.getElementById('legend-modal').classList.remove('hidden');
1044
+ },
1045
+
1046
+ /**
1047
+ * Close the legend modal
1048
+ */
1049
+ close: function() {
1050
+ document.getElementById('legend-modal').classList.add('hidden');
1051
+ }
1052
+ };
1053
+
1054
+ // ==========================================================================
1055
+ // Navigation & View Management
1056
+ // ==========================================================================
1057
+
1058
+ /**
1059
+ * Navigation operations for requirement tree
1060
+ */
1061
+ const navigation = {
1062
+ /**
1063
+ * Toggle a single requirement instance's children
1064
+ * @param {HTMLElement} element - The clicked element
1065
+ */
1066
+ toggleRequirement: function(element) {
1067
+ const item = element.closest('.req-item');
1068
+ const instanceId = item.dataset.instanceId;
1069
+ const icon = element.querySelector('.collapse-icon');
1070
+
1071
+ if (!icon || !icon.textContent) return; // No children to collapse
1072
+
1073
+ const isExpanding = state.collapsedInstances.has(instanceId);
1074
+
1075
+ if (isExpanding) {
1076
+ state.collapsedInstances.delete(instanceId);
1077
+ icon.classList.remove('collapsed');
1078
+ } else {
1079
+ state.collapsedInstances.add(instanceId);
1080
+ icon.classList.add('collapsed');
1081
+ }
1082
+
1083
+ if (state.currentView === 'hierarchy') {
1084
+ this.toggleRequirementHierarchy(instanceId, isExpanding);
1085
+ } else {
1086
+ if (isExpanding) {
1087
+ this.showDescendants(instanceId);
1088
+ } else {
1089
+ this.hideDescendants(instanceId);
1090
+ }
1091
+ }
1092
+ this.updateExpandCollapseButtons();
1093
+ },
1094
+
1095
+ /**
1096
+ * Hide all descendants of a requirement instance
1097
+ * @param {string} parentInstanceId - Parent instance ID
1098
+ */
1099
+ hideDescendants: function(parentInstanceId) {
1100
+ document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
1101
+ child.classList.add('collapsed-by-parent');
1102
+ this.hideDescendants(child.dataset.instanceId);
1103
+ });
1104
+ },
1105
+
1106
+ /**
1107
+ * Show immediate children of a requirement instance
1108
+ * @param {string} parentInstanceId - Parent instance ID
1109
+ */
1110
+ showDescendants: function(parentInstanceId) {
1111
+ document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
1112
+ child.classList.remove('collapsed-by-parent');
1113
+ });
1114
+ },
1115
+
1116
+ /**
1117
+ * Modified toggle for hierarchy view
1118
+ * @param {string} parentInstanceId - Parent instance ID
1119
+ * @param {boolean} isExpanding - Whether expanding or collapsing
1120
+ */
1121
+ toggleRequirementHierarchy: function(parentInstanceId, isExpanding) {
1122
+ document.querySelectorAll(`[data-parent-instance-id="${parentInstanceId}"]`).forEach(child => {
1123
+ if (isExpanding) {
1124
+ child.classList.add('hierarchy-visible');
1125
+ child.classList.remove('collapsed-by-parent');
1126
+ } else {
1127
+ child.classList.remove('hierarchy-visible');
1128
+ child.classList.add('collapsed-by-parent');
1129
+ const childIcon = child.querySelector('.collapse-icon');
1130
+ if (childIcon && childIcon.textContent) {
1131
+ state.collapsedInstances.add(child.dataset.instanceId);
1132
+ childIcon.classList.add('collapsed');
1133
+ this.toggleRequirementHierarchy(child.dataset.instanceId, false);
1134
+ }
1135
+ }
1136
+ });
1137
+ },
1138
+
1139
+ /**
1140
+ * Update expand/collapse button states
1141
+ */
1142
+ updateExpandCollapseButtons: function() {
1143
+ const btnExpand = document.getElementById('btnExpandAll');
1144
+ const btnCollapse = document.getElementById('btnCollapseAll');
1145
+
1146
+ let expandableCount = 0;
1147
+ let expandedCount = 0;
1148
+ let collapsedCount = 0;
1149
+
1150
+ document.querySelectorAll('.req-item:not(.filtered-out)').forEach(item => {
1151
+ const icon = item.querySelector('.collapse-icon');
1152
+ if (icon && icon.textContent) {
1153
+ expandableCount++;
1154
+ if (icon.classList.contains('collapsed')) {
1155
+ collapsedCount++;
1156
+ } else {
1157
+ expandedCount++;
1158
+ }
1159
+ }
1160
+ });
1161
+
1162
+ if (expandableCount > 0 && expandedCount === expandableCount) {
1163
+ btnExpand.classList.add('active');
1164
+ btnExpand.textContent = '▼ All Expanded';
1165
+ } else {
1166
+ btnExpand.classList.remove('active');
1167
+ btnExpand.textContent = '▼ Expand All';
1168
+ }
1169
+
1170
+ if (expandableCount > 0 && collapsedCount === expandableCount) {
1171
+ btnCollapse.classList.add('active');
1172
+ btnCollapse.textContent = '▶ All Collapsed';
1173
+ } else {
1174
+ btnCollapse.classList.remove('active');
1175
+ btnCollapse.textContent = '▶ Collapse All';
1176
+ }
1177
+ },
1178
+
1179
+ /**
1180
+ * Expand all requirements
1181
+ */
1182
+ expandAll: function() {
1183
+ state.collapsedInstances.clear();
1184
+ const isHierarchyView = state.currentView === 'hierarchy';
1185
+ document.querySelectorAll('.req-item').forEach(item => {
1186
+ item.classList.remove('collapsed-by-parent');
1187
+ if (isHierarchyView && item.dataset.isRoot !== 'true') {
1188
+ item.classList.add('hierarchy-visible');
1189
+ }
1190
+ });
1191
+ document.querySelectorAll('.collapse-icon').forEach(el => {
1192
+ el.classList.remove('collapsed');
1193
+ });
1194
+ this.updateExpandCollapseButtons();
1195
+ },
1196
+
1197
+ /**
1198
+ * Collapse all requirements
1199
+ */
1200
+ collapseAll: function() {
1201
+ const isHierarchyView = state.currentView === 'hierarchy';
1202
+ document.querySelectorAll('.req-item').forEach(item => {
1203
+ const icon = item.querySelector('.collapse-icon');
1204
+ if (isHierarchyView && item.dataset.isRoot !== 'true') {
1205
+ item.classList.remove('hierarchy-visible');
1206
+ item.classList.add('collapsed-by-parent');
1207
+ }
1208
+ if (icon && icon.textContent) {
1209
+ state.collapsedInstances.add(item.dataset.instanceId);
1210
+ this.hideDescendants(item.dataset.instanceId);
1211
+ icon.classList.add('collapsed');
1212
+ }
1213
+ });
1214
+ this.updateExpandCollapseButtons();
1215
+ },
1216
+
1217
+ /**
1218
+ * Switch between view modes
1219
+ * @param {string} viewMode - 'flat', 'hierarchy', 'uncommitted', or 'branch'
1220
+ */
1221
+ switchView: function(viewMode) {
1222
+ state.currentView = viewMode;
1223
+ const reqTree = document.getElementById('reqTree');
1224
+ const btnFlat = document.getElementById('btnFlatView');
1225
+ const btnHierarchy = document.getElementById('btnHierarchyView');
1226
+ const btnUncommitted = document.getElementById('btnUncommittedView');
1227
+ const btnBranch = document.getElementById('btnBranchView');
1228
+ const treeTitle = document.getElementById('treeTitle');
1229
+
1230
+ btnFlat.classList.remove('active');
1231
+ btnHierarchy.classList.remove('active');
1232
+ btnUncommitted.classList.remove('active');
1233
+ btnBranch.classList.remove('active');
1234
+ reqTree.classList.remove('hierarchy-view');
1235
+ reqTree.classList.remove('flat-view');
1236
+
1237
+ if (viewMode === 'hierarchy') {
1238
+ reqTree.classList.add('hierarchy-view');
1239
+ btnHierarchy.classList.add('active');
1240
+ treeTitle.textContent = 'Traceability Tree - Hierarchical View';
1241
+ state.collapsedInstances.clear();
1242
+ document.querySelectorAll('.req-item').forEach(item => {
1243
+ item.classList.remove('collapsed-by-parent');
1244
+ item.classList.remove('hierarchy-visible');
1245
+ const icon = item.querySelector('.collapse-icon');
1246
+ if (icon) {
1247
+ // Reset all icons first
1248
+ icon.classList.remove('collapsed');
1249
+ // Then collapse root items only
1250
+ if (icon.textContent && item.dataset.isRoot === 'true') {
1251
+ state.collapsedInstances.add(item.dataset.instanceId);
1252
+ icon.classList.add('collapsed');
1253
+ }
1254
+ }
1255
+ });
1256
+ } else if (viewMode === 'uncommitted') {
1257
+ btnUncommitted.classList.add('active');
1258
+ treeTitle.textContent = 'Traceability Tree - Uncommitted Changes';
1259
+ document.querySelectorAll('.req-item').forEach(item => {
1260
+ item.classList.remove('hierarchy-visible');
1261
+ });
1262
+ this.collapseAll();
1263
+ } else if (viewMode === 'branch') {
1264
+ btnBranch.classList.add('active');
1265
+ treeTitle.textContent = 'Traceability Tree - Changed vs Main';
1266
+ document.querySelectorAll('.req-item').forEach(item => {
1267
+ item.classList.remove('hierarchy-visible');
1268
+ });
1269
+ this.collapseAll();
1270
+ } else {
1271
+ btnFlat.classList.add('active');
1272
+ treeTitle.textContent = 'Traceability Tree - Flat View';
1273
+ reqTree.classList.add('flat-view');
1274
+ // Reset all collapse state for flat view
1275
+ state.collapsedInstances.clear();
1276
+ document.querySelectorAll('.req-item').forEach(item => {
1277
+ item.classList.remove('hierarchy-visible');
1278
+ item.classList.remove('collapsed-by-parent');
1279
+ });
1280
+ // Reset all collapse icons to expanded state
1281
+ document.querySelectorAll('.collapse-icon').forEach(el => {
1282
+ el.classList.remove('collapsed');
1283
+ });
1284
+ }
1285
+
1286
+ applyFilters();
1287
+ }
1288
+ };
1289
+
1290
+ // ==========================================================================
1291
+ // Filtering
1292
+ // ==========================================================================
1293
+
1294
+ /**
1295
+ * Apply all filters to the requirement tree
1296
+ */
1297
+ function applyFilters() {
1298
+ const filterReqId = document.getElementById('filterReqId');
1299
+ const filterTitle = document.getElementById('filterTitle');
1300
+ const filterLevel = document.getElementById('filterLevel');
1301
+ const filterStatus = document.getElementById('filterStatus');
1302
+ const filterTopic = document.getElementById('filterTopic');
1303
+ const filterTests = document.getElementById('filterTests');
1304
+ const filterCoverage = document.getElementById('filterCoverage');
1305
+ const chkIncludeDeprecated = document.getElementById('chkIncludeDeprecated');
1306
+ const chkIncludeRoadmap = document.getElementById('chkIncludeRoadmap');
1307
+
1308
+ const reqIdFilter = filterReqId ? filterReqId.value.toLowerCase().trim() : '';
1309
+ const titleFilter = filterTitle ? filterTitle.value.toLowerCase().trim() : '';
1310
+ const levelFilter = filterLevel ? filterLevel.value : '';
1311
+ const statusFilter = filterStatus ? filterStatus.value : '';
1312
+ const topicFilter = filterTopic ? filterTopic.value.toLowerCase().trim() : '';
1313
+ const testFilter = filterTests ? filterTests.value : '';
1314
+ const coverageFilter = filterCoverage ? filterCoverage.value : '';
1315
+ const isLeafOnly = state.leafOnlyActive;
1316
+ const includeDeprecated = chkIncludeDeprecated ? chkIncludeDeprecated.checked : false;
1317
+ const includeRoadmap = chkIncludeRoadmap ? chkIncludeRoadmap.checked : false;
1318
+
1319
+ const isUncommittedView = state.currentView === 'uncommitted';
1320
+ const isBranchView = state.currentView === 'branch';
1321
+ const isModifiedView = isUncommittedView || isBranchView;
1322
+ const anyFilterActive = reqIdFilter || titleFilter || levelFilter || statusFilter ||
1323
+ topicFilter || testFilter || coverageFilter || isLeafOnly || isModifiedView;
1324
+
1325
+ let visibleCount = 0;
1326
+ const seenReqIds = new Set();
1327
+ const seenVisibleReqIds = new Set();
1328
+ const allReqIds = new Set();
1329
+
1330
+ document.querySelectorAll('.req-item').forEach(item => {
1331
+ const reqId = item.dataset.reqId ? item.dataset.reqId.toLowerCase() : '';
1332
+ const isImplFile = item.classList.contains('impl-file');
1333
+ const status = item.dataset.status;
1334
+
1335
+ if (!isImplFile && reqId) {
1336
+ if (includeDeprecated || status !== 'Deprecated') {
1337
+ allReqIds.add(reqId);
1338
+ }
1339
+ }
1340
+
1341
+ const level = item.dataset.level;
1342
+ const topic = item.dataset.topic ? item.dataset.topic.toLowerCase() : '';
1343
+ const title = item.dataset.title ? item.dataset.title.toLowerCase() : '';
1344
+ const isUncommitted = item.dataset.uncommitted === 'true';
1345
+ const isBranchChanged = item.dataset.branchChanged === 'true';
1346
+
1347
+ let matches = true;
1348
+
1349
+ if (isUncommittedView) {
1350
+ if (isImplFile) {
1351
+ const parentId = item.dataset.parentInstanceId;
1352
+ const parent = document.querySelector(`[data-instance-id="${parentId}"]`);
1353
+ if (!parent || parent.dataset.uncommitted !== 'true') {
1354
+ matches = false;
1355
+ }
1356
+ } else if (!isUncommitted) {
1357
+ matches = false;
1358
+ }
1359
+ }
1360
+
1361
+ if (isBranchView) {
1362
+ if (isImplFile) {
1363
+ const parentId = item.dataset.parentInstanceId;
1364
+ const parent = document.querySelector(`[data-instance-id="${parentId}"]`);
1365
+ if (!parent || parent.dataset.branchChanged !== 'true') {
1366
+ matches = false;
1367
+ }
1368
+ } else if (!isBranchChanged) {
1369
+ matches = false;
1370
+ }
1371
+ }
1372
+
1373
+ if (reqIdFilter && !reqId.includes(reqIdFilter)) matches = false;
1374
+ if (titleFilter && !title.includes(titleFilter)) matches = false;
1375
+ if (levelFilter && level !== levelFilter) matches = false;
1376
+ if (statusFilter && status !== statusFilter) matches = false;
1377
+ if (topicFilter && topic !== topicFilter && !topic.startsWith(topicFilter + '-')) {
1378
+ matches = false;
1379
+ }
1380
+
1381
+ // Toggle filter: hidden levels (PRD/OPS/DEV buttons)
1382
+ if (matches && !isImplFile && TraceView.hiddenLevels && TraceView.hiddenLevels.has(level)) {
1383
+ matches = false;
1384
+ }
1385
+
1386
+ // Toggle filter: hidden repos (CORE/CAL/TTN etc buttons)
1387
+ if (matches && !isImplFile && TraceView.hiddenRepos) {
1388
+ const repo = item.dataset.repo || 'CORE'; // Empty repo = CORE
1389
+ if (TraceView.hiddenRepos.has(repo)) {
1390
+ matches = false;
1391
+ }
1392
+ }
1393
+
1394
+ // Toggle filter: hide implementation files
1395
+ if (matches && isImplFile) {
1396
+ // Hide all files if Files toggle is off
1397
+ if (TraceView.hideFiles) {
1398
+ matches = false;
1399
+ } else {
1400
+ // Check if parent requirement is hidden (due to level/repo filter)
1401
+ const parentInstanceId = item.dataset.parentInstanceId;
1402
+ if (parentInstanceId) {
1403
+ const parent = document.querySelector(`[data-instance-id="${parentInstanceId}"]`);
1404
+ if (parent) {
1405
+ const parentLevel = parent.dataset.level;
1406
+ const parentRepo = parent.dataset.repo || 'CORE';
1407
+ // Hide file if parent's level is hidden
1408
+ if (TraceView.hiddenLevels && TraceView.hiddenLevels.has(parentLevel)) {
1409
+ matches = false;
1410
+ }
1411
+ // Hide file if parent's repo is hidden
1412
+ if (matches && TraceView.hiddenRepos && TraceView.hiddenRepos.has(parentRepo)) {
1413
+ matches = false;
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ if (testFilter && matches) {
1421
+ const testStatus = item.dataset.testStatus || 'not-tested';
1422
+ if (testFilter !== testStatus) matches = false;
1423
+ }
1424
+
1425
+ if (coverageFilter && matches) {
1426
+ const coverage = item.dataset.coverage || 'none';
1427
+ if (coverageFilter !== coverage) matches = false;
1428
+ }
1429
+
1430
+ if (isLeafOnly && matches && !isImplFile) {
1431
+ const hasChildren = item.dataset.hasChildren === 'true';
1432
+ if (hasChildren) matches = false;
1433
+ }
1434
+
1435
+ if (!includeDeprecated && matches && !isImplFile) {
1436
+ if (status === 'Deprecated') matches = false;
1437
+ }
1438
+
1439
+ if (!includeRoadmap && matches && !isImplFile) {
1440
+ const isRoadmap = item.dataset.roadmap === 'true';
1441
+ const isConflict = item.dataset.conflict === 'true';
1442
+ const isCycle = item.dataset.cycle === 'true';
1443
+ if (isRoadmap && !isConflict && !isCycle) matches = false;
1444
+ }
1445
+
1446
+ if (matches && anyFilterActive && !isImplFile && seenReqIds.has(reqId)) {
1447
+ matches = false;
1448
+ }
1449
+
1450
+ if (matches) {
1451
+ item.classList.remove('filtered-out');
1452
+ if (anyFilterActive) {
1453
+ item.classList.remove('collapsed-by-parent');
1454
+ // In hierarchy view, non-root items need hierarchy-visible to be shown
1455
+ const isHierarchyView = state.currentView === 'hierarchy';
1456
+ if (isHierarchyView && item.dataset.isRoot !== 'true') {
1457
+ item.classList.add('hierarchy-visible');
1458
+ }
1459
+ if (!isImplFile) seenReqIds.add(reqId);
1460
+ }
1461
+ if (!isImplFile && reqId && !seenVisibleReqIds.has(reqId)) {
1462
+ seenVisibleReqIds.add(reqId);
1463
+ visibleCount++;
1464
+ }
1465
+ } else {
1466
+ item.classList.add('filtered-out');
1467
+ }
1468
+ });
1469
+
1470
+ const totalCount = allReqIds.size;
1471
+ let statsText;
1472
+ if (isUncommittedView) {
1473
+ statsText = `Showing ${visibleCount} uncommitted requirements`;
1474
+ } else if (isBranchView) {
1475
+ statsText = `Showing ${visibleCount} requirements changed vs main`;
1476
+ } else {
1477
+ statsText = `Showing ${visibleCount} of ${totalCount} requirements`;
1478
+ }
1479
+ document.getElementById('filterStats').textContent = statsText;
1480
+ navigation.updateExpandCollapseButtons();
1481
+ }
1482
+
1483
+ /**
1484
+ * Clear all filters
1485
+ */
1486
+ function clearFilters() {
1487
+ const filterReqId = document.getElementById('filterReqId');
1488
+ const filterTitle = document.getElementById('filterTitle');
1489
+ const filterLevel = document.getElementById('filterLevel');
1490
+ const filterStatus = document.getElementById('filterStatus');
1491
+ const filterTopic = document.getElementById('filterTopic');
1492
+ const filterTests = document.getElementById('filterTests');
1493
+ const filterCoverage = document.getElementById('filterCoverage');
1494
+ const btnLeafOnly = document.getElementById('btnLeafOnly');
1495
+ const chkIncludeDeprecated = document.getElementById('chkIncludeDeprecated');
1496
+ const chkIncludeRoadmap = document.getElementById('chkIncludeRoadmap');
1497
+
1498
+ if (filterReqId) filterReqId.value = '';
1499
+ if (filterTitle) filterTitle.value = '';
1500
+ if (filterLevel) filterLevel.value = '';
1501
+ if (filterStatus) filterStatus.value = '';
1502
+ if (filterTopic) filterTopic.value = '';
1503
+ if (filterTests) filterTests.value = '';
1504
+ if (filterCoverage) filterCoverage.value = '';
1505
+
1506
+ state.leafOnlyActive = false;
1507
+ if (btnLeafOnly) btnLeafOnly.classList.remove('active');
1508
+ if (chkIncludeDeprecated) chkIncludeDeprecated.checked = false;
1509
+ if (chkIncludeRoadmap) chkIncludeRoadmap.checked = false;
1510
+
1511
+ toggleIncludeDeprecated();
1512
+ }
1513
+
1514
+ // ==========================================================================
1515
+ // Leaf Only Filter
1516
+ // ==========================================================================
1517
+
1518
+ /**
1519
+ * Toggle leaf-only filter
1520
+ */
1521
+ function toggleLeafOnly() {
1522
+ state.leafOnlyActive = !state.leafOnlyActive;
1523
+ const btn = document.getElementById('btnLeafOnly');
1524
+ if (state.leafOnlyActive) {
1525
+ btn.classList.add('active');
1526
+ } else {
1527
+ btn.classList.remove('active');
1528
+ }
1529
+ applyFilters();
1530
+ }
1531
+
1532
+ /**
1533
+ * Toggle include deprecated checkbox
1534
+ */
1535
+ function toggleIncludeDeprecated() {
1536
+ const includeDeprecated = document.getElementById('chkIncludeDeprecated').checked;
1537
+
1538
+ ['PRD', 'OPS', 'DEV'].forEach(level => {
1539
+ const badge = document.getElementById('badge' + level);
1540
+ if (badge) {
1541
+ const count = includeDeprecated ? badge.dataset.all : badge.dataset.active;
1542
+ badge.textContent = level + ': ' + count;
1543
+ }
1544
+ });
1545
+
1546
+ applyFilters();
1547
+ }
1548
+
1549
+ /**
1550
+ * Toggle include roadmap checkbox
1551
+ */
1552
+ function toggleIncludeRoadmap() {
1553
+ applyFilters();
1554
+ }
1555
+
1556
+ // ==========================================================================
1557
+ // Review Mode Toggle (REQ-tv-d00016)
1558
+ // ==========================================================================
1559
+
1560
+ /**
1561
+ * Toggle review mode on/off
1562
+ * Shows/hides the review panel and updates body class for 3-column layout
1563
+ */
1564
+ function toggleReviewMode() {
1565
+ state.reviewModeActive = !state.reviewModeActive;
1566
+ const btn = document.getElementById('btnReviewMode');
1567
+ const panel = document.getElementById('review-panel');
1568
+ const packagesPanel = document.getElementById('rs-packages-panel');
1569
+
1570
+ if (state.reviewModeActive) {
1571
+ // Activate review mode
1572
+ document.body.classList.add('review-mode-active');
1573
+ if (btn) btn.classList.add('active');
1574
+ if (panel) panel.classList.remove('hidden');
1575
+ if (packagesPanel) packagesPanel.style.display = 'block';
1576
+
1577
+ // Initialize review system if available
1578
+ if (window.TraceView && window.TraceView.review) {
1579
+ window.TraceView.review.init();
1580
+ }
1581
+ } else {
1582
+ // Deactivate review mode
1583
+ document.body.classList.remove('review-mode-active');
1584
+ if (btn) btn.classList.remove('active');
1585
+ if (panel) panel.classList.add('hidden');
1586
+ if (packagesPanel) packagesPanel.style.display = 'none';
1587
+ }
1588
+
1589
+ // Dispatch custom event for review system to respond to
1590
+ document.dispatchEvent(new CustomEvent('traceview:review-mode-changed', {
1591
+ detail: { active: state.reviewModeActive }
1592
+ }));
1593
+ }
1594
+
1595
+ // ==========================================================================
1596
+ // Initialization (REQ-tv-d00003-J: addEventListener for dynamic elements)
1597
+ // ==========================================================================
1598
+
1599
+ /**
1600
+ * Initialize TraceView
1601
+ */
1602
+ function init() {
1603
+ panel.initResize();
1604
+
1605
+ // Close modals on escape key
1606
+ document.addEventListener('keydown', function(e) {
1607
+ if (e.key === 'Escape') {
1608
+ codeViewer.close();
1609
+ legend.close();
1610
+ filePicker.close();
1611
+ }
1612
+ });
1613
+
1614
+ // Sync review mode state with review system (REQ-d00092)
1615
+ // The review system dispatches this event when mode changes
1616
+ document.addEventListener('rs:review-mode-changed', function(e) {
1617
+ state.reviewModeActive = e.detail.active;
1618
+ });
1619
+ }
1620
+
1621
+ // ==========================================================================
1622
+ // Public API
1623
+ // ==========================================================================
1624
+
1625
+ return {
1626
+ // Sub-objects
1627
+ panel: panel,
1628
+ codeViewer: codeViewer,
1629
+ editMode: editMode,
1630
+ filePicker: filePicker,
1631
+ legend: legend,
1632
+ navigation: navigation,
1633
+ state: state,
1634
+
1635
+ // Functions
1636
+ init: init,
1637
+ toggleLeafOnly: toggleLeafOnly,
1638
+ toggleIncludeDeprecated: toggleIncludeDeprecated,
1639
+ toggleIncludeRoadmap: toggleIncludeRoadmap,
1640
+ toggleReviewMode: toggleReviewMode,
1641
+ applyFilters: applyFilters,
1642
+ clearFilters: clearFilters
1643
+ };
1644
+ })();
1645
+
1646
+ // ==========================================================================
1647
+ // Global function aliases for backward compatibility with inline onclick handlers
1648
+ // ==========================================================================
1649
+
1650
+ function openReqPanel(reqId) { TraceView.panel.open(reqId); }
1651
+ function closeReqCard(reqId) { TraceView.panel.close(reqId); }
1652
+ function closeAllCards() { TraceView.panel.closeAll(); }
1653
+ function openCodeViewer(filePath, lineNum) { TraceView.codeViewer.open(filePath, lineNum); }
1654
+ function closeCodeViewer() { TraceView.codeViewer.close(); }
1655
+ function openLegendModal() { TraceView.legend.open(); }
1656
+ function closeLegendModal() { TraceView.legend.close(); }
1657
+ function toggleEditMode() { TraceView.editMode.toggle(); }
1658
+ function addPendingMove(reqId, sourceFile, moveType) { TraceView.editMode.addMove(reqId, sourceFile, moveType); }
1659
+ function removePendingMove(index) { TraceView.editMode.removeMove(index); }
1660
+ function clearPendingMoves() { TraceView.editMode.clearMoves(); }
1661
+ function togglePendingMoves() { TraceView.editMode.togglePendingMoves(); }
1662
+ function applyMoves() { TraceView.editMode.applyMoves(); }
1663
+ function showMoveToFile(reqId, sourceFile) { TraceView.filePicker.show(reqId, sourceFile); }
1664
+ function closeFilePicker() { TraceView.filePicker.close(); }
1665
+ function filterFiles(value) { TraceView.filePicker.filter(value); }
1666
+ function selectFile(filename) { TraceView.filePicker.select(filename); }
1667
+ function confirmFilePicker() { TraceView.filePicker.confirm(); }
1668
+ function toggleLeafOnly() { TraceView.toggleLeafOnly(); }
1669
+ function toggleIncludeDeprecated() { TraceView.toggleIncludeDeprecated(); }
1670
+ function toggleIncludeRoadmap() { TraceView.toggleIncludeRoadmap(); }
1671
+ function toggleReviewMode() { TraceView.toggleReviewMode(); }
1672
+
1673
+ // Navigation functions
1674
+ function toggleRequirement(element) { TraceView.navigation.toggleRequirement(element); }
1675
+ function expandAll() { TraceView.navigation.expandAll(); }
1676
+ function collapseAll() { TraceView.navigation.collapseAll(); }
1677
+ function switchView(viewMode) { TraceView.navigation.switchView(viewMode); }
1678
+ function applyFilters() { TraceView.applyFilters(); }
1679
+ function clearFilters() { TraceView.clearFilters(); }
1680
+
1681
+ // Toggle filters - independent on/off toggles for levels and repos
1682
+ // Track hidden items - everything starts visible, clicking hides them
1683
+ TraceView.hiddenLevels = TraceView.hiddenLevels || new Set();
1684
+ TraceView.hiddenRepos = TraceView.hiddenRepos || new Set();
1685
+
1686
+ // Toggle level filter (PRD/OPS/DEV) - independent on/off
1687
+ function filterByLevel(level) {
1688
+ const badge = document.getElementById('badge' + level);
1689
+ if (!badge) return;
1690
+
1691
+ if (TraceView.hiddenLevels.has(level)) {
1692
+ // Currently hidden, show it
1693
+ TraceView.hiddenLevels.delete(level);
1694
+ badge.classList.remove('filter-hidden');
1695
+ } else {
1696
+ // Currently visible, hide it
1697
+ TraceView.hiddenLevels.add(level);
1698
+ badge.classList.add('filter-hidden');
1699
+ }
1700
+ applyFilters();
1701
+ }
1702
+
1703
+ // Toggle repo filter (CORE, CAL, TTN, etc.) - independent on/off
1704
+ function toggleRepoFilter(repoPrefix) {
1705
+ const badge = document.getElementById('badgeRepo' + repoPrefix);
1706
+ if (!badge) return;
1707
+
1708
+ if (TraceView.hiddenRepos.has(repoPrefix)) {
1709
+ // Currently hidden, show it
1710
+ TraceView.hiddenRepos.delete(repoPrefix);
1711
+ badge.classList.remove('filter-hidden');
1712
+ } else {
1713
+ // Currently visible, hide it
1714
+ TraceView.hiddenRepos.add(repoPrefix);
1715
+ badge.classList.add('filter-hidden');
1716
+ }
1717
+ applyFilters();
1718
+ }
1719
+
1720
+ // Toggle files filter - show/hide implementation files
1721
+ TraceView.hideFiles = false; // Files visible by default
1722
+
1723
+ function toggleFilesFilter() {
1724
+ const badge = document.getElementById('badgeFiles');
1725
+ if (!badge) return;
1726
+
1727
+ TraceView.hideFiles = !TraceView.hideFiles;
1728
+ if (TraceView.hideFiles) {
1729
+ badge.classList.add('filter-hidden');
1730
+ } else {
1731
+ badge.classList.remove('filter-hidden');
1732
+ }
1733
+ applyFilters();
1734
+ }
1735
+
1736
+ // Initialize on DOM ready
1737
+ document.addEventListener('DOMContentLoaded', function() {
1738
+ TraceView.init();
1739
+ // Start with hierarchical view - show tree structure with collapsible nodes
1740
+ TraceView.navigation.switchView('hierarchy');
1741
+ });