musubi-sdd 6.1.2 → 6.2.1

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.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Phase -1 Gate
3
+ *
4
+ * Triggers Phase -1 Gate review for Article VII/VIII violations.
5
+ *
6
+ * Requirement: IMP-6.2-005-02
7
+ * Design: Section 5.2
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+ const { ConstitutionalChecker, SEVERITY } = require('./checker');
13
+
14
+ /**
15
+ * Default configuration
16
+ */
17
+ const DEFAULT_CONFIG = {
18
+ storageDir: 'storage/phase-minus-one',
19
+ requiredReviewers: ['system-architect'],
20
+ optionalReviewers: ['project-manager'],
21
+ autoNotify: true
22
+ };
23
+
24
+ /**
25
+ * Gate status
26
+ */
27
+ const GATE_STATUS = {
28
+ PENDING: 'pending',
29
+ APPROVED: 'approved',
30
+ REJECTED: 'rejected',
31
+ WAIVED: 'waived'
32
+ };
33
+
34
+ /**
35
+ * PhaseMinusOneGate
36
+ *
37
+ * Manages Phase -1 Gate review process.
38
+ */
39
+ class PhaseMinusOneGate {
40
+ /**
41
+ * @param {Object} config - Configuration options
42
+ */
43
+ constructor(config = {}) {
44
+ this.config = { ...DEFAULT_CONFIG, ...config };
45
+ this.checker = new ConstitutionalChecker(config.checkerConfig);
46
+ }
47
+
48
+ /**
49
+ * Trigger Phase -1 Gate
50
+ * @param {Object} options - Trigger options
51
+ * @returns {Promise<Object>} Gate record
52
+ */
53
+ async trigger(options) {
54
+ const gateId = `GATE-${Date.now()}`;
55
+
56
+ const gate = {
57
+ id: gateId,
58
+ featureId: options.featureId,
59
+ triggeredBy: options.triggeredBy || 'system',
60
+ triggeredAt: new Date().toISOString(),
61
+ violations: options.violations || [],
62
+ affectedFiles: options.affectedFiles || [],
63
+ status: GATE_STATUS.PENDING,
64
+ requiredReviewers: this.config.requiredReviewers,
65
+ optionalReviewers: this.config.optionalReviewers,
66
+ reviews: [],
67
+ resolution: null,
68
+ resolvedAt: null
69
+ };
70
+
71
+ await this.saveGate(gate);
72
+
73
+ // Generate notification if auto-notify is enabled
74
+ if (this.config.autoNotify) {
75
+ gate.notifications = await this.generateNotifications(gate);
76
+ }
77
+
78
+ return gate;
79
+ }
80
+
81
+ /**
82
+ * Analyze files and trigger gate if needed
83
+ * @param {Array} filePaths - Files to analyze
84
+ * @param {Object} options - Options
85
+ * @returns {Promise<Object>} Analysis result
86
+ */
87
+ async analyzeAndTrigger(filePaths, options = {}) {
88
+ const checkResults = await this.checker.checkFiles(filePaths);
89
+ const blockDecision = this.checker.shouldBlockMerge(checkResults);
90
+
91
+ if (blockDecision.requiresPhaseMinusOne) {
92
+ const violations = checkResults.results
93
+ .flatMap(r => r.violations)
94
+ .filter(v => v.article === 'VII' || v.article === 'VIII');
95
+
96
+ const gate = await this.trigger({
97
+ featureId: options.featureId,
98
+ triggeredBy: options.triggeredBy || 'auto-analysis',
99
+ violations,
100
+ affectedFiles: filePaths
101
+ });
102
+
103
+ return {
104
+ triggered: true,
105
+ gate,
106
+ checkResults,
107
+ blockDecision
108
+ };
109
+ }
110
+
111
+ return {
112
+ triggered: false,
113
+ gate: null,
114
+ checkResults,
115
+ blockDecision
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Submit review for gate
121
+ * @param {string} gateId - Gate ID
122
+ * @param {Object} review - Review data
123
+ * @returns {Promise<Object>} Updated gate
124
+ */
125
+ async submitReview(gateId, review) {
126
+ const gate = await this.loadGate(gateId);
127
+ if (!gate) {
128
+ throw new Error(`Gate not found: ${gateId}`);
129
+ }
130
+
131
+ const reviewEntry = {
132
+ id: `REV-${Date.now()}`,
133
+ reviewer: review.reviewer,
134
+ decision: review.decision, // 'approve', 'reject', 'request-changes'
135
+ comments: review.comments || '',
136
+ submittedAt: new Date().toISOString()
137
+ };
138
+
139
+ gate.reviews.push(reviewEntry);
140
+
141
+ // Check if all required reviewers have approved
142
+ const approvedReviewers = gate.reviews
143
+ .filter(r => r.decision === 'approve')
144
+ .map(r => r.reviewer);
145
+
146
+ const allRequiredApproved = gate.requiredReviewers.every(
147
+ r => approvedReviewers.includes(r)
148
+ );
149
+
150
+ const hasRejection = gate.reviews.some(r => r.decision === 'reject');
151
+
152
+ if (hasRejection) {
153
+ gate.status = GATE_STATUS.REJECTED;
154
+ gate.resolution = 'rejected';
155
+ gate.resolvedAt = new Date().toISOString();
156
+ } else if (allRequiredApproved) {
157
+ gate.status = GATE_STATUS.APPROVED;
158
+ gate.resolution = 'approved';
159
+ gate.resolvedAt = new Date().toISOString();
160
+ }
161
+
162
+ await this.saveGate(gate);
163
+
164
+ return gate;
165
+ }
166
+
167
+ /**
168
+ * Waive gate (with justification)
169
+ * @param {string} gateId - Gate ID
170
+ * @param {Object} waiver - Waiver data
171
+ * @returns {Promise<Object>} Updated gate
172
+ */
173
+ async waiveGate(gateId, waiver) {
174
+ const gate = await this.loadGate(gateId);
175
+ if (!gate) {
176
+ throw new Error(`Gate not found: ${gateId}`);
177
+ }
178
+
179
+ gate.status = GATE_STATUS.WAIVED;
180
+ gate.resolution = 'waived';
181
+ gate.waiver = {
182
+ waivedBy: waiver.waivedBy,
183
+ justification: waiver.justification,
184
+ waivedAt: new Date().toISOString()
185
+ };
186
+ gate.resolvedAt = new Date().toISOString();
187
+
188
+ await this.saveGate(gate);
189
+
190
+ return gate;
191
+ }
192
+
193
+ /**
194
+ * Get gate by ID
195
+ * @param {string} gateId - Gate ID
196
+ * @returns {Promise<Object|null>} Gate
197
+ */
198
+ async getGate(gateId) {
199
+ return await this.loadGate(gateId);
200
+ }
201
+
202
+ /**
203
+ * List gates by status
204
+ * @param {string} status - Status filter (optional)
205
+ * @returns {Promise<Array>} Gates
206
+ */
207
+ async listGates(status = null) {
208
+ try {
209
+ await this.ensureStorageDir();
210
+ const files = await fs.readdir(this.config.storageDir);
211
+ const gates = [];
212
+
213
+ for (const file of files) {
214
+ if (file.endsWith('.json')) {
215
+ const gate = await this.loadGate(file.replace('.json', ''));
216
+ if (gate && (!status || gate.status === status)) {
217
+ gates.push(gate);
218
+ }
219
+ }
220
+ }
221
+
222
+ return gates;
223
+ } catch {
224
+ return [];
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get pending gates for reviewer
230
+ * @param {string} reviewer - Reviewer name
231
+ * @returns {Promise<Array>} Pending gates
232
+ */
233
+ async getPendingForReviewer(reviewer) {
234
+ const pendingGates = await this.listGates(GATE_STATUS.PENDING);
235
+
236
+ return pendingGates.filter(gate => {
237
+ const hasReviewed = gate.reviews.some(r => r.reviewer === reviewer);
238
+ const isRequired = gate.requiredReviewers.includes(reviewer);
239
+ const isOptional = gate.optionalReviewers.includes(reviewer);
240
+
241
+ return !hasReviewed && (isRequired || isOptional);
242
+ });
243
+ }
244
+
245
+ /**
246
+ * Generate notifications for gate
247
+ * @param {Object} gate - Gate data
248
+ * @returns {Promise<Array>} Notifications
249
+ */
250
+ async generateNotifications(gate) {
251
+ const notifications = [];
252
+
253
+ // Notify required reviewers
254
+ for (const reviewer of gate.requiredReviewers) {
255
+ notifications.push({
256
+ type: 'required-review',
257
+ recipient: reviewer,
258
+ gateId: gate.id,
259
+ message: `Phase -1 Gate review required for ${gate.featureId || 'unknown feature'}`,
260
+ violations: gate.violations.length,
261
+ createdAt: new Date().toISOString()
262
+ });
263
+ }
264
+
265
+ // Notify optional reviewers
266
+ for (const reviewer of gate.optionalReviewers) {
267
+ notifications.push({
268
+ type: 'optional-review',
269
+ recipient: reviewer,
270
+ gateId: gate.id,
271
+ message: `Phase -1 Gate review available for ${gate.featureId || 'unknown feature'}`,
272
+ violations: gate.violations.length,
273
+ createdAt: new Date().toISOString()
274
+ });
275
+ }
276
+
277
+ return notifications;
278
+ }
279
+
280
+ /**
281
+ * Generate gate report
282
+ * @param {string} gateId - Gate ID
283
+ * @returns {Promise<string>} Markdown report
284
+ */
285
+ async generateReport(gateId) {
286
+ const gate = await this.loadGate(gateId);
287
+ if (!gate) {
288
+ throw new Error(`Gate not found: ${gateId}`);
289
+ }
290
+
291
+ const lines = [];
292
+
293
+ lines.push('# Phase -1 Gate Report');
294
+ lines.push('');
295
+ lines.push(`**Gate ID:** ${gate.id}`);
296
+ lines.push(`**Feature:** ${gate.featureId || 'N/A'}`);
297
+ lines.push(`**Status:** ${this.getStatusEmoji(gate.status)} ${gate.status}`);
298
+ lines.push(`**Triggered:** ${gate.triggeredAt}`);
299
+ if (gate.resolvedAt) {
300
+ lines.push(`**Resolved:** ${gate.resolvedAt}`);
301
+ }
302
+ lines.push('');
303
+
304
+ // Violations
305
+ lines.push('## Violations');
306
+ lines.push('');
307
+ if (gate.violations.length === 0) {
308
+ lines.push('No violations recorded.');
309
+ } else {
310
+ for (const v of gate.violations) {
311
+ lines.push(`### Article ${v.article}: ${v.articleName}`);
312
+ lines.push(`- **Severity:** ${v.severity}`);
313
+ lines.push(`- **File:** ${v.filePath}`);
314
+ lines.push(`- **Message:** ${v.message}`);
315
+ lines.push(`- **Suggestion:** ${v.suggestion}`);
316
+ lines.push('');
317
+ }
318
+ }
319
+
320
+ // Reviews
321
+ lines.push('## Reviews');
322
+ lines.push('');
323
+ if (gate.reviews.length === 0) {
324
+ lines.push('No reviews submitted yet.');
325
+ } else {
326
+ lines.push('| Reviewer | Decision | Date |');
327
+ lines.push('|----------|----------|------|');
328
+ for (const r of gate.reviews) {
329
+ const emoji = r.decision === 'approve' ? '✅' :
330
+ r.decision === 'reject' ? '❌' : '🔄';
331
+ lines.push(`| ${r.reviewer} | ${emoji} ${r.decision} | ${r.submittedAt} |`);
332
+ }
333
+ lines.push('');
334
+ }
335
+
336
+ // Waiver info if applicable
337
+ if (gate.waiver) {
338
+ lines.push('## Waiver');
339
+ lines.push('');
340
+ lines.push(`**Waived By:** ${gate.waiver.waivedBy}`);
341
+ lines.push(`**Justification:** ${gate.waiver.justification}`);
342
+ lines.push(`**Waived At:** ${gate.waiver.waivedAt}`);
343
+ lines.push('');
344
+ }
345
+
346
+ return lines.join('\n');
347
+ }
348
+
349
+ /**
350
+ * Get status emoji
351
+ * @param {string} status - Gate status
352
+ * @returns {string} Emoji
353
+ */
354
+ getStatusEmoji(status) {
355
+ const emojis = {
356
+ [GATE_STATUS.PENDING]: '⏳',
357
+ [GATE_STATUS.APPROVED]: '✅',
358
+ [GATE_STATUS.REJECTED]: '❌',
359
+ [GATE_STATUS.WAIVED]: '⚠️'
360
+ };
361
+ return emojis[status] || '❓';
362
+ }
363
+
364
+ /**
365
+ * Save gate to storage
366
+ * @param {Object} gate - Gate to save
367
+ */
368
+ async saveGate(gate) {
369
+ await this.ensureStorageDir();
370
+ const filePath = path.join(this.config.storageDir, `${gate.id}.json`);
371
+ await fs.writeFile(filePath, JSON.stringify(gate, null, 2), 'utf-8');
372
+ }
373
+
374
+ /**
375
+ * Load gate from storage
376
+ * @param {string} gateId - Gate ID
377
+ * @returns {Promise<Object|null>} Gate
378
+ */
379
+ async loadGate(gateId) {
380
+ try {
381
+ const filePath = path.join(this.config.storageDir, `${gateId}.json`);
382
+ const content = await fs.readFile(filePath, 'utf-8');
383
+ return JSON.parse(content);
384
+ } catch {
385
+ return null;
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Ensure storage directory exists
391
+ */
392
+ async ensureStorageDir() {
393
+ try {
394
+ await fs.access(this.config.storageDir);
395
+ } catch {
396
+ await fs.mkdir(this.config.storageDir, { recursive: true });
397
+ }
398
+ }
399
+ }
400
+
401
+ module.exports = {
402
+ PhaseMinusOneGate,
403
+ GATE_STATUS
404
+ };