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,928 @@
1
+ /**
2
+ * TraceView Review Comment UI Module
3
+ *
4
+ * User interface for comment threads:
5
+ * - Thread rendering (collapsible)
6
+ * - Comment form (new thread, reply)
7
+ * - Resolve/unresolve actions
8
+ * - Position selection UI
9
+ * - Click-to-highlight positions
10
+ *
11
+ * IMPLEMENTS REQUIREMENTS:
12
+ * REQ-tv-d00016: Review JavaScript Integration
13
+ * REQ-d00092: Click-to-Highlight Positions
14
+ * REQ-d00087: Position Resolution with Fallback
15
+ */
16
+
17
+ // Ensure TraceView.review namespace exists
18
+ window.TraceView = window.TraceView || {};
19
+ TraceView.review = TraceView.review || {};
20
+
21
+ (function(review) {
22
+ 'use strict';
23
+
24
+ // ==========================================================================
25
+ // Templates
26
+ // ==========================================================================
27
+
28
+ /**
29
+ * Create thread list container HTML
30
+ * REQ-d00099: Support read-only mode for archived packages
31
+ * @param {string} reqId - Requirement ID
32
+ * @returns {string} HTML
33
+ */
34
+ function threadListTemplate(reqId) {
35
+ const isArchiveMode = review.packages && review.packages.isArchiveMode;
36
+ const addButtonHtml = isArchiveMode
37
+ ? `<span class="rs-readonly-notice" title="Archived packages are read-only">Read Only</span>`
38
+ : `<button class="rs-btn rs-btn-primary rs-add-comment-btn" title="Add comment">+ Add Comment</button>`;
39
+
40
+ return `
41
+ <div class="rs-thread-list${isArchiveMode ? ' rs-archive-mode' : ''}" data-req-id="${reqId}">
42
+ <div class="rs-thread-list-header">
43
+ <h4>Comments</h4>
44
+ ${addButtonHtml}
45
+ </div>
46
+ <div class="rs-thread-list-content">
47
+ <div class="rs-threads"></div>
48
+ <div class="rs-no-threads" style="display: none;">
49
+ No comments yet.
50
+ </div>
51
+ </div>
52
+ </div>
53
+ `;
54
+ }
55
+
56
+ /**
57
+ * Create thread HTML
58
+ * REQ-d00099-C: Hide action buttons in archive mode
59
+ * @param {Thread} thread - Thread object
60
+ * @returns {string} HTML
61
+ */
62
+ function threadTemplate(thread) {
63
+ const resolvedClass = thread.resolved ? 'rs-thread-resolved' : '';
64
+ const resolvedBadge = thread.resolved ?
65
+ `<span class="rs-badge rs-badge-resolved">Resolved</span>` : '';
66
+ const confidenceClass = getConfidenceClass(thread);
67
+ const isArchiveMode = review.packages && review.packages.isArchiveMode;
68
+
69
+ // REQ-d00099-C: No action buttons in archive mode
70
+ const actionButtonsHtml = isArchiveMode ? '' : `
71
+ ${thread.resolved ?
72
+ `<button class="rs-btn rs-btn-sm rs-unresolve-btn">Reopen</button>` :
73
+ `<button class="rs-btn rs-btn-sm rs-resolve-btn">Resolve</button>`
74
+ }
75
+ `;
76
+
77
+ // REQ-d00099-C: No reply form in archive mode
78
+ const replyHtml = isArchiveMode ? '' : `
79
+ <div class="rs-reply-form" style="display: none;">
80
+ <textarea class="rs-reply-input" placeholder="Write a reply..."></textarea>
81
+ <div class="rs-reply-actions">
82
+ <button class="rs-btn rs-btn-primary rs-submit-reply">Reply</button>
83
+ <button class="rs-btn rs-cancel-reply">Cancel</button>
84
+ </div>
85
+ </div>
86
+ <button class="rs-btn rs-btn-link rs-show-reply-btn">Reply</button>
87
+ `;
88
+
89
+ return `
90
+ <div class="rs-thread ${resolvedClass}${isArchiveMode ? ' rs-archive-mode' : ''}" data-thread-id="${thread.threadId}">
91
+ <div class="rs-thread-header">
92
+ <div class="rs-thread-meta">
93
+ <span class="rs-position-label ${confidenceClass}"
94
+ data-thread-id="${thread.threadId}"
95
+ data-position-type="${thread.position?.type || 'general'}"
96
+ title="Click to highlight in REQ... click again to clear">
97
+ ${getPositionIcon(thread)} ${getPositionLabel(thread)}
98
+ </span>
99
+ ${resolvedBadge}
100
+ </div>
101
+ <div class="rs-thread-actions">
102
+ ${actionButtonsHtml}
103
+ <button class="rs-btn rs-btn-sm rs-collapse-btn" title="Collapse">V</button>
104
+ </div>
105
+ </div>
106
+ <div class="rs-thread-body">
107
+ <div class="rs-comments">
108
+ ${thread.comments.map(c => commentTemplate(c)).join('')}
109
+ </div>
110
+ ${replyHtml}
111
+ </div>
112
+ </div>
113
+ `;
114
+ }
115
+
116
+ /**
117
+ * Create comment HTML
118
+ * @param {Comment} comment - Comment object
119
+ * @returns {string} HTML
120
+ */
121
+ function commentTemplate(comment) {
122
+ return `
123
+ <div class="rs-comment" data-comment-id="${comment.id}">
124
+ <div class="rs-comment-header">
125
+ <span class="rs-author">${escapeHtml(comment.author)}</span>
126
+ <span class="rs-time">${formatTime(comment.timestamp)}</span>
127
+ </div>
128
+ <div class="rs-comment-body">
129
+ ${formatCommentBody(comment.body)}
130
+ </div>
131
+ </div>
132
+ `;
133
+ }
134
+
135
+ /**
136
+ * Create new comment form HTML
137
+ * @param {string} reqId - Requirement ID
138
+ * @returns {string} HTML
139
+ */
140
+ function newCommentFormTemplate(reqId) {
141
+ return `
142
+ <div class="rs-new-comment-form" data-req-id="${reqId}">
143
+ <h4>New Comment</h4>
144
+ <div class="rs-form-group">
145
+ <label>Position</label>
146
+ <select class="rs-position-type">
147
+ <option value="general">General (whole requirement)</option>
148
+ <option value="line">Specific line</option>
149
+ <option value="block">Line range</option>
150
+ <option value="word">Word/phrase</option>
151
+ </select>
152
+ </div>
153
+ <div class="rs-position-options" style="display: none;">
154
+ <div class="rs-line-options" style="display: none;">
155
+ <label>Line number</label>
156
+ <input type="number" class="rs-line-input" min="1" value="1">
157
+ </div>
158
+ <div class="rs-block-options" style="display: none;">
159
+ <label>Line range</label>
160
+ <input type="number" class="rs-block-start" min="1" value="1">
161
+ <span>to</span>
162
+ <input type="number" class="rs-block-end" min="1" value="1">
163
+ </div>
164
+ <div class="rs-word-options" style="display: none;">
165
+ <label>Word/phrase</label>
166
+ <input type="text" class="rs-keyword" placeholder="Enter word or phrase">
167
+ <label>Occurrence</label>
168
+ <input type="number" class="rs-keyword-occurrence" min="1" value="1">
169
+ </div>
170
+ </div>
171
+ <div class="rs-form-group">
172
+ <label>Comment</label>
173
+ <textarea class="rs-comment-body-input"
174
+ placeholder="Write your comment..." rows="4"></textarea>
175
+ </div>
176
+ <div class="rs-form-actions">
177
+ <button class="rs-btn rs-btn-primary rs-submit-comment">Add Comment</button>
178
+ <button class="rs-btn rs-cancel-comment">Cancel</button>
179
+ </div>
180
+ </div>
181
+ `;
182
+ }
183
+
184
+ // ==========================================================================
185
+ // Helper Functions
186
+ // ==========================================================================
187
+
188
+ function escapeHtml(text) {
189
+ const div = document.createElement('div');
190
+ div.textContent = text;
191
+ return div.innerHTML;
192
+ }
193
+
194
+ function formatTime(isoString) {
195
+ try {
196
+ const date = new Date(isoString);
197
+ const now = new Date();
198
+ const diff = now - date;
199
+
200
+ if (diff < 60000) return 'just now';
201
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
202
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
203
+ if (diff < 604800000) return Math.floor(diff / 86400000) + 'd ago';
204
+
205
+ return date.toLocaleDateString();
206
+ } catch (e) {
207
+ return isoString;
208
+ }
209
+ }
210
+
211
+ function formatCommentBody(body) {
212
+ // Simple markdown-like formatting
213
+ let html = escapeHtml(body);
214
+ // Bold
215
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
216
+ // Italic
217
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
218
+ // Code
219
+ html = html.replace(/`(.+?)`/g, '<code>$1</code>');
220
+ // Line breaks
221
+ html = html.replace(/\n/g, '<br>');
222
+ return html;
223
+ }
224
+
225
+ /**
226
+ * Get confidence class for position label styling
227
+ * REQ-d00092: Click-to-Highlight Positions
228
+ * REQ-d00087: Position Resolution with Fallback
229
+ * @param {Thread} thread - Thread object
230
+ * @returns {string} CSS class name
231
+ */
232
+ function getConfidenceClass(thread) {
233
+ const resolvedPosition = thread.resolvedPosition;
234
+ if (!resolvedPosition || thread.position?.type === 'general') {
235
+ return 'rs-confidence-unanchored';
236
+ }
237
+ const confidence = resolvedPosition.confidence;
238
+ if (confidence === 'EXACT') return 'rs-confidence-exact';
239
+ if (confidence === 'APPROXIMATE') return 'rs-confidence-approximate';
240
+ return 'rs-confidence-unanchored';
241
+ }
242
+
243
+ /**
244
+ * Get highlight class for line highlighting based on confidence
245
+ * REQ-d00092: Click-to-Highlight Positions
246
+ * REQ-d00087: Position Resolution with Fallback
247
+ * @param {Thread} thread - Thread object
248
+ * @returns {string} CSS class name for highlighting
249
+ */
250
+ function getHighlightClassForThread(thread) {
251
+ const resolvedPosition = thread.resolvedPosition;
252
+ if (!resolvedPosition || thread.position?.type === 'general') {
253
+ return 'rs-highlight-unanchored';
254
+ }
255
+ const confidence = resolvedPosition.confidence;
256
+ if (confidence === 'EXACT') return 'rs-highlight-exact';
257
+ if (confidence === 'APPROXIMATE') return 'rs-highlight-approximate';
258
+ return 'rs-highlight-unanchored';
259
+ }
260
+
261
+ function getPositionIcon(thread) {
262
+ switch (thread.position.type) {
263
+ case review.PositionType.LINE: return '[L]';
264
+ case review.PositionType.BLOCK: return '[B]';
265
+ case review.PositionType.WORD: return '[W]';
266
+ default: return '[G]';
267
+ }
268
+ }
269
+
270
+ function getPositionTooltip(thread) {
271
+ const pos = thread.position;
272
+ switch (pos.type) {
273
+ case review.PositionType.LINE:
274
+ return `Line ${pos.lineNumber}`;
275
+ case review.PositionType.BLOCK:
276
+ return `Lines ${pos.lineRange[0]}-${pos.lineRange[1]}`;
277
+ case review.PositionType.WORD:
278
+ return `"${pos.keyword}" (occurrence ${pos.keywordOccurrence || 1})`;
279
+ default:
280
+ return 'General comment';
281
+ }
282
+ }
283
+
284
+ function getPositionLabel(thread) {
285
+ const pos = thread.position;
286
+ switch (pos.type) {
287
+ case review.PositionType.LINE:
288
+ return `Line ${pos.lineNumber}`;
289
+ case review.PositionType.BLOCK:
290
+ return `Lines ${pos.lineRange[0]}-${pos.lineRange[1]}`;
291
+ case review.PositionType.WORD:
292
+ return `"${escapeHtml(pos.keyword)}"`;
293
+ default:
294
+ return 'General';
295
+ }
296
+ }
297
+
298
+ // ==========================================================================
299
+ // UI Components
300
+ // ==========================================================================
301
+
302
+ /**
303
+ * Render thread list for a requirement
304
+ * @param {Element} container - Container element
305
+ * @param {string} reqId - Requirement ID
306
+ */
307
+ function renderThreadList(container, reqId) {
308
+ container.innerHTML = threadListTemplate(reqId);
309
+
310
+ const threads = review.state.getThreads(reqId);
311
+ const threadsContainer = container.querySelector('.rs-threads');
312
+ const noThreads = container.querySelector('.rs-no-threads');
313
+
314
+ if (threads.length === 0) {
315
+ noThreads.style.display = 'block';
316
+ } else {
317
+ threads.forEach(thread => {
318
+ threadsContainer.insertAdjacentHTML('beforeend', threadTemplate(thread));
319
+ });
320
+ bindThreadEvents(container);
321
+ }
322
+
323
+ // Bind add comment button
324
+ const addBtn = container.querySelector('.rs-add-comment-btn');
325
+ if (addBtn) {
326
+ addBtn.addEventListener('click', (event) => {
327
+ // Stop propagation to prevent any parent handlers from clearing selection
328
+ event.stopPropagation();
329
+ showNewCommentForm(container, reqId);
330
+ });
331
+ }
332
+ }
333
+ review.renderThreadList = renderThreadList;
334
+
335
+ /**
336
+ * Show new comment form
337
+ * REQ-d00099: Block in archive mode
338
+ * @param {Element} container - Container element (optional - uses current selection if not provided)
339
+ * @param {string} reqId - Requirement ID (optional - uses current selection if not provided)
340
+ */
341
+ function showNewCommentForm(container, reqId) {
342
+ // REQ-d00099-C: Block comment creation in archive mode
343
+ if (review.packages && review.packages.isArchiveMode) {
344
+ alert('This package is archived and read-only.\n\nComments cannot be added to archived packages.');
345
+ return;
346
+ }
347
+
348
+ // IMPORTANT: Capture line selection state FIRST, before any DOM manipulation
349
+ // This prevents loss of selection due to focus changes or event handling
350
+ const lineSelection = review.getLineSelection ? review.getLineSelection() : {
351
+ type: window.selectedLineRange ? 'block' : (window.selectedLineNumber ? 'line' : null),
352
+ lineNumber: window.selectedLineNumber,
353
+ lineRange: window.selectedLineRange
354
+ };
355
+ // Make a copy of the values in case they get cleared
356
+ const capturedSelection = {
357
+ type: lineSelection.type,
358
+ lineNumber: lineSelection.lineNumber,
359
+ lineRange: lineSelection.lineRange ? { ...lineSelection.lineRange } : null
360
+ };
361
+
362
+ // If called without arguments, get from current review state
363
+ if (!reqId) {
364
+ reqId = review.selectedReqId;
365
+ if (!reqId) {
366
+ console.warn('showNewCommentForm: No REQ selected');
367
+ return;
368
+ }
369
+ }
370
+ if (!container) {
371
+ // Find the comments section for the current REQ
372
+ container = document.querySelector('.rs-comments-section[data-req-id="' + reqId + '"]') ||
373
+ document.querySelector('.rs-thread-list[data-req-id="' + reqId + '"]') ||
374
+ document.getElementById('rs-comments-content');
375
+ if (!container) {
376
+ console.warn('showNewCommentForm: No container found');
377
+ return;
378
+ }
379
+ }
380
+
381
+ // Check if form already exists
382
+ let form = container.querySelector('.rs-new-comment-form');
383
+ if (form) {
384
+ form.remove();
385
+ }
386
+
387
+ container.insertAdjacentHTML('afterbegin', newCommentFormTemplate(reqId));
388
+ form = container.querySelector('.rs-new-comment-form');
389
+
390
+ // Position type change handler
391
+ const posType = form.querySelector('.rs-position-type');
392
+ const posOptions = form.querySelector('.rs-position-options');
393
+ const lineOpts = form.querySelector('.rs-line-options');
394
+ const blockOpts = form.querySelector('.rs-block-options');
395
+ const wordOpts = form.querySelector('.rs-word-options');
396
+
397
+ posType.addEventListener('change', () => {
398
+ const val = posType.value;
399
+ posOptions.style.display = val === 'general' ? 'none' : 'block';
400
+ lineOpts.style.display = val === 'line' ? 'block' : 'none';
401
+ blockOpts.style.display = val === 'block' ? 'block' : 'none';
402
+ wordOpts.style.display = val === 'word' ? 'block' : 'none';
403
+ });
404
+
405
+ // Use the captured selection (captured at function start before any DOM changes)
406
+ if (capturedSelection.lineRange && capturedSelection.lineRange.start && capturedSelection.lineRange.end) {
407
+ // Range selection - lineRange is {start, end} object
408
+ posType.value = 'block';
409
+ posType.dispatchEvent(new Event('change'));
410
+ const startInput = form.querySelector('.rs-block-start');
411
+ const endInput = form.querySelector('.rs-block-end');
412
+ if (startInput) startInput.value = capturedSelection.lineRange.start;
413
+ if (endInput) endInput.value = capturedSelection.lineRange.end;
414
+ } else if (capturedSelection.lineNumber) {
415
+ // Single line selection
416
+ posType.value = 'line';
417
+ posType.dispatchEvent(new Event('change'));
418
+ const lineInput = form.querySelector('.rs-line-input');
419
+ if (lineInput) lineInput.value = capturedSelection.lineNumber;
420
+ }
421
+
422
+ // Submit handler
423
+ form.querySelector('.rs-submit-comment').addEventListener('click', () => {
424
+ submitNewComment(form, reqId);
425
+ });
426
+
427
+ // Cancel handler
428
+ form.querySelector('.rs-cancel-comment').addEventListener('click', () => {
429
+ form.remove();
430
+ });
431
+
432
+ // Focus textarea
433
+ form.querySelector('.rs-comment-body-input').focus();
434
+ }
435
+ review.showNewCommentForm = showNewCommentForm;
436
+
437
+ /**
438
+ * Submit new comment
439
+ * REQ-d00094: Threads must be owned by a package
440
+ * @param {Element} form - Form element
441
+ * @param {string} reqId - Requirement ID
442
+ */
443
+ function submitNewComment(form, reqId) {
444
+ // REQ-d00095-B: Require explicit package selection
445
+ const activePackageId = review.packages && review.packages.activeId;
446
+ if (!activePackageId) {
447
+ alert('Please select a package first.\n\nThreads must be owned by a package.');
448
+ return;
449
+ }
450
+
451
+ // REQ-d00095-B: Verify the active package actually exists
452
+ const activePackage = review.packages.items.find(p => p.packageId === activePackageId);
453
+ if (!activePackage) {
454
+ console.error('Active package not found in packages list:', activePackageId);
455
+ alert('The selected package no longer exists.\n\nPlease select a different package.');
456
+ // Clear the stale activePackageId
457
+ review.packages.activeId = null;
458
+ return;
459
+ }
460
+
461
+ const body = form.querySelector('.rs-comment-body-input').value.trim();
462
+ if (!body) {
463
+ alert('Please enter a comment');
464
+ return;
465
+ }
466
+
467
+ const user = review.state.currentUser || 'anonymous';
468
+ const posType = form.querySelector('.rs-position-type').value;
469
+
470
+ // Get current REQ hash (would come from embedded data)
471
+ const hash = window.REQ_CONTENT_DATA?.[reqId]?.hash || '00000000';
472
+
473
+ // Create position based on type
474
+ let position;
475
+ switch (posType) {
476
+ case 'line': {
477
+ const lineNum = parseInt(form.querySelector('.rs-line-input').value, 10);
478
+ position = review.CommentPosition.createLine(hash, lineNum);
479
+ break;
480
+ }
481
+ case 'block': {
482
+ const start = parseInt(form.querySelector('.rs-block-start').value, 10);
483
+ const end = parseInt(form.querySelector('.rs-block-end').value, 10);
484
+ position = review.CommentPosition.createBlock(hash, start, end);
485
+ break;
486
+ }
487
+ case 'word': {
488
+ const keyword = form.querySelector('.rs-keyword').value.trim();
489
+ const occurrence = parseInt(form.querySelector('.rs-keyword-occurrence').value, 10);
490
+ if (!keyword) {
491
+ alert('Please enter a word or phrase');
492
+ return;
493
+ }
494
+ position = review.CommentPosition.createWord(hash, keyword, occurrence);
495
+ break;
496
+ }
497
+ default:
498
+ position = review.CommentPosition.createGeneral(hash);
499
+ }
500
+
501
+ // REQ-d00094-A: Create thread with packageId
502
+ const thread = review.Thread.create(reqId, user, position, body, activePackageId);
503
+ review.state.addThread(thread);
504
+
505
+ // Auto-change status to Review if currently Draft
506
+ const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
507
+ if (reqData && reqData.status === 'Draft' && typeof review.toggleToReview === 'function') {
508
+ review.toggleToReview(reqId).then(result => {
509
+ if (result.success) {
510
+ console.log(`Auto-changed REQ-${reqId} status to Review`);
511
+ }
512
+ }).catch(err => {
513
+ console.warn('Failed to auto-change status:', err);
514
+ });
515
+ }
516
+
517
+ // Trigger change event
518
+ document.dispatchEvent(new CustomEvent('traceview:thread-created', {
519
+ detail: { thread, reqId }
520
+ }));
521
+
522
+ // Re-render the thread list
523
+ // The form is inside #review-panel-content, find the thread list's parent container
524
+ const threadList = form.closest('.rs-thread-list') ||
525
+ form.parentElement?.querySelector('.rs-thread-list');
526
+ const reviewPanelContent = document.getElementById('review-panel-content');
527
+
528
+ if (threadList && threadList.parentElement) {
529
+ renderThreadList(threadList.parentElement, reqId);
530
+ } else if (reviewPanelContent) {
531
+ // Form is directly in review-panel-content, re-render there
532
+ renderThreadList(reviewPanelContent, reqId);
533
+ } else {
534
+ form.remove();
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Bind event handlers to thread elements
540
+ * @param {Element} container - Container element
541
+ */
542
+ function bindThreadEvents(container) {
543
+ // Collapse/expand buttons
544
+ container.querySelectorAll('.rs-collapse-btn').forEach(btn => {
545
+ btn.addEventListener('click', () => {
546
+ const thread = btn.closest('.rs-thread');
547
+ const body = thread.querySelector('.rs-thread-body');
548
+ const isCollapsed = body.style.display === 'none';
549
+ body.style.display = isCollapsed ? 'block' : 'none';
550
+ btn.textContent = isCollapsed ? 'V' : '>';
551
+ });
552
+ });
553
+
554
+ // Resolve buttons
555
+ container.querySelectorAll('.rs-resolve-btn').forEach(btn => {
556
+ btn.addEventListener('click', () => {
557
+ const threadEl = btn.closest('.rs-thread');
558
+ const threadId = threadEl.getAttribute('data-thread-id');
559
+ resolveThread(threadId, container);
560
+ });
561
+ });
562
+
563
+ // Unresolve buttons
564
+ container.querySelectorAll('.rs-unresolve-btn').forEach(btn => {
565
+ btn.addEventListener('click', () => {
566
+ const threadEl = btn.closest('.rs-thread');
567
+ const threadId = threadEl.getAttribute('data-thread-id');
568
+ unresolveThread(threadId, container);
569
+ });
570
+ });
571
+
572
+ // Reply buttons
573
+ container.querySelectorAll('.rs-show-reply-btn').forEach(btn => {
574
+ btn.addEventListener('click', () => {
575
+ const thread = btn.closest('.rs-thread');
576
+ const replyForm = thread.querySelector('.rs-reply-form');
577
+ replyForm.style.display = 'block';
578
+ btn.style.display = 'none';
579
+ replyForm.querySelector('.rs-reply-input').focus();
580
+ });
581
+ });
582
+
583
+ // Cancel reply
584
+ container.querySelectorAll('.rs-cancel-reply').forEach(btn => {
585
+ btn.addEventListener('click', () => {
586
+ const thread = btn.closest('.rs-thread');
587
+ const replyForm = thread.querySelector('.rs-reply-form');
588
+ const showBtn = thread.querySelector('.rs-show-reply-btn');
589
+ replyForm.style.display = 'none';
590
+ replyForm.querySelector('.rs-reply-input').value = '';
591
+ showBtn.style.display = 'inline-block';
592
+ });
593
+ });
594
+
595
+ // Submit reply
596
+ container.querySelectorAll('.rs-submit-reply').forEach(btn => {
597
+ btn.addEventListener('click', () => {
598
+ const threadEl = btn.closest('.rs-thread');
599
+ submitReply(threadEl, container);
600
+ });
601
+ });
602
+
603
+ // REQ-d00092: Position label click handler with toggle behavior
604
+ container.querySelectorAll('.rs-position-label').forEach(positionLabel => {
605
+ positionLabel.addEventListener('click', (e) => {
606
+ e.stopPropagation();
607
+
608
+ const threadId = positionLabel.getAttribute('data-thread-id');
609
+ const isActive = positionLabel.classList.contains('rs-position-active');
610
+
611
+ // Clear all other active position labels
612
+ document.querySelectorAll('.rs-position-label.rs-position-active').forEach(el => {
613
+ el.classList.remove('rs-position-active');
614
+ });
615
+
616
+ if (isActive) {
617
+ // Toggle off - clear highlights
618
+ clearAllPositionHighlights();
619
+ const reqCard = document.querySelector(`[data-req-id]`);
620
+ if (reqCard) {
621
+ const lineContainer = reqCard.querySelector('.rs-lines-table');
622
+ clearCommentHighlights(lineContainer);
623
+ }
624
+ } else {
625
+ // Toggle on - highlight and mark active
626
+ positionLabel.classList.add('rs-position-active');
627
+ highlightThreadPositionInCard(threadId, container);
628
+ }
629
+ });
630
+ });
631
+
632
+ // Hover to highlight position
633
+ container.querySelectorAll('.rs-thread').forEach(threadEl => {
634
+ threadEl.addEventListener('mouseenter', () => {
635
+ const threadId = threadEl.getAttribute('data-thread-id');
636
+ review.activateHighlight(threadId);
637
+ });
638
+ threadEl.addEventListener('mouseleave', () => {
639
+ review.activateHighlight(null);
640
+ });
641
+
642
+ // Click to highlight position in REQ card
643
+ threadEl.addEventListener('click', (e) => {
644
+ // Don't trigger if clicking on buttons or reply form
645
+ if (e.target.closest('button') || e.target.closest('.rs-reply-form') ||
646
+ e.target.closest('textarea') || e.target.closest('input')) {
647
+ return;
648
+ }
649
+ const threadId = threadEl.getAttribute('data-thread-id');
650
+ highlightThreadPositionInCard(threadId, container);
651
+ });
652
+ });
653
+ }
654
+
655
+ /**
656
+ * Highlight the position referenced by a thread in the REQ card
657
+ * REQ-d00092: Click-to-Highlight Positions
658
+ * @param {string} threadId - Thread ID
659
+ * @param {Element} container - Container element
660
+ */
661
+ function highlightThreadPositionInCard(threadId, container) {
662
+ // Get the reqId and find the thread
663
+ const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
664
+ container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
665
+ container.getAttribute('data-req-id') ||
666
+ (typeof currentReviewReqId !== 'undefined' ? currentReviewReqId : null);
667
+
668
+ if (!reqId) return;
669
+
670
+ const threads = review.state.getThreads(reqId);
671
+ const thread = threads.find(t => t.threadId === threadId);
672
+ if (!thread || !thread.position) return;
673
+
674
+ const position = thread.position;
675
+
676
+ // REQ-d00092: Get confidence-based highlight class
677
+ const highlightClass = getHighlightClassForThread(thread);
678
+
679
+ // Find the REQ card's line-numbered view
680
+ const reqCard = document.getElementById(`req-card-${reqId}`);
681
+ if (!reqCard) return;
682
+
683
+ const lineContainer = reqCard.querySelector('.rs-lines-table');
684
+
685
+ // Clear any existing highlights
686
+ clearAllPositionHighlights();
687
+ if (lineContainer) {
688
+ clearCommentHighlights(lineContainer);
689
+ }
690
+
691
+ // REQ-d00092: For GENERAL position type, highlight the whole card
692
+ if (position.type === 'general' || position.type === review.PositionType?.GENERAL) {
693
+ reqCard.classList.add('rs-card-highlight');
694
+ reqCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
695
+ return;
696
+ }
697
+
698
+ if (!lineContainer) return;
699
+
700
+ // Highlight based on position type
701
+ let linesToHighlight = [];
702
+
703
+ if (position.type === review.PositionType.LINE && position.lineNumber) {
704
+ linesToHighlight = [position.lineNumber];
705
+ } else if (position.type === review.PositionType.BLOCK && position.lineRange) {
706
+ const [start, end] = position.lineRange;
707
+ for (let i = start; i <= end; i++) {
708
+ linesToHighlight.push(i);
709
+ }
710
+ } else if (position.type === review.PositionType.WORD && position.keyword) {
711
+ // For word positions, try to find the line containing the keyword
712
+ const reqData = window.REQ_CONTENT_DATA && window.REQ_CONTENT_DATA[reqId];
713
+ if (reqData && reqData.body) {
714
+ const foundLine = review.findKeywordOccurrence(
715
+ reqData.body,
716
+ position.keyword,
717
+ position.keywordOccurrence || 1
718
+ );
719
+ if (foundLine) {
720
+ linesToHighlight = [foundLine.line];
721
+ }
722
+ }
723
+ }
724
+
725
+ // Apply highlights and scroll to first highlighted line
726
+ if (linesToHighlight.length > 0) {
727
+ let firstRow = null;
728
+ linesToHighlight.forEach(lineNum => {
729
+ const lineRow = lineContainer.querySelector(`.rs-line-row[data-line="${lineNum}"]`);
730
+ if (lineRow) {
731
+ lineRow.classList.add('rs-comment-highlight');
732
+ lineRow.classList.add(highlightClass); // REQ-d00092: Add confidence class
733
+ lineRow.setAttribute('data-highlight-thread', threadId); // REQ-d00092: Track thread
734
+ if (!firstRow) firstRow = lineRow;
735
+ }
736
+ });
737
+
738
+ // Scroll the first highlighted line into view
739
+ if (firstRow) {
740
+ firstRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
741
+ }
742
+ }
743
+ }
744
+ review.highlightThreadPositionInCard = highlightThreadPositionInCard;
745
+
746
+ /**
747
+ * Clear all position highlights (card-level highlights for GENERAL position)
748
+ * REQ-d00092: Click-to-Highlight Positions
749
+ * REQ-d00087: Position Resolution with Fallback
750
+ */
751
+ function clearAllPositionHighlights() {
752
+ // Clear card-level highlights (for GENERAL position)
753
+ document.querySelectorAll('.rs-card-highlight').forEach(el => {
754
+ el.classList.remove('rs-card-highlight');
755
+ });
756
+ // REQ-d00092: Clear highlight classes from req cards
757
+ document.querySelectorAll('[data-req-id]').forEach(reqCard => {
758
+ reqCard.classList.remove('rs-highlight-unanchored', 'rs-card-highlight');
759
+ });
760
+ }
761
+ review.clearAllPositionHighlights = clearAllPositionHighlights;
762
+
763
+ /**
764
+ * Clear comment highlights from line container
765
+ * REQ-d00092: Enhanced to remove all highlight-related classes and attributes
766
+ * @param {Element} lineContainer - The lines table element
767
+ */
768
+ function clearCommentHighlights(lineContainer) {
769
+ if (!lineContainer) return;
770
+ // Remove all highlight-related classes and data attributes
771
+ lineContainer.querySelectorAll('.rs-comment-highlight, .rs-highlight-exact, .rs-highlight-approximate, .rs-highlight-unanchored, .rs-highlight-active').forEach(el => {
772
+ el.classList.remove('rs-comment-highlight', 'rs-highlight-exact', 'rs-highlight-approximate', 'rs-highlight-unanchored', 'rs-highlight-active');
773
+ el.removeAttribute('data-highlight-thread');
774
+ });
775
+ }
776
+ review.clearCommentHighlights = clearCommentHighlights;
777
+
778
+ /**
779
+ * Submit reply to a thread
780
+ * @param {Element} threadEl - Thread element
781
+ * @param {Element} container - Container element
782
+ */
783
+ function submitReply(threadEl, container) {
784
+ const threadId = threadEl.getAttribute('data-thread-id');
785
+ const replyInput = threadEl.querySelector('.rs-reply-input');
786
+ const body = replyInput.value.trim();
787
+
788
+ if (!body) {
789
+ alert('Please enter a reply');
790
+ return;
791
+ }
792
+
793
+ const user = review.state.currentUser || 'anonymous';
794
+ // Look for data-req-id in the container or its children (thread-list element)
795
+ const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
796
+ container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
797
+ container.getAttribute('data-req-id');
798
+
799
+ // Find thread in state
800
+ if (reqId) {
801
+ const threads = review.state.getThreads(reqId);
802
+ const thread = threads.find(t => t.threadId === threadId);
803
+ if (thread) {
804
+ thread.addComment(user, body);
805
+
806
+ // Trigger change event
807
+ document.dispatchEvent(new CustomEvent('traceview:comment-added', {
808
+ detail: { thread, reqId, body }
809
+ }));
810
+
811
+ // Re-render - find the proper container
812
+ const threadListEl = container.querySelector('.rs-thread-list') || container;
813
+ const renderTarget = threadListEl.parentElement || container;
814
+ renderThreadList(renderTarget, reqId);
815
+ }
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Resolve a thread
821
+ * @param {string} threadId - Thread ID
822
+ * @param {Element} container - Container element
823
+ */
824
+ function resolveThread(threadId, container) {
825
+ const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
826
+ container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
827
+ container.getAttribute('data-req-id');
828
+ const user = review.state.currentUser || 'anonymous';
829
+
830
+ if (reqId) {
831
+ const threads = review.state.getThreads(reqId);
832
+ const thread = threads.find(t => t.threadId === threadId);
833
+ if (thread) {
834
+ thread.resolve(user);
835
+
836
+ // Trigger event
837
+ document.dispatchEvent(new CustomEvent('traceview:thread-resolved', {
838
+ detail: { thread, reqId, user }
839
+ }));
840
+
841
+ // Re-render - find the proper container
842
+ const threadListEl = container.querySelector('.rs-thread-list') || container;
843
+ const renderTarget = threadListEl.parentElement || container;
844
+ renderThreadList(renderTarget, reqId);
845
+ }
846
+ }
847
+ }
848
+
849
+ /**
850
+ * Unresolve a thread
851
+ * @param {string} threadId - Thread ID
852
+ * @param {Element} container - Container element
853
+ */
854
+ function unresolveThread(threadId, container) {
855
+ const reqId = container.querySelector('[data-req-id]')?.getAttribute('data-req-id') ||
856
+ container.closest('[data-req-id]')?.getAttribute('data-req-id') ||
857
+ container.getAttribute('data-req-id');
858
+
859
+ if (reqId) {
860
+ const threads = review.state.getThreads(reqId);
861
+ const thread = threads.find(t => t.threadId === threadId);
862
+ if (thread) {
863
+ thread.unresolve();
864
+
865
+ // Trigger event
866
+ document.dispatchEvent(new CustomEvent('traceview:thread-unresolved', {
867
+ detail: { thread, reqId }
868
+ }));
869
+
870
+ // Re-render - find the proper container
871
+ const threadListEl = container.querySelector('.rs-thread-list') || container;
872
+ const renderTarget = threadListEl.parentElement || container;
873
+ renderThreadList(renderTarget, reqId);
874
+ }
875
+ }
876
+ }
877
+
878
+ /**
879
+ * Get comment count for a requirement
880
+ * @param {string} reqId - Requirement ID
881
+ * @returns {Object} {total, unresolved}
882
+ */
883
+ function getCommentCount(reqId) {
884
+ const threads = review.state.getThreads(reqId);
885
+ return {
886
+ total: threads.length,
887
+ unresolved: threads.filter(t => !t.resolved).length
888
+ };
889
+ }
890
+ review.getCommentCount = getCommentCount;
891
+
892
+ // ==========================================================================
893
+ // Review Panel Integration
894
+ // ==========================================================================
895
+
896
+ /**
897
+ * Handle review panel ready event - add comments section
898
+ * @param {CustomEvent} event - Event with reqId and sectionsContainer
899
+ */
900
+ function handleReviewPanelReady(event) {
901
+ const { reqId, sectionsContainer } = event.detail;
902
+ if (!sectionsContainer) return;
903
+
904
+ // Create comments section
905
+ const commentsSection = document.createElement('div');
906
+ commentsSection.className = 'rs-comments-section';
907
+ commentsSection.setAttribute('data-req-id', reqId);
908
+ sectionsContainer.appendChild(commentsSection);
909
+
910
+ // Render thread list
911
+ renderThreadList(commentsSection, reqId);
912
+ }
913
+
914
+ // Register event listener
915
+ document.addEventListener('traceview:review-panel-ready', handleReviewPanelReady);
916
+
917
+ // ==========================================================================
918
+ // RS Namespace Exports (REQ-d00092: Test Accessibility)
919
+ // ==========================================================================
920
+ // Export functions to RS namespace for test access
921
+ if (typeof window.RS === 'undefined') {
922
+ window.RS = {};
923
+ }
924
+ RS.highlightThreadPositionInCard = highlightThreadPositionInCard;
925
+ RS.clearAllPositionHighlights = clearAllPositionHighlights;
926
+ RS.clearCommentHighlights = clearCommentHighlights;
927
+
928
+ })(TraceView.review);