elspais 0.9.3__py3-none-any.whl → 0.11.1__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 +141 -10
  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.1.dist-info}/METADATA +36 -18
  69. elspais-0.11.1.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.1.dist-info}/WHEEL +0 -0
  72. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/entry_points.txt +0 -0
  73. {elspais-0.9.3.dist-info → elspais-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1029 @@
1
+ /**
2
+ * TraceView Review Packages UI Module
3
+ *
4
+ * Provides UI for managing review packages:
5
+ * - Display collapsible packages panel
6
+ * - Create, edit, delete packages
7
+ * - Select active package for filtering
8
+ * - Add/remove REQs from packages
9
+ *
10
+ * IMPLEMENTS REQUIREMENTS:
11
+ * REQ-tv-d00016: Review JavaScript Integration
12
+ *
13
+ * =============================================================================
14
+ * FILTERING ARCHITECTURE
15
+ * =============================================================================
16
+ *
17
+ * This codebase uses a unified filter orchestrator (applyAllFilters) that
18
+ * coordinates multiple filtering mechanisms:
19
+ *
20
+ * CSS CLASSES:
21
+ * - filtered-out: View filter exclusion (display: none !important)
22
+ * - package-filtered: Package filter exclusion (display: none !important)
23
+ * - collapsed-by-parent: Hierarchy collapse (display: none)
24
+ * - in-active-package, in-other-package, not-in-package: Styling only
25
+ *
26
+ * HIERARCHY PROMOTION MODEL:
27
+ * Items are siblings with data-parent-instance-id (NOT DOM-nested).
28
+ * When filtering is active, matching items are "promoted" by removing
29
+ * collapsed-by-parent, making them visible regardless of parent state.
30
+ *
31
+ * FILTER PRECEDENCE (all ANDed):
32
+ * 1. View filters (filtered-out) - text inputs, dropdowns, checkboxes
33
+ * 2. Package filter (package-filtered) - "Show only Package REQs" toggle
34
+ * 3. Collapse state (collapsed-by-parent) - overridden by promotion when filtering
35
+ *
36
+ * ENTRY POINT:
37
+ * applyAllFilters() in generate_traceability.py - call this when ANY filter changes.
38
+ * Both applyFilters() and applyPackageContext() delegate to applyAllFilters().
39
+ *
40
+ * SINGLE RESPONSIBILITY:
41
+ * - computeViewFilterState(): Computes which items match view filters
42
+ * - computePackageFilterState(): Computes which items match package filter
43
+ * - applyViewFilterClasses(): Manages filtered-out class ONLY
44
+ * - applyPackageFilterClasses(): Manages package-* classes ONLY
45
+ * - applyPromotion(): Removes collapsed-by-parent for promoted items (ONE PLACE)
46
+ * - updateFilteredChildrenIcons(): Updates collapse icons (called ONCE)
47
+ * =============================================================================
48
+ */
49
+
50
+ (function() {
51
+ 'use strict';
52
+
53
+ // Initialize TraceView.review if not exists
54
+ window.TraceView = window.TraceView || {};
55
+ TraceView.review = TraceView.review || { state: {} };
56
+ const review = TraceView.review;
57
+
58
+ // Package state
59
+ // REQ-d00095-B: No automatic "default" package - packages must be explicitly created
60
+ // REQ-d00099: Archive state for viewing archived packages
61
+ review.packages = {
62
+ items: [],
63
+ archivedItems: [], // REQ-d00099: Archived packages list
64
+ activeId: null,
65
+ panelExpanded: true,
66
+ archivePanelExpanded: false, // REQ-d00099: Archive viewer collapse state
67
+ filterEnabled: false, // Whether "Show only Package REQs" is active
68
+ isArchiveMode: false // REQ-d00099: Whether viewing archived package (read-only)
69
+ };
70
+
71
+ // ==========================================================================
72
+ // Toast Notification
73
+ // ==========================================================================
74
+
75
+ let toastElement = null;
76
+
77
+ /**
78
+ * Show a toast notification positioned near the packages panel
79
+ */
80
+ function showToast(message, showSpinner = false) {
81
+ if (!toastElement) {
82
+ toastElement = document.createElement('div');
83
+ toastElement.className = 'rs-toast';
84
+ document.body.appendChild(toastElement);
85
+ }
86
+
87
+ toastElement.innerHTML = showSpinner
88
+ ? `<div class="rs-toast-spinner"></div><span>${message}</span>`
89
+ : `<span>${message}</span>`;
90
+
91
+ // Position near the packages panel header
92
+ const packagesPanel = document.getElementById('reviewPackagesPanel');
93
+ if (packagesPanel) {
94
+ const rect = packagesPanel.getBoundingClientRect();
95
+ toastElement.style.top = `${rect.top + window.scrollY + 8}px`;
96
+ toastElement.style.left = `${rect.left + rect.width / 2}px`;
97
+ toastElement.style.transform = 'translateX(-50%) scale(0.9)';
98
+ } else {
99
+ // Fallback to fixed position
100
+ toastElement.style.position = 'fixed';
101
+ toastElement.style.top = '80px';
102
+ toastElement.style.left = '50%';
103
+ toastElement.style.transform = 'translateX(-50%) scale(0.9)';
104
+ }
105
+
106
+ // Force reflow then show
107
+ toastElement.offsetHeight;
108
+ toastElement.classList.add('visible');
109
+ if (packagesPanel) {
110
+ toastElement.style.transform = 'translateX(-50%) scale(1)';
111
+ } else {
112
+ toastElement.style.transform = 'translateX(-50%) scale(1)';
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Hide the toast notification
118
+ */
119
+ function hideToast() {
120
+ if (toastElement) {
121
+ toastElement.classList.remove('visible');
122
+ }
123
+ }
124
+
125
+ // ==========================================================================
126
+ // API Functions
127
+ // ==========================================================================
128
+
129
+ /**
130
+ * Fetch all packages from the API
131
+ * REQ-d00095-B: No default package - packages must be explicitly created
132
+ */
133
+ async function fetchPackages() {
134
+ try {
135
+ const response = await fetch('/api/reviews/packages');
136
+ if (!response.ok) {
137
+ throw new Error(`HTTP ${response.status}`);
138
+ }
139
+ const data = await response.json();
140
+ review.packages.items = data.packages || [];
141
+ review.packages.activeId = data.activePackageId || null;
142
+
143
+ // REQ-d00095-B: Validate activePackageId exists in packages list
144
+ if (review.packages.activeId) {
145
+ const exists = review.packages.items.some(p => p.packageId === review.packages.activeId);
146
+ if (!exists) {
147
+ console.warn('Stale activePackageId detected, clearing:', review.packages.activeId);
148
+ const staleId = review.packages.activeId;
149
+ review.packages.activeId = null;
150
+ // Persist the cleanup via API (async, don't await)
151
+ fetch('/api/reviews/packages/active', {
152
+ method: 'PUT',
153
+ headers: { 'Content-Type': 'application/json' },
154
+ body: JSON.stringify({ packageId: null, user: 'system' })
155
+ }).catch(err => console.error('Failed to clear stale activePackageId:', staleId, err));
156
+ }
157
+ }
158
+
159
+ console.log('Packages loaded:', {
160
+ count: review.packages.items.length,
161
+ activeId: review.packages.activeId,
162
+ packages: review.packages.items.map(p => ({
163
+ id: p.packageId,
164
+ name: p.name,
165
+ reqCount: (p.reqIds || []).length
166
+ }))
167
+ });
168
+
169
+ return review.packages;
170
+ } catch (error) {
171
+ console.error('Failed to fetch packages:', error);
172
+ return { items: [], activeId: null };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Fetch archived packages from the API
178
+ * REQ-d00099: Read-only access to archived packages
179
+ */
180
+ async function fetchArchivedPackages() {
181
+ try {
182
+ const response = await fetch('/api/reviews/archive');
183
+ if (!response.ok) {
184
+ throw new Error(`HTTP ${response.status}`);
185
+ }
186
+ const data = await response.json();
187
+ review.packages.archivedItems = data.packages || [];
188
+
189
+ console.log('Archived packages loaded:', {
190
+ count: review.packages.archivedItems.length,
191
+ packages: review.packages.archivedItems.map(p => ({
192
+ id: p.packageId,
193
+ name: p.name,
194
+ archiveReason: p.archiveReason
195
+ }))
196
+ });
197
+
198
+ return review.packages.archivedItems;
199
+ } catch (error) {
200
+ console.error('Failed to fetch archived packages:', error);
201
+ return [];
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Create a new package
207
+ */
208
+ async function createPackage(name, description) {
209
+ const user = review.state.currentUser || 'anonymous';
210
+ try {
211
+ const response = await fetch('/api/reviews/packages', {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ name, description, user })
215
+ });
216
+
217
+ if (!response.ok) {
218
+ throw new Error(`HTTP ${response.status}`);
219
+ }
220
+
221
+ const result = await response.json();
222
+ if (result.success) {
223
+ await fetchPackages();
224
+ renderPackagesPanel();
225
+ }
226
+ return result;
227
+ } catch (error) {
228
+ console.error('Failed to create package:', error);
229
+ return { success: false, error: error.message };
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Update a package's name or description
235
+ */
236
+ async function updatePackage(packageId, updates) {
237
+ try {
238
+ const response = await fetch(`/api/reviews/packages/${packageId}`, {
239
+ method: 'PUT',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify(updates)
242
+ });
243
+
244
+ if (!response.ok) {
245
+ throw new Error(`HTTP ${response.status}`);
246
+ }
247
+
248
+ const result = await response.json();
249
+ if (result.success) {
250
+ await fetchPackages();
251
+ renderPackagesPanel();
252
+ }
253
+ return result;
254
+ } catch (error) {
255
+ console.error('Failed to update package:', error);
256
+ return { success: false, error: error.message };
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Delete a package (archives it per REQ-d00097-E)
262
+ * REQ-d00095-F: Package deletion SHALL archive (not destroy) the package
263
+ */
264
+ async function deletePackage(packageId) {
265
+ try {
266
+ const response = await fetch(`/api/reviews/packages/${packageId}`, {
267
+ method: 'DELETE'
268
+ });
269
+
270
+ if (!response.ok) {
271
+ throw new Error(`HTTP ${response.status}`);
272
+ }
273
+
274
+ const result = await response.json();
275
+ if (result.success) {
276
+ await fetchPackages();
277
+ await fetchArchivedPackages(); // REQ-d00097: Deleted packages go to archive
278
+ renderPackagesPanel();
279
+ }
280
+ return result;
281
+ } catch (error) {
282
+ console.error('Failed to delete package:', error);
283
+ return { success: false, error: error.message };
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Manually archive a package
289
+ * REQ-d00097-D: Manual archive action
290
+ */
291
+ async function archivePackage(packageId, reason = 'manual') {
292
+ const user = review.state.currentUser || 'anonymous';
293
+ try {
294
+ const response = await fetch(`/api/reviews/packages/${packageId}/archive`, {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ reason, user })
298
+ });
299
+
300
+ if (!response.ok) {
301
+ throw new Error(`HTTP ${response.status}`);
302
+ }
303
+
304
+ const result = await response.json();
305
+ if (result.success) {
306
+ showToast(`Package archived (${reason})`);
307
+ await fetchPackages();
308
+ await fetchArchivedPackages();
309
+ renderPackagesPanel();
310
+ }
311
+ return result;
312
+ } catch (error) {
313
+ console.error('Failed to archive package:', error);
314
+ return { success: false, error: error.message };
315
+ }
316
+ }
317
+
318
+ /**
319
+ * View an archived package (read-only mode)
320
+ * REQ-d00099-B: Archived packages open in read-only mode
321
+ */
322
+ async function viewArchivedPackage(packageId) {
323
+ try {
324
+ const response = await fetch(`/api/reviews/archive/${packageId}`);
325
+ if (!response.ok) {
326
+ throw new Error(`HTTP ${response.status}`);
327
+ }
328
+
329
+ const pkg = await response.json();
330
+ review.packages.isArchiveMode = true;
331
+ review.packages.viewingArchivedPackage = pkg;
332
+
333
+ // Trigger event for UI updates
334
+ document.dispatchEvent(new CustomEvent('traceview:archive-view-opened', {
335
+ detail: { package: pkg }
336
+ }));
337
+
338
+ // Render archive viewer
339
+ renderArchiveViewer(pkg);
340
+ return pkg;
341
+ } catch (error) {
342
+ console.error('Failed to view archived package:', error);
343
+ return null;
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Close archive viewer and exit read-only mode
349
+ */
350
+ function closeArchiveViewer() {
351
+ review.packages.isArchiveMode = false;
352
+ review.packages.viewingArchivedPackage = null;
353
+
354
+ // Hide archive viewer
355
+ const viewer = document.getElementById('archiveViewer');
356
+ if (viewer) {
357
+ viewer.style.display = 'none';
358
+ }
359
+
360
+ document.dispatchEvent(new CustomEvent('traceview:archive-view-closed'));
361
+ }
362
+
363
+ /**
364
+ * Set the active package and switch to its git branch
365
+ */
366
+ async function setActivePackage(packageId) {
367
+ const user = review.state.currentUser || 'anonymous';
368
+
369
+ // Show toast when switching to a package (not when selecting None)
370
+ if (packageId) {
371
+ showToast('Syncing with GitHub...', true);
372
+ }
373
+
374
+ try {
375
+ // 1. Set the active package in packages.json
376
+ const response = await fetch('/api/reviews/packages/active', {
377
+ method: 'PUT',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify({ packageId, user })
380
+ });
381
+
382
+ if (!response.ok) {
383
+ throw new Error(`HTTP ${response.status}`);
384
+ }
385
+
386
+ const result = await response.json();
387
+ if (!result.success) {
388
+ hideToast();
389
+ return result;
390
+ }
391
+
392
+ review.packages.activeId = packageId;
393
+
394
+ // 2. Switch to package branch (creates branch if needed)
395
+ if (packageId) {
396
+ await switchToPackageBranch(packageId, user);
397
+ }
398
+
399
+ // 3. Re-render panel to update radio buttons and highlights
400
+ renderPackagesPanel();
401
+
402
+ // 4. Apply context styling
403
+ applyPackageFilter();
404
+
405
+ // 5. Update git sync indicator to show new branch
406
+ if (review.updateGitSyncIndicator) {
407
+ review.updateGitSyncIndicator();
408
+ }
409
+
410
+ // Toast is hidden in fetchConsolidatedPackageData after sync completes
411
+ // But if no packageId, hide it now
412
+ if (!packageId) {
413
+ hideToast();
414
+ }
415
+
416
+ return result;
417
+ } catch (error) {
418
+ console.error('Failed to set active package:', error);
419
+ hideToast();
420
+ return { success: false, error: error.message };
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Switch to a package branch for the current user
426
+ */
427
+ async function switchToPackageBranch(packageId, user) {
428
+ try {
429
+ const response = await fetch('/api/reviews/packages/switch', {
430
+ method: 'POST',
431
+ headers: { 'Content-Type': 'application/json' },
432
+ body: JSON.stringify({ packageId, user })
433
+ });
434
+
435
+ if (!response.ok) {
436
+ throw new Error(`HTTP ${response.status}`);
437
+ }
438
+
439
+ const result = await response.json();
440
+ if (result.success) {
441
+ console.log(`Switched to branch: ${result.branch}`);
442
+ review.packages.currentBranch = result.branch;
443
+
444
+ // Re-fetch packages to get updated reqIds from new branch
445
+ await fetchPackages();
446
+
447
+ // Fetch consolidated data from all package branches
448
+ await fetchConsolidatedPackageData();
449
+ } else {
450
+ // Branch switch failed, hide the toast
451
+ hideToast();
452
+ }
453
+ return result;
454
+ } catch (error) {
455
+ console.error('Failed to switch to package branch:', error);
456
+ hideToast();
457
+ return { success: false, error: error.message };
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Fetch consolidated review data from all users' branches for current package
463
+ */
464
+ async function fetchConsolidatedPackageData() {
465
+ // Toast already shown by setActivePackage
466
+
467
+ try {
468
+ const response = await fetch('/api/reviews/sync/fetch-all-package', {
469
+ method: 'POST'
470
+ });
471
+
472
+ if (!response.ok) {
473
+ throw new Error(`HTTP ${response.status}`);
474
+ }
475
+
476
+ const data = await response.json();
477
+ review.packages.contributors = data.contributors || [];
478
+
479
+ // If there's merged thread data, update the state
480
+ if (data.threads && Object.keys(data.threads).length > 0) {
481
+ console.log(`Loaded threads from ${data.contributors.length} contributor(s)`);
482
+ // Trigger refresh event so UI updates
483
+ document.dispatchEvent(new CustomEvent('traceview:data-fetched', {
484
+ detail: { data, timestamp: new Date() }
485
+ }));
486
+ }
487
+
488
+ hideToast();
489
+ return data;
490
+ } catch (error) {
491
+ console.error('Failed to fetch consolidated package data:', error);
492
+ hideToast();
493
+ return { threads: {}, flags: {}, contributors: [] };
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Get package contributors (users who have branches for this package)
499
+ */
500
+ async function getPackageContributors(packageId) {
501
+ try {
502
+ const response = await fetch(`/api/reviews/packages/${packageId}/contributors`);
503
+ if (!response.ok) {
504
+ throw new Error(`HTTP ${response.status}`);
505
+ }
506
+ const data = await response.json();
507
+ return data.contributors || [];
508
+ } catch (error) {
509
+ console.error('Failed to get package contributors:', error);
510
+ return [];
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Get current package context from git branch
516
+ */
517
+ async function getCurrentPackageContext() {
518
+ try {
519
+ const response = await fetch('/api/reviews/context');
520
+ if (!response.ok) {
521
+ throw new Error(`HTTP ${response.status}`);
522
+ }
523
+ const data = await response.json();
524
+ return data; // { packageId, user, branch } or null
525
+ } catch (error) {
526
+ console.error('Failed to get package context:', error);
527
+ return null;
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Add a REQ to a package
533
+ */
534
+ async function addReqToPackage(packageId, reqId) {
535
+ try {
536
+ const response = await fetch(`/api/reviews/packages/${packageId}/reqs/${reqId}`, {
537
+ method: 'POST'
538
+ });
539
+
540
+ if (!response.ok) {
541
+ throw new Error(`HTTP ${response.status}`);
542
+ }
543
+
544
+ const result = await response.json();
545
+ if (result.success) {
546
+ // Update local state
547
+ const pkg = review.packages.items.find(p => p.packageId === packageId);
548
+ if (pkg && !pkg.reqIds.includes(reqId)) {
549
+ pkg.reqIds.push(reqId);
550
+ }
551
+ renderPackagesPanel();
552
+ }
553
+ return result;
554
+ } catch (error) {
555
+ console.error('Failed to add REQ to package:', error);
556
+ return { success: false, error: error.message };
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Remove a REQ from a package
562
+ */
563
+ async function removeReqFromPackage(packageId, reqId) {
564
+ try {
565
+ const response = await fetch(`/api/reviews/packages/${packageId}/reqs/${reqId}`, {
566
+ method: 'DELETE'
567
+ });
568
+
569
+ if (!response.ok) {
570
+ throw new Error(`HTTP ${response.status}`);
571
+ }
572
+
573
+ const result = await response.json();
574
+ if (result.success) {
575
+ // Update local state
576
+ const pkg = review.packages.items.find(p => p.packageId === packageId);
577
+ if (pkg) {
578
+ pkg.reqIds = pkg.reqIds.filter(id => id !== reqId);
579
+ }
580
+ renderPackagesPanel();
581
+ }
582
+ return result;
583
+ } catch (error) {
584
+ console.error('Failed to remove REQ from package:', error);
585
+ return { success: false, error: error.message };
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Add REQ to active package
591
+ * REQ-d00095-B: Requires explicit package selection (no default)
592
+ */
593
+ async function addReqToActivePackage(reqId) {
594
+ const packageId = review.packages.activeId;
595
+ if (!packageId) {
596
+ console.warn('addReqToActivePackage: No package selected. Select a package first.');
597
+ showToast('Please select a package first');
598
+ return { success: false, error: 'No package selected' };
599
+ }
600
+ return addReqToPackage(packageId, reqId);
601
+ }
602
+
603
+ // ==========================================================================
604
+ // UI Functions
605
+ // ==========================================================================
606
+
607
+ /**
608
+ * Render the packages panel
609
+ * REQ-d00095-B: No default package concept
610
+ * REQ-d00099: Include archive viewer section
611
+ */
612
+ function renderPackagesPanel() {
613
+ const panel = document.getElementById('reviewPackagesPanel');
614
+ if (!panel) return;
615
+
616
+ const packagesContent = panel.querySelector('.packages-content');
617
+ if (!packagesContent) return;
618
+
619
+ const items = review.packages.items;
620
+ const archivedItems = review.packages.archivedItems;
621
+ const activeId = review.packages.activeId;
622
+
623
+ // Build package list HTML
624
+ let html = '<div class="package-list">';
625
+
626
+ // "None" option (show all REQs)
627
+ html += `
628
+ <label class="package-item${!activeId ? ' active' : ''}">
629
+ <input type="radio" name="activePackage" value=""
630
+ ${!activeId ? 'checked' : ''}
631
+ onchange="TraceView.review.setActivePackage(null)">
632
+ <span class="package-info">
633
+ <span class="package-name">None (Show All)</span>
634
+ <span class="package-desc">No package filter applied</span>
635
+ </span>
636
+ </label>
637
+ `;
638
+
639
+ // Package items - REQ-d00095-B: All packages are explicit (no default)
640
+ for (const pkg of items) {
641
+ const isActive = pkg.packageId === activeId;
642
+ const reqCount = pkg.reqIds ? pkg.reqIds.length : 0;
643
+ const reqIds = pkg.reqIds || [];
644
+
645
+ // Build REQ list HTML for active package
646
+ let reqListHtml = '';
647
+ if (isActive && reqIds.length > 0) {
648
+ reqListHtml = `
649
+ <div class="package-req-list">
650
+ ${reqIds.map(reqId => `
651
+ <div class="package-req-item" data-req-id="${reqId}">
652
+ <span class="package-req-id" onclick="TraceView.panel.open('${reqId}')" title="View REQ-${reqId}">REQ-${reqId}</span>
653
+ <button class="rs-btn rs-btn-sm rs-btn-danger package-req-remove"
654
+ onclick="TraceView.review.removeReqFromPackage('${pkg.packageId}', '${reqId}'); event.stopPropagation();"
655
+ title="Remove from package">&times;</button>
656
+ </div>
657
+ `).join('')}
658
+ </div>
659
+ `;
660
+ }
661
+
662
+ html += `
663
+ <div class="package-item-wrapper${isActive ? ' active' : ''}">
664
+ <label class="package-item${isActive ? ' active' : ''}">
665
+ <input type="radio" name="activePackage" value="${pkg.packageId}"
666
+ ${isActive ? 'checked' : ''}
667
+ onchange="TraceView.review.setActivePackage('${pkg.packageId}')">
668
+ <span class="package-info">
669
+ <span class="package-name">${escapeHtml(pkg.name)}</span>
670
+ <span class="package-desc">${escapeHtml(pkg.description || '')}</span>
671
+ </span>
672
+ <span class="package-count">${reqCount}</span>
673
+ <span class="package-actions">
674
+ <button class="rs-btn rs-btn-sm" onclick="TraceView.review.editPackageDialog('${pkg.packageId}', event)" title="Edit">
675
+ &#9998;
676
+ </button>
677
+ <button class="rs-btn rs-btn-sm" onclick="TraceView.review.confirmArchivePackage('${pkg.packageId}', event)" title="Archive">
678
+ &#128451;
679
+ </button>
680
+ <button class="rs-btn rs-btn-sm rs-btn-danger" onclick="TraceView.review.confirmDeletePackage('${pkg.packageId}', event)" title="Delete (archives)">
681
+ &times;
682
+ </button>
683
+ </span>
684
+ </label>
685
+ ${reqListHtml}
686
+ </div>
687
+ `;
688
+ }
689
+
690
+ html += '</div>';
691
+
692
+ // REQ-d00099: Archive viewer section
693
+ if (archivedItems.length > 0) {
694
+ const archiveExpanded = review.packages.archivePanelExpanded;
695
+ html += `
696
+ <div class="archive-section">
697
+ <div class="archive-header" onclick="TraceView.review.toggleArchivePanel()">
698
+ <span class="collapse-icon">${archiveExpanded ? '▼' : '▶'}</span>
699
+ <h4>Archived Packages (${archivedItems.length})</h4>
700
+ </div>
701
+ <div class="archive-list" style="${archiveExpanded ? '' : 'display: none;'}">
702
+ ${archivedItems.map(pkg => `
703
+ <div class="archive-item" onclick="TraceView.review.viewArchivedPackage('${pkg.packageId}')">
704
+ <span class="archive-item-name">${escapeHtml(pkg.name)}</span>
705
+ <span class="archive-item-reason rs-badge rs-badge-${pkg.archiveReason || 'manual'}">${pkg.archiveReason || 'archived'}</span>
706
+ <span class="archive-item-date" title="${pkg.archivedAt || ''}">${formatArchiveDate(pkg.archivedAt)}</span>
707
+ </div>
708
+ `).join('')}
709
+ </div>
710
+ </div>
711
+ `;
712
+ }
713
+
714
+ packagesContent.innerHTML = html;
715
+ }
716
+
717
+ /**
718
+ * Format archive date for display
719
+ */
720
+ function formatArchiveDate(isoDate) {
721
+ if (!isoDate) return '';
722
+ try {
723
+ const date = new Date(isoDate);
724
+ return date.toLocaleDateString();
725
+ } catch (e) {
726
+ return '';
727
+ }
728
+ }
729
+
730
+ /**
731
+ * Toggle archive panel expansion
732
+ * REQ-d00099: Archive viewer
733
+ */
734
+ function toggleArchivePanel() {
735
+ review.packages.archivePanelExpanded = !review.packages.archivePanelExpanded;
736
+ renderPackagesPanel();
737
+ }
738
+
739
+ /**
740
+ * Render archive viewer for a specific archived package
741
+ * REQ-d00099: Read-only archive viewer
742
+ */
743
+ function renderArchiveViewer(pkg) {
744
+ let viewer = document.getElementById('archiveViewer');
745
+ if (!viewer) {
746
+ viewer = document.createElement('div');
747
+ viewer.id = 'archiveViewer';
748
+ viewer.className = 'archive-viewer';
749
+ document.body.appendChild(viewer);
750
+ }
751
+
752
+ // REQ-d00099-D: Display archival metadata
753
+ // REQ-d00099-E: Display git audit trail information
754
+ viewer.innerHTML = `
755
+ <div class="archive-viewer-overlay" onclick="TraceView.review.closeArchiveViewer()"></div>
756
+ <div class="archive-viewer-content">
757
+ <div class="archive-viewer-header">
758
+ <h3>${escapeHtml(pkg.name)} <span class="archive-badge">ARCHIVED</span></h3>
759
+ <button class="rs-btn rs-btn-link" onclick="TraceView.review.closeArchiveViewer()">&times;</button>
760
+ </div>
761
+ <div class="archive-viewer-meta">
762
+ <div class="archive-meta-row">
763
+ <span class="archive-meta-label">Archive Reason:</span>
764
+ <span class="archive-meta-value rs-badge rs-badge-${pkg.archiveReason || 'manual'}">${pkg.archiveReason || 'archived'}</span>
765
+ </div>
766
+ <div class="archive-meta-row">
767
+ <span class="archive-meta-label">Archived At:</span>
768
+ <span class="archive-meta-value">${pkg.archivedAt || 'Unknown'}</span>
769
+ </div>
770
+ <div class="archive-meta-row">
771
+ <span class="archive-meta-label">Archived By:</span>
772
+ <span class="archive-meta-value">${escapeHtml(pkg.archivedBy || 'Unknown')}</span>
773
+ </div>
774
+ ${pkg.branchName ? `
775
+ <div class="archive-meta-row">
776
+ <span class="archive-meta-label">Branch:</span>
777
+ <span class="archive-meta-value">${escapeHtml(pkg.branchName)}</span>
778
+ </div>
779
+ ` : ''}
780
+ ${pkg.creationCommitHash ? `
781
+ <div class="archive-meta-row">
782
+ <span class="archive-meta-label">Creation Commit:</span>
783
+ <span class="archive-meta-value commit-hash" title="May not exist if branch was squash-merged">${pkg.creationCommitHash.substring(0, 7)}</span>
784
+ </div>
785
+ ` : ''}
786
+ ${pkg.lastReviewedCommitHash ? `
787
+ <div class="archive-meta-row">
788
+ <span class="archive-meta-label">Last Reviewed Commit:</span>
789
+ <span class="archive-meta-value commit-hash" title="May not exist if branch was squash-merged">${pkg.lastReviewedCommitHash.substring(0, 7)}</span>
790
+ </div>
791
+ ` : ''}
792
+ </div>
793
+ <div class="archive-viewer-body">
794
+ <p class="archive-readonly-notice">This package is archived and read-only. Comments cannot be added or modified.</p>
795
+ ${pkg.description ? `<p class="archive-description">${escapeHtml(pkg.description)}</p>` : ''}
796
+ <div class="archive-reqs">
797
+ <h4>Requirements (${(pkg.reqIds || []).length})</h4>
798
+ <div class="archive-req-list">
799
+ ${(pkg.reqIds || []).map(reqId => `
800
+ <span class="archive-req-badge">REQ-${reqId}</span>
801
+ `).join(' ')}
802
+ </div>
803
+ </div>
804
+ </div>
805
+ <div class="archive-viewer-footer">
806
+ <button class="rs-btn" onclick="TraceView.review.closeArchiveViewer()">Close</button>
807
+ </div>
808
+ </div>
809
+ `;
810
+
811
+ viewer.style.display = 'flex';
812
+ }
813
+
814
+ /**
815
+ * Toggle packages panel expansion
816
+ */
817
+ function togglePackagesPanel() {
818
+ const panel = document.getElementById('reviewPackagesPanel');
819
+ if (!panel) return;
820
+
821
+ review.packages.panelExpanded = !review.packages.panelExpanded;
822
+ panel.classList.toggle('collapsed', !review.packages.panelExpanded);
823
+
824
+ const icon = panel.querySelector('.collapse-icon');
825
+ if (icon) {
826
+ icon.textContent = review.packages.panelExpanded ? '\u25BC' : '\u25B6';
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Show create package dialog
832
+ */
833
+ function showCreatePackageDialog(event) {
834
+ if (event) event.stopPropagation();
835
+
836
+ const name = prompt('Package name:');
837
+ if (!name || !name.trim()) return;
838
+
839
+ const description = prompt('Package description (optional):') || '';
840
+ createPackage(name.trim(), description.trim());
841
+ }
842
+
843
+ /**
844
+ * Show edit package dialog
845
+ */
846
+ function editPackageDialog(packageId, event) {
847
+ if (event) event.stopPropagation();
848
+
849
+ const pkg = review.packages.items.find(p => p.packageId === packageId);
850
+ if (!pkg) return;
851
+
852
+ const name = prompt('Package name:', pkg.name);
853
+ if (!name || !name.trim()) return;
854
+
855
+ const description = prompt('Package description:', pkg.description || '');
856
+ updatePackage(packageId, {
857
+ name: name.trim(),
858
+ description: description ? description.trim() : ''
859
+ });
860
+ }
861
+
862
+ /**
863
+ * Confirm and delete package (archives it per REQ-d00095-F)
864
+ */
865
+ function confirmDeletePackage(packageId, event) {
866
+ if (event) event.stopPropagation();
867
+
868
+ const pkg = review.packages.items.find(p => p.packageId === packageId);
869
+ if (!pkg) return;
870
+
871
+ if (confirm(`Delete package "${pkg.name}"?\n\nThis will archive the package (not destroy it). REQs will not be deleted.`)) {
872
+ deletePackage(packageId);
873
+ }
874
+ }
875
+
876
+ /**
877
+ * Confirm and archive a package manually
878
+ * REQ-d00097-D: Manual archive action
879
+ */
880
+ function confirmArchivePackage(packageId, event) {
881
+ if (event) event.stopPropagation();
882
+
883
+ const pkg = review.packages.items.find(p => p.packageId === packageId);
884
+ if (!pkg) return;
885
+
886
+ if (confirm(`Archive package "${pkg.name}"?\n\nArchived packages are read-only but preserved for audit purposes.`)) {
887
+ archivePackage(packageId, 'manual');
888
+ }
889
+ }
890
+
891
+ /**
892
+ * Apply package context and optional filtering to the requirement tree.
893
+ * Context (activeId) determines which package new REQs are added to.
894
+ * Filter (filterEnabled) determines whether to hide non-package REQs.
895
+ *
896
+ * NOTE: This function delegates to the unified applyAllFilters() orchestrator
897
+ * which handles all filtering (view filters, package filters, and hierarchy promotion)
898
+ * in a single, consistent pass. See generate_traceability.py for the implementation.
899
+ */
900
+ function applyPackageContext() {
901
+ // Update context indicator UI
902
+ updateContextIndicator(review.packages.activeId);
903
+
904
+ // Delegate all filtering to the unified orchestrator
905
+ // This handles: view filters, package filters, hierarchy promotion, icon updates, and stats
906
+ if (typeof applyAllFilters === 'function') {
907
+ applyAllFilters();
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Update context indicator in UI
913
+ */
914
+ function updateContextIndicator(activeId) {
915
+ const indicator = document.getElementById('packageContextIndicator');
916
+ if (!indicator) return;
917
+
918
+ if (activeId) {
919
+ const pkg = review.packages.items.find(p => p.packageId === activeId);
920
+ const name = pkg ? pkg.name : 'Unknown';
921
+ indicator.textContent = `Context: ${name}`;
922
+ } else {
923
+ indicator.textContent = 'Context: Default';
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Toggle the package filter on/off
929
+ */
930
+ function togglePackageFilter(event) {
931
+ if (event) event.stopPropagation();
932
+
933
+ review.packages.filterEnabled = !review.packages.filterEnabled;
934
+
935
+ // Update toggle button styling
936
+ const toggle = document.getElementById('packageFilterToggle');
937
+ if (toggle) {
938
+ toggle.classList.toggle('active', review.packages.filterEnabled);
939
+ }
940
+
941
+ // Re-apply filtering
942
+ applyPackageContext();
943
+ }
944
+
945
+ // Alias for backwards compatibility
946
+ function applyPackageFilter() {
947
+ applyPackageContext();
948
+ }
949
+
950
+ /**
951
+ * Escape HTML special characters
952
+ */
953
+ function escapeHtml(text) {
954
+ const div = document.createElement('div');
955
+ div.textContent = text;
956
+ return div.innerHTML;
957
+ }
958
+
959
+ /**
960
+ * Initialize packages panel when review mode is activated
961
+ * REQ-d00099: Also fetch archived packages
962
+ */
963
+ async function initPackagesPanel() {
964
+ await Promise.all([
965
+ fetchPackages(),
966
+ fetchArchivedPackages() // REQ-d00099: Load archived packages
967
+ ]);
968
+ renderPackagesPanel();
969
+ applyPackageFilter();
970
+ }
971
+
972
+ // ==========================================================================
973
+ // Export Functions
974
+ // ==========================================================================
975
+
976
+ review.fetchPackages = fetchPackages;
977
+ review.fetchArchivedPackages = fetchArchivedPackages; // REQ-d00099
978
+ review.createPackage = createPackage;
979
+ review.updatePackage = updatePackage;
980
+ review.deletePackage = deletePackage;
981
+ review.archivePackage = archivePackage; // REQ-d00097
982
+ review.viewArchivedPackage = viewArchivedPackage; // REQ-d00099
983
+ review.closeArchiveViewer = closeArchiveViewer; // REQ-d00099
984
+ review.setActivePackage = setActivePackage;
985
+ review.switchToPackageBranch = switchToPackageBranch;
986
+ review.fetchConsolidatedPackageData = fetchConsolidatedPackageData;
987
+ review.getPackageContributors = getPackageContributors;
988
+ review.getCurrentPackageContext = getCurrentPackageContext;
989
+ review.addReqToPackage = addReqToPackage;
990
+ review.removeReqFromPackage = removeReqFromPackage;
991
+ review.addReqToActivePackage = addReqToActivePackage;
992
+ review.renderPackagesPanel = renderPackagesPanel;
993
+ review.togglePackagesPanel = togglePackagesPanel;
994
+ review.toggleArchivePanel = toggleArchivePanel; // REQ-d00099
995
+ review.togglePackageFilter = togglePackageFilter;
996
+ review.showCreatePackageDialog = showCreatePackageDialog;
997
+ review.editPackageDialog = editPackageDialog;
998
+ review.confirmDeletePackage = confirmDeletePackage;
999
+ review.confirmArchivePackage = confirmArchivePackage; // REQ-d00097
1000
+ review.initPackagesPanel = initPackagesPanel;
1001
+ review.applyPackageFilter = applyPackageFilter;
1002
+ review.applyPackageContext = applyPackageContext;
1003
+
1004
+ })();
1005
+
1006
+ // Export to ReviewSystem alias with RS. pattern (REQ-d00092)
1007
+ window.ReviewSystem = window.ReviewSystem || {};
1008
+ var RS = window.ReviewSystem;
1009
+
1010
+ // Initialize packages state on ReviewSystem (REQ-d00092)
1011
+ RS.packages = {
1012
+ items: [],
1013
+ activeId: null,
1014
+ filterEnabled: false
1015
+ };
1016
+
1017
+ // Export package functions
1018
+ RS.renderPackagesPanel = TraceView.review.renderPackagesPanel;
1019
+ RS.togglePackagesPanel = TraceView.review.togglePackagesPanel;
1020
+ RS.togglePackageFilter = TraceView.review.togglePackageFilter;
1021
+ RS.toggleArchivePanel = TraceView.review.toggleArchivePanel; // REQ-d00099
1022
+ RS.showCreatePackageDialog = TraceView.review.showCreatePackageDialog;
1023
+ RS.setActivePackage = TraceView.review.setActivePackage;
1024
+ RS.initPackagesPanel = TraceView.review.initPackagesPanel;
1025
+ RS.applyPackageFilter = TraceView.review.applyPackageFilter;
1026
+ RS.archivePackage = TraceView.review.archivePackage; // REQ-d00097
1027
+ RS.viewArchivedPackage = TraceView.review.viewArchivedPackage; // REQ-d00099
1028
+ RS.closeArchiveViewer = TraceView.review.closeArchiveViewer; // REQ-d00099
1029
+ RS.confirmArchivePackage = TraceView.review.confirmArchivePackage; // REQ-d00097