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.
- elspais/cli.py +99 -1
- elspais/commands/hash_cmd.py +72 -26
- elspais/commands/reformat_cmd.py +458 -0
- elspais/commands/trace.py +157 -3
- elspais/commands/validate.py +44 -16
- elspais/core/models.py +2 -0
- elspais/core/parser.py +68 -24
- elspais/reformat/__init__.py +50 -0
- elspais/reformat/detector.py +119 -0
- elspais/reformat/hierarchy.py +246 -0
- elspais/reformat/line_breaks.py +220 -0
- elspais/reformat/prompts.py +123 -0
- elspais/reformat/transformer.py +264 -0
- elspais/sponsors/__init__.py +432 -0
- elspais/trace_view/__init__.py +54 -0
- elspais/trace_view/coverage.py +183 -0
- elspais/trace_view/generators/__init__.py +12 -0
- elspais/trace_view/generators/base.py +329 -0
- elspais/trace_view/generators/csv.py +122 -0
- elspais/trace_view/generators/markdown.py +175 -0
- elspais/trace_view/html/__init__.py +31 -0
- elspais/trace_view/html/generator.py +1006 -0
- elspais/trace_view/html/templates/base.html +283 -0
- elspais/trace_view/html/templates/components/code_viewer_modal.html +14 -0
- elspais/trace_view/html/templates/components/file_picker_modal.html +20 -0
- elspais/trace_view/html/templates/components/legend_modal.html +69 -0
- elspais/trace_view/html/templates/components/review_panel.html +118 -0
- elspais/trace_view/html/templates/partials/review/help/help-panel.json +244 -0
- elspais/trace_view/html/templates/partials/review/help/onboarding.json +77 -0
- elspais/trace_view/html/templates/partials/review/help/tooltips.json +237 -0
- elspais/trace_view/html/templates/partials/review/review-comments.js +928 -0
- elspais/trace_view/html/templates/partials/review/review-data.js +961 -0
- elspais/trace_view/html/templates/partials/review/review-help.js +679 -0
- elspais/trace_view/html/templates/partials/review/review-init.js +177 -0
- elspais/trace_view/html/templates/partials/review/review-line-numbers.js +429 -0
- elspais/trace_view/html/templates/partials/review/review-packages.js +1029 -0
- elspais/trace_view/html/templates/partials/review/review-position.js +540 -0
- elspais/trace_view/html/templates/partials/review/review-resize.js +115 -0
- elspais/trace_view/html/templates/partials/review/review-status.js +659 -0
- elspais/trace_view/html/templates/partials/review/review-sync.js +992 -0
- elspais/trace_view/html/templates/partials/review-styles.css +2238 -0
- elspais/trace_view/html/templates/partials/scripts.js +1741 -0
- elspais/trace_view/html/templates/partials/styles.css +1756 -0
- elspais/trace_view/models.py +353 -0
- elspais/trace_view/review/__init__.py +60 -0
- elspais/trace_view/review/branches.py +1149 -0
- elspais/trace_view/review/models.py +1205 -0
- elspais/trace_view/review/position.py +609 -0
- elspais/trace_view/review/server.py +1056 -0
- elspais/trace_view/review/status.py +470 -0
- elspais/trace_view/review/storage.py +1367 -0
- elspais/trace_view/scanning.py +213 -0
- elspais/trace_view/specs/README.md +84 -0
- elspais/trace_view/specs/tv-d00001-template-architecture.md +36 -0
- elspais/trace_view/specs/tv-d00002-css-extraction.md +37 -0
- elspais/trace_view/specs/tv-d00003-js-extraction.md +43 -0
- elspais/trace_view/specs/tv-d00004-build-embedding.md +40 -0
- elspais/trace_view/specs/tv-d00005-test-format.md +78 -0
- elspais/trace_view/specs/tv-d00010-review-data-models.md +33 -0
- elspais/trace_view/specs/tv-d00011-review-storage.md +33 -0
- elspais/trace_view/specs/tv-d00012-position-resolution.md +33 -0
- elspais/trace_view/specs/tv-d00013-git-branches.md +31 -0
- elspais/trace_view/specs/tv-d00014-review-api-server.md +31 -0
- elspais/trace_view/specs/tv-d00015-status-modifier.md +27 -0
- elspais/trace_view/specs/tv-d00016-js-integration.md +33 -0
- elspais/trace_view/specs/tv-p00001-html-generator.md +33 -0
- elspais/trace_view/specs/tv-p00002-review-system.md +29 -0
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/METADATA +33 -18
- elspais-0.11.0.dist-info/RECORD +101 -0
- elspais-0.9.3.dist-info/RECORD +0 -40
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/WHEEL +0 -0
- {elspais-0.9.3.dist-info → elspais-0.11.0.dist-info}/entry_points.txt +0 -0
- {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);
|