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,961 @@
1
+ /**
2
+ * TraceView Review Data Module
3
+ *
4
+ * Client-side data structures matching Python models.
5
+ * Provides validation, serialization, and local state management.
6
+ *
7
+ * IMPLEMENTS REQUIREMENTS:
8
+ * REQ-tv-d00016: Review JavaScript Integration
9
+ */
10
+
11
+ // Ensure TraceView.review namespace exists
12
+ window.TraceView = window.TraceView || {};
13
+ TraceView.review = TraceView.review || {};
14
+
15
+ (function(review) {
16
+ 'use strict';
17
+
18
+ // ==========================================================================
19
+ // Constants
20
+ // ==========================================================================
21
+
22
+ review.PositionType = Object.freeze({
23
+ LINE: 'line',
24
+ BLOCK: 'block',
25
+ WORD: 'word',
26
+ GENERAL: 'general'
27
+ });
28
+
29
+ review.RequestState = Object.freeze({
30
+ PENDING: 'pending',
31
+ APPROVED: 'approved',
32
+ REJECTED: 'rejected',
33
+ APPLIED: 'applied'
34
+ });
35
+
36
+ review.ApprovalDecision = Object.freeze({
37
+ APPROVE: 'approve',
38
+ REJECT: 'reject'
39
+ });
40
+
41
+ review.VALID_REQ_STATUSES = ['Draft', 'Active', 'Deprecated'];
42
+
43
+ review.DEFAULT_APPROVAL_RULES = {
44
+ 'Draft->Active': ['product_owner', 'tech_lead'],
45
+ 'Active->Deprecated': ['product_owner'],
46
+ 'Draft->Deprecated': ['product_owner']
47
+ };
48
+
49
+ // ==========================================================================
50
+ // Utility Functions
51
+ // ==========================================================================
52
+
53
+ /**
54
+ * Generate a UUID v4
55
+ * @returns {string} UUID string
56
+ */
57
+ review.generateUuid = function() {
58
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
59
+ const r = Math.random() * 16 | 0;
60
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
61
+ return v.toString(16);
62
+ });
63
+ };
64
+
65
+ /**
66
+ * Get current UTC timestamp in ISO 8601 format
67
+ * @returns {string} ISO timestamp
68
+ */
69
+ review.nowIso = function() {
70
+ return new Date().toISOString();
71
+ };
72
+
73
+ /**
74
+ * Validate REQ ID format
75
+ * @param {string} reqId - Requirement ID to validate
76
+ * @returns {boolean} True if valid
77
+ */
78
+ review.validateReqId = function(reqId) {
79
+ if (!reqId) return false;
80
+ // Match: d00001, p00042, o00003, or CAL-d00001 (sponsor prefix)
81
+ // But NOT REQ-d00001
82
+ const pattern = /^(?!REQ-)(?:[A-Z]{2,4}-)?[pod]\d{5}$/;
83
+ return pattern.test(reqId);
84
+ };
85
+
86
+ /**
87
+ * Validate 8-character hex hash format
88
+ * @param {string} hash - Hash to validate
89
+ * @returns {boolean} True if valid
90
+ */
91
+ review.validateHash = function(hash) {
92
+ if (!hash) return false;
93
+ return /^[a-fA-F0-9]{8}$/.test(hash);
94
+ };
95
+
96
+ /**
97
+ * Normalize REQ ID (remove REQ- prefix, lowercase)
98
+ * @param {string} reqId - Requirement ID
99
+ * @returns {string} Normalized ID
100
+ */
101
+ review.normalizeReqId = function(reqId) {
102
+ if (!reqId) return '';
103
+ if (reqId.toUpperCase().startsWith('REQ-')) {
104
+ reqId = reqId.substring(4);
105
+ }
106
+ return reqId.toLowerCase();
107
+ };
108
+
109
+ // ==========================================================================
110
+ // Data Classes
111
+ // ==========================================================================
112
+
113
+ /**
114
+ * Comment position anchor within a requirement
115
+ */
116
+ class CommentPosition {
117
+ constructor(data) {
118
+ this.type = data.type;
119
+ this.hashWhenCreated = data.hashWhenCreated;
120
+ this.lineNumber = data.lineNumber || null;
121
+ this.lineRange = data.lineRange || null;
122
+ this.keyword = data.keyword || null;
123
+ this.keywordOccurrence = data.keywordOccurrence || null;
124
+ this.fallbackContext = data.fallbackContext || null;
125
+ }
126
+
127
+ static createLine(hash, lineNumber, context) {
128
+ return new CommentPosition({
129
+ type: review.PositionType.LINE,
130
+ hashWhenCreated: hash,
131
+ lineNumber: lineNumber,
132
+ fallbackContext: context || null
133
+ });
134
+ }
135
+
136
+ static createBlock(hash, startLine, endLine, context) {
137
+ return new CommentPosition({
138
+ type: review.PositionType.BLOCK,
139
+ hashWhenCreated: hash,
140
+ lineRange: [startLine, endLine],
141
+ fallbackContext: context || null
142
+ });
143
+ }
144
+
145
+ static createWord(hash, keyword, occurrence, context) {
146
+ return new CommentPosition({
147
+ type: review.PositionType.WORD,
148
+ hashWhenCreated: hash,
149
+ keyword: keyword,
150
+ keywordOccurrence: occurrence || 1,
151
+ fallbackContext: context || null
152
+ });
153
+ }
154
+
155
+ static createGeneral(hash) {
156
+ return new CommentPosition({
157
+ type: review.PositionType.GENERAL,
158
+ hashWhenCreated: hash
159
+ });
160
+ }
161
+
162
+ validate() {
163
+ const errors = [];
164
+ const validTypes = Object.values(review.PositionType);
165
+
166
+ if (!validTypes.includes(this.type)) {
167
+ errors.push(`Invalid position type: ${this.type}`);
168
+ }
169
+
170
+ if (!review.validateHash(this.hashWhenCreated)) {
171
+ errors.push(`Invalid hash format: ${this.hashWhenCreated}`);
172
+ }
173
+
174
+ if (this.type === review.PositionType.LINE) {
175
+ if (this.lineNumber === null) {
176
+ errors.push('lineNumber required for line type');
177
+ } else if (this.lineNumber < 1) {
178
+ errors.push('lineNumber must be positive');
179
+ }
180
+ }
181
+
182
+ if (this.type === review.PositionType.BLOCK) {
183
+ if (!this.lineRange || this.lineRange.length !== 2) {
184
+ errors.push('lineRange required for block type');
185
+ } else if (this.lineRange[0] < 1 || this.lineRange[1] < this.lineRange[0]) {
186
+ errors.push('Invalid lineRange');
187
+ }
188
+ }
189
+
190
+ if (this.type === review.PositionType.WORD) {
191
+ if (!this.keyword) {
192
+ errors.push('keyword required for word type');
193
+ }
194
+ }
195
+
196
+ return { valid: errors.length === 0, errors: errors };
197
+ }
198
+
199
+ toDict() {
200
+ const result = {
201
+ type: this.type,
202
+ hashWhenCreated: this.hashWhenCreated
203
+ };
204
+ if (this.lineNumber !== null) result.lineNumber = this.lineNumber;
205
+ if (this.lineRange !== null) result.lineRange = this.lineRange;
206
+ if (this.keyword !== null) result.keyword = this.keyword;
207
+ if (this.keywordOccurrence !== null) result.keywordOccurrence = this.keywordOccurrence;
208
+ if (this.fallbackContext !== null) result.fallbackContext = this.fallbackContext;
209
+ return result;
210
+ }
211
+
212
+ static fromDict(data) {
213
+ return new CommentPosition(data);
214
+ }
215
+ }
216
+ review.CommentPosition = CommentPosition;
217
+
218
+ /**
219
+ * Single comment in a thread
220
+ */
221
+ class Comment {
222
+ constructor(data) {
223
+ this.id = data.id;
224
+ this.author = data.author;
225
+ this.timestamp = data.timestamp;
226
+ this.body = data.body;
227
+ }
228
+
229
+ static create(author, body) {
230
+ return new Comment({
231
+ id: review.generateUuid(),
232
+ author: author,
233
+ timestamp: review.nowIso(),
234
+ body: body
235
+ });
236
+ }
237
+
238
+ validate() {
239
+ const errors = [];
240
+ if (!this.id) errors.push('Comment id is required');
241
+ if (!this.author) errors.push('Comment author is required');
242
+ if (!this.timestamp) errors.push('Comment timestamp is required');
243
+ if (!this.body || !this.body.trim()) errors.push('Comment body cannot be empty');
244
+ return { valid: errors.length === 0, errors: errors };
245
+ }
246
+
247
+ toDict() {
248
+ return {
249
+ id: this.id,
250
+ author: this.author,
251
+ timestamp: this.timestamp,
252
+ body: this.body
253
+ };
254
+ }
255
+
256
+ static fromDict(data) {
257
+ return new Comment(data);
258
+ }
259
+ }
260
+ review.Comment = Comment;
261
+
262
+ /**
263
+ * Comment thread with position anchor
264
+ * REQ-d00094: Thread model with packageId for package-centric ownership
265
+ */
266
+ class Thread {
267
+ constructor(data) {
268
+ this.threadId = data.threadId;
269
+ this.reqId = data.reqId;
270
+ this.packageId = data.packageId || null; // REQ-d00094-A: Package owning this thread
271
+ this.createdBy = data.createdBy;
272
+ this.createdAt = data.createdAt;
273
+ this.position = data.position instanceof CommentPosition ?
274
+ data.position : CommentPosition.fromDict(data.position);
275
+ this.resolved = data.resolved || false;
276
+ this.resolvedBy = data.resolvedBy || null;
277
+ this.resolvedAt = data.resolvedAt || null;
278
+ this.comments = (data.comments || []).map(c =>
279
+ c instanceof Comment ? c : Comment.fromDict(c)
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Create a new thread
285
+ * REQ-d00094: Threads must be owned by a package
286
+ * @param {string} reqId - Requirement ID
287
+ * @param {string} creator - Username of creator
288
+ * @param {CommentPosition} position - Position anchor
289
+ * @param {string} initialComment - Initial comment body
290
+ * @param {string} packageId - Package ID (required for new threads)
291
+ */
292
+ static create(reqId, creator, position, initialComment, packageId = null) {
293
+ const thread = new Thread({
294
+ threadId: review.generateUuid(),
295
+ reqId: reqId,
296
+ packageId: packageId, // REQ-d00094-A: Package ownership
297
+ createdBy: creator,
298
+ createdAt: review.nowIso(),
299
+ position: position,
300
+ comments: []
301
+ });
302
+ if (initialComment) {
303
+ thread.addComment(creator, initialComment);
304
+ }
305
+ return thread;
306
+ }
307
+
308
+ addComment(author, body) {
309
+ const comment = Comment.create(author, body);
310
+ this.comments.push(comment);
311
+ return comment;
312
+ }
313
+
314
+ resolve(user) {
315
+ this.resolved = true;
316
+ this.resolvedBy = user;
317
+ this.resolvedAt = review.nowIso();
318
+ }
319
+
320
+ unresolve() {
321
+ this.resolved = false;
322
+ this.resolvedBy = null;
323
+ this.resolvedAt = null;
324
+ }
325
+
326
+ validate() {
327
+ const errors = [];
328
+ if (!this.threadId) errors.push('Thread threadId is required');
329
+ if (!review.validateReqId(this.reqId)) {
330
+ errors.push(`Invalid requirement ID: ${this.reqId}`);
331
+ }
332
+ if (!this.createdBy) errors.push('Thread createdBy is required');
333
+
334
+ const posValidation = this.position.validate();
335
+ posValidation.errors.forEach(e => errors.push(`Position: ${e}`));
336
+
337
+ if (this.resolved) {
338
+ if (!this.resolvedBy) errors.push('Resolved thread must have resolvedBy');
339
+ if (!this.resolvedAt) errors.push('Resolved thread must have resolvedAt');
340
+ }
341
+
342
+ this.comments.forEach((c, i) => {
343
+ const commentValidation = c.validate();
344
+ commentValidation.errors.forEach(e => errors.push(`Comment[${i}]: ${e}`));
345
+ });
346
+
347
+ return { valid: errors.length === 0, errors: errors };
348
+ }
349
+
350
+ toDict() {
351
+ const result = {
352
+ threadId: this.threadId,
353
+ reqId: this.reqId,
354
+ createdBy: this.createdBy,
355
+ createdAt: this.createdAt,
356
+ position: this.position.toDict(),
357
+ resolved: this.resolved,
358
+ resolvedBy: this.resolvedBy,
359
+ resolvedAt: this.resolvedAt,
360
+ comments: this.comments.map(c => c.toDict())
361
+ };
362
+ // REQ-d00094-A: Include packageId in serialization
363
+ if (this.packageId !== null) {
364
+ result.packageId = this.packageId;
365
+ }
366
+ return result;
367
+ }
368
+
369
+ static fromDict(data) {
370
+ return new Thread(data);
371
+ }
372
+ }
373
+ review.Thread = Thread;
374
+
375
+ /**
376
+ * Review flag for a requirement
377
+ */
378
+ class ReviewFlag {
379
+ constructor(data) {
380
+ this.flaggedForReview = data.flaggedForReview;
381
+ this.flaggedBy = data.flaggedBy || '';
382
+ this.flaggedAt = data.flaggedAt || '';
383
+ this.reason = data.reason || '';
384
+ this.scope = data.scope || [];
385
+ }
386
+
387
+ static create(user, reason, scope) {
388
+ return new ReviewFlag({
389
+ flaggedForReview: true,
390
+ flaggedBy: user,
391
+ flaggedAt: review.nowIso(),
392
+ reason: reason,
393
+ scope: scope
394
+ });
395
+ }
396
+
397
+ static cleared() {
398
+ return new ReviewFlag({
399
+ flaggedForReview: false
400
+ });
401
+ }
402
+
403
+ validate() {
404
+ const errors = [];
405
+ if (this.flaggedForReview) {
406
+ if (!this.flaggedBy) errors.push('Flagged review must have flaggedBy');
407
+ if (!this.flaggedAt) errors.push('Flagged review must have flaggedAt');
408
+ if (!this.reason) errors.push('Flagged review must have reason');
409
+ if (!this.scope || this.scope.length === 0) {
410
+ errors.push('Flagged review must have non-empty scope');
411
+ }
412
+ }
413
+ return { valid: errors.length === 0, errors: errors };
414
+ }
415
+
416
+ toDict() {
417
+ return {
418
+ flaggedForReview: this.flaggedForReview,
419
+ flaggedBy: this.flaggedBy,
420
+ flaggedAt: this.flaggedAt,
421
+ reason: this.reason,
422
+ scope: this.scope
423
+ };
424
+ }
425
+
426
+ static fromDict(data) {
427
+ return new ReviewFlag(data);
428
+ }
429
+ }
430
+ review.ReviewFlag = ReviewFlag;
431
+
432
+ /**
433
+ * Approval on a status change request
434
+ */
435
+ class Approval {
436
+ constructor(data) {
437
+ this.user = data.user;
438
+ this.decision = data.decision;
439
+ this.at = data.at;
440
+ this.comment = data.comment || null;
441
+ }
442
+
443
+ static create(user, decision, comment) {
444
+ return new Approval({
445
+ user: user,
446
+ decision: decision,
447
+ at: review.nowIso(),
448
+ comment: comment || null
449
+ });
450
+ }
451
+
452
+ validate() {
453
+ const errors = [];
454
+ if (!this.user) errors.push('Approval user is required');
455
+ if (!Object.values(review.ApprovalDecision).includes(this.decision)) {
456
+ errors.push(`Invalid decision: ${this.decision}`);
457
+ }
458
+ if (!this.at) errors.push('Approval timestamp is required');
459
+ return { valid: errors.length === 0, errors: errors };
460
+ }
461
+
462
+ toDict() {
463
+ const result = {
464
+ user: this.user,
465
+ decision: this.decision,
466
+ at: this.at
467
+ };
468
+ if (this.comment !== null) result.comment = this.comment;
469
+ return result;
470
+ }
471
+
472
+ static fromDict(data) {
473
+ return new Approval(data);
474
+ }
475
+ }
476
+ review.Approval = Approval;
477
+
478
+ /**
479
+ * Status change request
480
+ */
481
+ class StatusRequest {
482
+ constructor(data) {
483
+ this.requestId = data.requestId;
484
+ this.reqId = data.reqId;
485
+ this.type = data.type || 'status_change';
486
+ this.fromStatus = data.fromStatus;
487
+ this.toStatus = data.toStatus;
488
+ this.requestedBy = data.requestedBy;
489
+ this.requestedAt = data.requestedAt;
490
+ this.justification = data.justification;
491
+ this.approvals = (data.approvals || []).map(a =>
492
+ a instanceof Approval ? a : Approval.fromDict(a)
493
+ );
494
+ this.requiredApprovers = data.requiredApprovers || [];
495
+ this.state = data.state || review.RequestState.PENDING;
496
+ }
497
+
498
+ static create(reqId, fromStatus, toStatus, requestedBy, justification, requiredApprovers) {
499
+ if (!requiredApprovers) {
500
+ const key = `${fromStatus}->${toStatus}`;
501
+ requiredApprovers = review.DEFAULT_APPROVAL_RULES[key] || ['product_owner'];
502
+ }
503
+ return new StatusRequest({
504
+ requestId: review.generateUuid(),
505
+ reqId: reqId,
506
+ type: 'status_change',
507
+ fromStatus: fromStatus,
508
+ toStatus: toStatus,
509
+ requestedBy: requestedBy,
510
+ requestedAt: review.nowIso(),
511
+ justification: justification,
512
+ approvals: [],
513
+ requiredApprovers: requiredApprovers,
514
+ state: review.RequestState.PENDING
515
+ });
516
+ }
517
+
518
+ addApproval(user, decision, comment) {
519
+ const approval = Approval.create(user, decision, comment);
520
+ this.approvals.push(approval);
521
+ this._updateState();
522
+ return approval;
523
+ }
524
+
525
+ _updateState() {
526
+ if (this.state === review.RequestState.APPLIED) return;
527
+
528
+ // Check for rejections
529
+ for (const approval of this.approvals) {
530
+ if (approval.decision === review.ApprovalDecision.REJECT) {
531
+ this.state = review.RequestState.REJECTED;
532
+ return;
533
+ }
534
+ }
535
+
536
+ // Check if all required approvers have approved
537
+ const approvedUsers = new Set(
538
+ this.approvals
539
+ .filter(a => a.decision === review.ApprovalDecision.APPROVE)
540
+ .map(a => a.user)
541
+ );
542
+
543
+ const allApproved = this.requiredApprovers.every(
544
+ approver => approvedUsers.has(approver)
545
+ );
546
+
547
+ this.state = allApproved ? review.RequestState.APPROVED : review.RequestState.PENDING;
548
+ }
549
+
550
+ markApplied() {
551
+ if (this.state !== review.RequestState.APPROVED) {
552
+ throw new Error('Can only apply approved requests');
553
+ }
554
+ this.state = review.RequestState.APPLIED;
555
+ }
556
+
557
+ validate() {
558
+ const errors = [];
559
+ if (!this.requestId) errors.push('requestId is required');
560
+ if (!review.validateReqId(this.reqId)) {
561
+ errors.push(`Invalid requirement ID: ${this.reqId}`);
562
+ }
563
+ if (this.type !== 'status_change') {
564
+ errors.push(`Invalid type: ${this.type}`);
565
+ }
566
+ if (!review.VALID_REQ_STATUSES.includes(this.fromStatus)) {
567
+ errors.push(`Invalid fromStatus: ${this.fromStatus}`);
568
+ }
569
+ if (!review.VALID_REQ_STATUSES.includes(this.toStatus)) {
570
+ errors.push(`Invalid toStatus: ${this.toStatus}`);
571
+ }
572
+ if (this.fromStatus === this.toStatus) {
573
+ errors.push('fromStatus and toStatus must be different');
574
+ }
575
+ if (!this.requestedBy) errors.push('requestedBy is required');
576
+ if (!this.justification) errors.push('justification is required');
577
+ if (!Object.values(review.RequestState).includes(this.state)) {
578
+ errors.push(`Invalid state: ${this.state}`);
579
+ }
580
+
581
+ this.approvals.forEach((a, i) => {
582
+ const approvalValidation = a.validate();
583
+ approvalValidation.errors.forEach(e => errors.push(`Approval[${i}]: ${e}`));
584
+ });
585
+
586
+ return { valid: errors.length === 0, errors: errors };
587
+ }
588
+
589
+ toDict() {
590
+ return {
591
+ requestId: this.requestId,
592
+ reqId: this.reqId,
593
+ type: this.type,
594
+ fromStatus: this.fromStatus,
595
+ toStatus: this.toStatus,
596
+ requestedBy: this.requestedBy,
597
+ requestedAt: this.requestedAt,
598
+ justification: this.justification,
599
+ approvals: this.approvals.map(a => a.toDict()),
600
+ requiredApprovers: this.requiredApprovers,
601
+ state: this.state
602
+ };
603
+ }
604
+
605
+ static fromDict(data) {
606
+ return new StatusRequest(data);
607
+ }
608
+ }
609
+ review.StatusRequest = StatusRequest;
610
+
611
+ /**
612
+ * Review session metadata
613
+ */
614
+ class ReviewSession {
615
+ constructor(data) {
616
+ this.sessionId = data.sessionId;
617
+ this.user = data.user;
618
+ this.name = data.name;
619
+ this.createdAt = data.createdAt;
620
+ this.description = data.description || null;
621
+ }
622
+
623
+ static create(user, name, description) {
624
+ return new ReviewSession({
625
+ sessionId: review.generateUuid(),
626
+ user: user,
627
+ name: name,
628
+ createdAt: review.nowIso(),
629
+ description: description || null
630
+ });
631
+ }
632
+
633
+ validate() {
634
+ const errors = [];
635
+ if (!this.sessionId) errors.push('sessionId is required');
636
+ if (!this.user) errors.push('user is required');
637
+ if (!this.name) errors.push('name is required');
638
+ if (!this.createdAt) errors.push('createdAt is required');
639
+ return { valid: errors.length === 0, errors: errors };
640
+ }
641
+
642
+ toDict() {
643
+ const result = {
644
+ sessionId: this.sessionId,
645
+ user: this.user,
646
+ name: this.name,
647
+ createdAt: this.createdAt
648
+ };
649
+ if (this.description !== null) result.description = this.description;
650
+ return result;
651
+ }
652
+
653
+ static fromDict(data) {
654
+ return new ReviewSession(data);
655
+ }
656
+ }
657
+ review.ReviewSession = ReviewSession;
658
+
659
+ /**
660
+ * Review system configuration
661
+ */
662
+ class ReviewConfig {
663
+ constructor(data) {
664
+ this.approvalRules = data.approvalRules || Object.assign({}, review.DEFAULT_APPROVAL_RULES);
665
+ this.pushOnComment = data.pushOnComment !== undefined ? data.pushOnComment : true;
666
+ this.autoFetchOnOpen = data.autoFetchOnOpen !== undefined ? data.autoFetchOnOpen : true;
667
+ }
668
+
669
+ static createDefault() {
670
+ return new ReviewConfig({});
671
+ }
672
+
673
+ getRequiredApprovers(fromStatus, toStatus) {
674
+ const key = `${fromStatus}->${toStatus}`;
675
+ return this.approvalRules[key] || ['product_owner'];
676
+ }
677
+
678
+ toDict() {
679
+ return {
680
+ approvalRules: this.approvalRules,
681
+ pushOnComment: this.pushOnComment,
682
+ autoFetchOnOpen: this.autoFetchOnOpen
683
+ };
684
+ }
685
+
686
+ static fromDict(data) {
687
+ return new ReviewConfig(data);
688
+ }
689
+ }
690
+ review.ReviewConfig = ReviewConfig;
691
+
692
+ // ==========================================================================
693
+ // State Management
694
+ // ==========================================================================
695
+
696
+ /**
697
+ * Local state manager for review data
698
+ */
699
+ class ReviewState {
700
+ constructor() {
701
+ this.threads = {}; // reqId -> Thread[]
702
+ this.flags = {}; // reqId -> ReviewFlag
703
+ this.requests = {}; // reqId -> StatusRequest[]
704
+ this.sessions = []; // ReviewSession[]
705
+ this.config = ReviewConfig.createDefault();
706
+ this.currentUser = null;
707
+ this.currentSession = null;
708
+ }
709
+
710
+ /**
711
+ * Load review data from embedded JSON
712
+ * @param {Object} data - Embedded review data
713
+ */
714
+ loadFromEmbedded(data) {
715
+ if (data.threads) {
716
+ for (const reqId in data.threads) {
717
+ this.threads[reqId] = data.threads[reqId].map(t => Thread.fromDict(t));
718
+ }
719
+ }
720
+ if (data.flags) {
721
+ for (const reqId in data.flags) {
722
+ this.flags[reqId] = ReviewFlag.fromDict(data.flags[reqId]);
723
+ }
724
+ }
725
+ if (data.requests) {
726
+ for (const reqId in data.requests) {
727
+ this.requests[reqId] = data.requests[reqId].map(r => StatusRequest.fromDict(r));
728
+ }
729
+ }
730
+ if (data.sessions) {
731
+ this.sessions = data.sessions.map(s => ReviewSession.fromDict(s));
732
+ }
733
+ if (data.config) {
734
+ this.config = ReviewConfig.fromDict(data.config);
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Get threads for a requirement
740
+ * @param {string} reqId - Requirement ID
741
+ * @returns {Thread[]} Array of threads
742
+ */
743
+ getThreads(reqId) {
744
+ const normalizedId = review.normalizeReqId(reqId);
745
+ return this.threads[normalizedId] || [];
746
+ }
747
+
748
+ /**
749
+ * Get review flag for a requirement
750
+ * @param {string} reqId - Requirement ID
751
+ * @returns {ReviewFlag} Review flag
752
+ */
753
+ getFlag(reqId) {
754
+ const normalizedId = review.normalizeReqId(reqId);
755
+ return this.flags[normalizedId] || ReviewFlag.cleared();
756
+ }
757
+
758
+ /**
759
+ * Get status requests for a requirement
760
+ * @param {string} reqId - Requirement ID
761
+ * @returns {StatusRequest[]} Array of requests
762
+ */
763
+ getRequests(reqId) {
764
+ const normalizedId = review.normalizeReqId(reqId);
765
+ return this.requests[normalizedId] || [];
766
+ }
767
+
768
+ /**
769
+ * Get all flagged requirement IDs
770
+ * @returns {string[]} Array of flagged requirement IDs
771
+ */
772
+ getFlaggedReqs() {
773
+ return Object.keys(this.flags).filter(
774
+ reqId => this.flags[reqId].flaggedForReview
775
+ );
776
+ }
777
+
778
+ /**
779
+ * Add a thread
780
+ * @param {Thread} thread - Thread to add
781
+ */
782
+ addThread(thread) {
783
+ const normalizedId = review.normalizeReqId(thread.reqId);
784
+ if (!this.threads[normalizedId]) {
785
+ this.threads[normalizedId] = [];
786
+ }
787
+ this.threads[normalizedId].push(thread);
788
+ }
789
+
790
+ /**
791
+ * Set review flag
792
+ * @param {string} reqId - Requirement ID
793
+ * @param {ReviewFlag} flag - Review flag
794
+ */
795
+ setFlag(reqId, flag) {
796
+ const normalizedId = review.normalizeReqId(reqId);
797
+ this.flags[normalizedId] = flag;
798
+ }
799
+
800
+ /**
801
+ * Add a status request
802
+ * @param {StatusRequest} request - Request to add
803
+ */
804
+ addRequest(request) {
805
+ const normalizedId = review.normalizeReqId(request.reqId);
806
+ if (!this.requests[normalizedId]) {
807
+ this.requests[normalizedId] = [];
808
+ }
809
+ this.requests[normalizedId].push(request);
810
+ }
811
+
812
+ /**
813
+ * Export state to JSON
814
+ * @returns {Object} Serializable state
815
+ */
816
+ toJSON() {
817
+ const result = {
818
+ threads: {},
819
+ flags: {},
820
+ requests: {},
821
+ sessions: this.sessions.map(s => s.toDict()),
822
+ config: this.config.toDict()
823
+ };
824
+
825
+ for (const reqId in this.threads) {
826
+ result.threads[reqId] = this.threads[reqId].map(t => t.toDict());
827
+ }
828
+ for (const reqId in this.flags) {
829
+ result.flags[reqId] = this.flags[reqId].toDict();
830
+ }
831
+ for (const reqId in this.requests) {
832
+ result.requests[reqId] = this.requests[reqId].map(r => r.toDict());
833
+ }
834
+
835
+ return result;
836
+ }
837
+ }
838
+ review.ReviewState = ReviewState;
839
+
840
+ // Global state instance
841
+ review.state = new ReviewState();
842
+
843
+ // ==========================================================================
844
+ // Event Listeners for Requirement Selection
845
+ // ==========================================================================
846
+
847
+ /**
848
+ * Currently selected requirement ID
849
+ */
850
+ review.selectedReqId = null;
851
+
852
+ /**
853
+ * Handle requirement selection event
854
+ * @param {CustomEvent} event - Event with reqId in detail
855
+ */
856
+ function handleReqSelected(event) {
857
+ const { reqId, req } = event.detail;
858
+ const previousReqId = review.selectedReqId;
859
+ review.selectedReqId = reqId;
860
+
861
+ // Skip rebuild if same REQ is already selected (e.g., clicking on lines)
862
+ if (previousReqId === reqId) {
863
+ return;
864
+ }
865
+
866
+ // Update review panel header
867
+ const panelContent = document.getElementById('review-panel-content');
868
+ const noSelection = document.getElementById('review-panel-no-selection');
869
+
870
+ if (panelContent && noSelection) {
871
+ noSelection.style.display = 'none';
872
+ panelContent.style.display = 'block';
873
+
874
+ // Build requirement header
875
+ const reqHeader = document.createElement('div');
876
+ reqHeader.className = 'review-panel-req-header';
877
+ reqHeader.innerHTML = `
878
+ <span class="req-id-badge">REQ-${reqId}</span>
879
+ <span class="req-title-text">${req ? req.title : reqId}</span>
880
+ `;
881
+
882
+ // Clear existing content and add header
883
+ panelContent.innerHTML = '';
884
+ panelContent.appendChild(reqHeader);
885
+
886
+ // Create sections container
887
+ const sectionsDiv = document.createElement('div');
888
+ sectionsDiv.className = 'review-panel-sections';
889
+ panelContent.appendChild(sectionsDiv);
890
+
891
+ // Dispatch event for other modules to populate sections
892
+ document.dispatchEvent(new CustomEvent('traceview:review-panel-ready', {
893
+ detail: { reqId, req, sectionsContainer: sectionsDiv }
894
+ }));
895
+ }
896
+ }
897
+
898
+ // Register event listener
899
+ document.addEventListener('traceview:req-selected', handleReqSelected);
900
+
901
+ // ==========================================================================
902
+ // Main Initialization Function
903
+ // ==========================================================================
904
+
905
+ /**
906
+ * Initialize the review system
907
+ *
908
+ * This is the main entry point called when review mode is activated.
909
+ * It sets up the user, loads embedded data, and initializes sub-components.
910
+ */
911
+ async function init() {
912
+ console.log('Initializing TraceView Review System...');
913
+
914
+ // Set default user from localStorage or prompt
915
+ let user = localStorage.getItem('traceview_review_user');
916
+ if (!user) {
917
+ user = 'reviewer'; // Default username
918
+ localStorage.setItem('traceview_review_user', user);
919
+ }
920
+ review.state.currentUser = user;
921
+ console.log(`Review user: ${user}`);
922
+
923
+ // Load embedded review data if available
924
+ if (window.REVIEW_DATA) {
925
+ review.state.loadFromEmbedded(window.REVIEW_DATA);
926
+ console.log('Loaded embedded review data');
927
+ }
928
+
929
+ // Initialize sync module if available
930
+ if (typeof review.initSync === 'function') {
931
+ review.initSync(window.REVIEW_DATA);
932
+ }
933
+
934
+ // Initialize packages panel if available
935
+ if (typeof review.initPackagesPanel === 'function') {
936
+ await review.initPackagesPanel();
937
+ }
938
+
939
+ // Initialize git sync if available
940
+ if (typeof review.initGitSync === 'function') {
941
+ review.initGitSync();
942
+ }
943
+
944
+ // Initialize help system if available
945
+ if (typeof review.help?.init === 'function') {
946
+ await review.help.init();
947
+ }
948
+
949
+ console.log('TraceView Review System initialized');
950
+ }
951
+
952
+ // Export init function
953
+ review.init = init;
954
+
955
+ })(TraceView.review);
956
+
957
+ // Create window.ReviewSystem alias for backwards compatibility (REQ-d00092)
958
+ // This allows tests and templates to use window.ReviewSystem while
959
+ // the implementation uses TraceView.review namespace
960
+ window.ReviewSystem = window.ReviewSystem || {};
961
+ Object.assign(window.ReviewSystem, TraceView.review);