musubi-sdd 6.2.0 → 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.
- package/README.ja.md +60 -1
- package/README.md +60 -1
- package/bin/musubi-dashboard.js +340 -0
- package/package.json +3 -2
- package/src/cli/dashboard-cli.js +536 -0
- package/src/constitutional/checker.js +633 -0
- package/src/constitutional/ci-reporter.js +336 -0
- package/src/constitutional/index.js +22 -0
- package/src/constitutional/phase-minus-one.js +404 -0
- package/src/constitutional/steering-sync.js +473 -0
- package/src/dashboard/index.js +20 -0
- package/src/dashboard/sprint-planner.js +361 -0
- package/src/dashboard/sprint-reporter.js +378 -0
- package/src/dashboard/transition-recorder.js +209 -0
- package/src/dashboard/workflow-dashboard.js +434 -0
- package/src/enterprise/error-recovery.js +524 -0
- package/src/enterprise/experiment-report.js +573 -0
- package/src/enterprise/index.js +57 -4
- package/src/enterprise/rollback-manager.js +584 -0
- package/src/enterprise/tech-article.js +509 -0
- package/src/traceability/extractor.js +294 -0
- package/src/traceability/gap-detector.js +230 -0
- package/src/traceability/index.js +15 -0
- package/src/traceability/matrix-storage.js +368 -0
|
@@ -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
|
+
};
|