musubi-sdd 6.2.1 → 6.3.0
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 +3 -3
- package/README.md +3 -3
- package/bin/musubi-dashboard.js +23 -14
- package/bin/musubi-design.js +3 -3
- package/bin/musubi-gaps.js +9 -9
- package/bin/musubi-init.js +14 -1310
- package/bin/musubi-requirements.js +1 -1
- package/bin/musubi-tasks.js +5 -5
- package/bin/musubi-trace.js +23 -23
- package/bin/musubi-upgrade.js +400 -0
- package/bin/musubi.js +16 -1
- package/package.json +4 -3
- package/src/analyzers/gap-detector.js +3 -3
- package/src/analyzers/traceability.js +17 -17
- package/src/cli/dashboard-cli.js +54 -60
- package/src/cli/init-generators.js +464 -0
- package/src/cli/init-helpers.js +884 -0
- package/src/constitutional/checker.js +67 -65
- package/src/constitutional/ci-reporter.js +58 -49
- package/src/constitutional/index.js +2 -2
- package/src/constitutional/phase-minus-one.js +24 -27
- package/src/constitutional/steering-sync.js +29 -40
- package/src/dashboard/index.js +2 -2
- package/src/dashboard/sprint-planner.js +17 -19
- package/src/dashboard/sprint-reporter.js +47 -38
- package/src/dashboard/transition-recorder.js +12 -18
- package/src/dashboard/workflow-dashboard.js +28 -39
- package/src/enterprise/error-recovery.js +109 -49
- package/src/enterprise/experiment-report.js +63 -37
- package/src/enterprise/index.js +5 -5
- package/src/enterprise/rollback-manager.js +28 -29
- package/src/enterprise/tech-article.js +41 -35
- package/src/generators/design.js +3 -3
- package/src/generators/requirements.js +5 -3
- package/src/generators/tasks.js +2 -2
- package/src/integrations/platforms.js +1 -1
- package/src/templates/agents/claude-code/CLAUDE.md +1 -1
- package/src/templates/agents/claude-code/skills/design-reviewer/SKILL.md +132 -113
- package/src/templates/agents/claude-code/skills/requirements-reviewer/SKILL.md +85 -56
- package/src/templates/agents/codex/AGENTS.md +2 -2
- package/src/templates/agents/cursor/AGENTS.md +2 -2
- package/src/templates/agents/gemini-cli/GEMINI.md +2 -2
- package/src/templates/agents/github-copilot/AGENTS.md +2 -2
- package/src/templates/agents/github-copilot/commands/sdd-requirements.prompt.md +23 -4
- package/src/templates/agents/qwen-code/QWEN.md +2 -2
- package/src/templates/agents/shared/AGENTS.md +1 -1
- package/src/templates/agents/windsurf/AGENTS.md +2 -2
- package/src/templates/skills/browser-agent.md +1 -1
- package/src/traceability/extractor.js +22 -21
- package/src/traceability/gap-detector.js +19 -17
- package/src/traceability/index.js +2 -2
- package/src/traceability/matrix-storage.js +20 -22
- package/src/validators/constitution.js +5 -2
- package/src/validators/critic-system.js +6 -6
- package/src/validators/traceability-validator.js +3 -3
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MUSUBI Initialization Helpers
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for musubi-init.js
|
|
5
|
+
* Extracted to reduce file size and improve maintainability
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* External specification reference handler
|
|
14
|
+
* Supports: URL (http/https), local file path, Git repository
|
|
15
|
+
* @param {string} specSource - Specification source (URL, file path, or git URL)
|
|
16
|
+
* @returns {object} Parsed specification with metadata
|
|
17
|
+
*/
|
|
18
|
+
async function fetchExternalSpec(specSource) {
|
|
19
|
+
const result = {
|
|
20
|
+
source: specSource,
|
|
21
|
+
type: 'unknown',
|
|
22
|
+
content: null,
|
|
23
|
+
metadata: {},
|
|
24
|
+
error: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Determine source type
|
|
29
|
+
if (specSource.startsWith('http://') || specSource.startsWith('https://')) {
|
|
30
|
+
result.type = 'url';
|
|
31
|
+
const https = require('https');
|
|
32
|
+
const http = require('http');
|
|
33
|
+
const protocol = specSource.startsWith('https://') ? https : http;
|
|
34
|
+
|
|
35
|
+
result.content = await new Promise((resolve, reject) => {
|
|
36
|
+
protocol
|
|
37
|
+
.get(specSource, res => {
|
|
38
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
39
|
+
// Handle redirect
|
|
40
|
+
fetchExternalSpec(res.headers.location).then(r => resolve(r.content));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (res.statusCode !== 200) {
|
|
44
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let data = '';
|
|
48
|
+
res.on('data', chunk => (data += chunk));
|
|
49
|
+
res.on('end', () => resolve(data));
|
|
50
|
+
})
|
|
51
|
+
.on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Extract metadata from URL
|
|
55
|
+
result.metadata.url = specSource;
|
|
56
|
+
result.metadata.fetchedAt = new Date().toISOString();
|
|
57
|
+
} else if (specSource.startsWith('git://') || specSource.includes('.git')) {
|
|
58
|
+
result.type = 'git';
|
|
59
|
+
result.metadata.repository = specSource;
|
|
60
|
+
// For Git repos, we'll store the reference for later cloning
|
|
61
|
+
result.content = `# External Specification Reference\n\nRepository: ${specSource}\n\n> Clone this repository to access the full specification.\n`;
|
|
62
|
+
} else if (fs.existsSync(specSource)) {
|
|
63
|
+
result.type = 'file';
|
|
64
|
+
result.content = await fs.readFile(specSource, 'utf8');
|
|
65
|
+
result.metadata.path = path.resolve(specSource);
|
|
66
|
+
result.metadata.readAt = new Date().toISOString();
|
|
67
|
+
} else {
|
|
68
|
+
result.error = `Specification source not found: ${specSource}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try to parse specification format
|
|
72
|
+
if (result.content) {
|
|
73
|
+
result.metadata.format = detectSpecFormat(result.content, specSource);
|
|
74
|
+
result.metadata.summary = extractSpecSummary(result.content);
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
result.error = err.message;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse GitHub repository reference
|
|
85
|
+
* Supports formats:
|
|
86
|
+
* - owner/repo
|
|
87
|
+
* - https://github.com/owner/repo
|
|
88
|
+
* - git@github.com:owner/repo.git
|
|
89
|
+
* @param {string} repoRef - Repository reference string
|
|
90
|
+
* @returns {object} Parsed repository info
|
|
91
|
+
*/
|
|
92
|
+
function parseGitHubRepo(repoRef) {
|
|
93
|
+
let owner = '';
|
|
94
|
+
let repo = '';
|
|
95
|
+
let branch = 'main';
|
|
96
|
+
let repoPath = '';
|
|
97
|
+
|
|
98
|
+
// Handle owner/repo format
|
|
99
|
+
const simpleMatch = repoRef.match(/^([^/]+)\/([^/@#]+)(?:@([^#]+))?(?:#(.+))?$/);
|
|
100
|
+
if (simpleMatch) {
|
|
101
|
+
owner = simpleMatch[1];
|
|
102
|
+
repo = simpleMatch[2];
|
|
103
|
+
branch = simpleMatch[3] || 'main';
|
|
104
|
+
repoPath = simpleMatch[4] || '';
|
|
105
|
+
return { owner, repo, branch, path: repoPath, url: `https://github.com/${owner}/${repo}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle https://github.com/owner/repo format
|
|
109
|
+
const httpsMatch = repoRef.match(
|
|
110
|
+
/github\.com\/([^/]+)\/([^/@#\s]+?)(?:\.git)?(?:@([^#]+))?(?:#(.+))?$/
|
|
111
|
+
);
|
|
112
|
+
if (httpsMatch) {
|
|
113
|
+
owner = httpsMatch[1];
|
|
114
|
+
repo = httpsMatch[2];
|
|
115
|
+
branch = httpsMatch[3] || 'main';
|
|
116
|
+
repoPath = httpsMatch[4] || '';
|
|
117
|
+
return { owner, repo, branch, path: repoPath, url: `https://github.com/${owner}/${repo}` };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle git@github.com:owner/repo.git format
|
|
121
|
+
const sshMatch = repoRef.match(
|
|
122
|
+
/git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?(?:@([^#]+))?(?:#(.+))?$/
|
|
123
|
+
);
|
|
124
|
+
if (sshMatch) {
|
|
125
|
+
owner = sshMatch[1];
|
|
126
|
+
repo = sshMatch[2];
|
|
127
|
+
branch = sshMatch[3] || 'main';
|
|
128
|
+
repoPath = sshMatch[4] || '';
|
|
129
|
+
return { owner, repo, branch, path: repoPath, url: `https://github.com/${owner}/${repo}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { error: `Invalid GitHub repository format: ${repoRef}` };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fetch GitHub repository metadata and key files
|
|
137
|
+
* @param {string} repoRef - Repository reference (owner/repo, URL, etc.)
|
|
138
|
+
* @returns {object} Repository data with structure and key files
|
|
139
|
+
*/
|
|
140
|
+
async function fetchGitHubRepo(repoRef) {
|
|
141
|
+
const parsed = parseGitHubRepo(repoRef);
|
|
142
|
+
if (parsed.error) {
|
|
143
|
+
return { source: repoRef, error: parsed.error };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { owner, repo, branch, path: subPath } = parsed;
|
|
147
|
+
const https = require('https');
|
|
148
|
+
|
|
149
|
+
const result = {
|
|
150
|
+
source: repoRef,
|
|
151
|
+
owner,
|
|
152
|
+
repo,
|
|
153
|
+
branch,
|
|
154
|
+
url: parsed.url,
|
|
155
|
+
metadata: {},
|
|
156
|
+
files: {},
|
|
157
|
+
structure: [],
|
|
158
|
+
improvements: [],
|
|
159
|
+
error: null,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Helper to fetch from GitHub API
|
|
163
|
+
const fetchGitHubAPI = endpoint =>
|
|
164
|
+
new Promise((resolve, reject) => {
|
|
165
|
+
const options = {
|
|
166
|
+
hostname: 'api.github.com',
|
|
167
|
+
path: endpoint,
|
|
168
|
+
headers: {
|
|
169
|
+
'User-Agent': 'MUSUBI-SDD',
|
|
170
|
+
Accept: 'application/vnd.github.v3+json',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Add GitHub token if available
|
|
175
|
+
if (process.env.GITHUB_TOKEN) {
|
|
176
|
+
options.headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
https
|
|
180
|
+
.get(options, res => {
|
|
181
|
+
let data = '';
|
|
182
|
+
res.on('data', chunk => (data += chunk));
|
|
183
|
+
res.on('end', () => {
|
|
184
|
+
if (res.statusCode === 200) {
|
|
185
|
+
try {
|
|
186
|
+
resolve(JSON.parse(data));
|
|
187
|
+
} catch {
|
|
188
|
+
reject(new Error('Invalid JSON response'));
|
|
189
|
+
}
|
|
190
|
+
} else if (res.statusCode === 404) {
|
|
191
|
+
reject(new Error(`Repository not found: ${owner}/${repo}`));
|
|
192
|
+
} else if (res.statusCode === 403) {
|
|
193
|
+
reject(
|
|
194
|
+
new Error('GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable.')
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
reject(new Error(`GitHub API error: ${res.statusCode}`));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
})
|
|
201
|
+
.on('error', reject);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Fetch raw file content
|
|
205
|
+
const fetchRawFile = filePath =>
|
|
206
|
+
new Promise((resolve, reject) => {
|
|
207
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
|
|
208
|
+
https
|
|
209
|
+
.get(rawUrl, res => {
|
|
210
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
211
|
+
https
|
|
212
|
+
.get(res.headers.location, res2 => {
|
|
213
|
+
let data = '';
|
|
214
|
+
res2.on('data', chunk => (data += chunk));
|
|
215
|
+
res2.on('end', () => resolve(data));
|
|
216
|
+
})
|
|
217
|
+
.on('error', reject);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (res.statusCode !== 200) {
|
|
221
|
+
resolve(null); // File not found is OK
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
let data = '';
|
|
225
|
+
res.on('data', chunk => (data += chunk));
|
|
226
|
+
res.on('end', () => resolve(data));
|
|
227
|
+
})
|
|
228
|
+
.on('error', reject);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
// Fetch repository metadata
|
|
233
|
+
const repoData = await fetchGitHubAPI(`/repos/${owner}/${repo}`);
|
|
234
|
+
result.metadata = {
|
|
235
|
+
name: repoData.name,
|
|
236
|
+
description: repoData.description,
|
|
237
|
+
language: repoData.language,
|
|
238
|
+
stars: repoData.stargazers_count,
|
|
239
|
+
topics: repoData.topics || [],
|
|
240
|
+
license: repoData.license?.spdx_id,
|
|
241
|
+
defaultBranch: repoData.default_branch,
|
|
242
|
+
updatedAt: repoData.updated_at,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Fetch directory structure (root level)
|
|
246
|
+
const treePath = subPath
|
|
247
|
+
? `/repos/${owner}/${repo}/contents/${subPath}`
|
|
248
|
+
: `/repos/${owner}/${repo}/contents`;
|
|
249
|
+
try {
|
|
250
|
+
const contents = await fetchGitHubAPI(treePath);
|
|
251
|
+
if (Array.isArray(contents)) {
|
|
252
|
+
result.structure = contents.map(item => ({
|
|
253
|
+
name: item.name,
|
|
254
|
+
type: item.type,
|
|
255
|
+
path: item.path,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
// Ignore structure fetch errors
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fetch key files for analysis
|
|
263
|
+
const keyFiles = [
|
|
264
|
+
'README.md',
|
|
265
|
+
'package.json',
|
|
266
|
+
'Cargo.toml',
|
|
267
|
+
'pyproject.toml',
|
|
268
|
+
'go.mod',
|
|
269
|
+
'pom.xml',
|
|
270
|
+
'.github/CODEOWNERS',
|
|
271
|
+
'ARCHITECTURE.md',
|
|
272
|
+
'CONTRIBUTING.md',
|
|
273
|
+
'docs/architecture.md',
|
|
274
|
+
'src/lib.rs',
|
|
275
|
+
'src/index.ts',
|
|
276
|
+
'src/main.ts',
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
for (const file of keyFiles) {
|
|
280
|
+
const filePath = subPath ? `${subPath}/${file}` : file;
|
|
281
|
+
try {
|
|
282
|
+
const content = await fetchRawFile(filePath);
|
|
283
|
+
if (content) {
|
|
284
|
+
result.files[file] = content.slice(0, 10000); // Limit content size
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Ignore individual file fetch errors
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (err) {
|
|
291
|
+
result.error = err.message;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Fetch multiple GitHub repositories
|
|
299
|
+
* @param {string[]} repos - Array of repository references
|
|
300
|
+
* @returns {object[]} Array of repository data
|
|
301
|
+
*/
|
|
302
|
+
async function fetchGitHubRepos(repos) {
|
|
303
|
+
const results = [];
|
|
304
|
+
|
|
305
|
+
for (const repoRef of repos) {
|
|
306
|
+
console.log(chalk.cyan(` 📦 Fetching ${repoRef}...`));
|
|
307
|
+
const repoData = await fetchGitHubRepo(repoRef);
|
|
308
|
+
|
|
309
|
+
if (repoData.error) {
|
|
310
|
+
console.log(chalk.yellow(` ⚠️ ${repoData.error}`));
|
|
311
|
+
} else {
|
|
312
|
+
console.log(
|
|
313
|
+
chalk.green(
|
|
314
|
+
` ✓ ${repoData.metadata.name || repoData.repo} (${repoData.metadata.language || 'unknown'})`
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
if (repoData.metadata.description) {
|
|
318
|
+
console.log(chalk.gray(` ${repoData.metadata.description.slice(0, 80)}`));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
results.push(repoData);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return results;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Analyze repositories for improvement suggestions
|
|
330
|
+
* @param {object[]} repos - Array of fetched repository data
|
|
331
|
+
* @returns {object} Analysis results with patterns and suggestions
|
|
332
|
+
*/
|
|
333
|
+
function analyzeReposForImprovements(repos) {
|
|
334
|
+
const analysis = {
|
|
335
|
+
patterns: [],
|
|
336
|
+
architectures: [],
|
|
337
|
+
technologies: [],
|
|
338
|
+
configurations: [],
|
|
339
|
+
suggestions: [],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
for (const repo of repos) {
|
|
343
|
+
if (repo.error) continue;
|
|
344
|
+
|
|
345
|
+
// Detect architecture patterns from structure
|
|
346
|
+
const dirs = repo.structure.filter(s => s.type === 'dir').map(s => s.name);
|
|
347
|
+
const files = repo.structure.filter(s => s.type === 'file').map(s => s.name);
|
|
348
|
+
|
|
349
|
+
// Check for Clean Architecture
|
|
350
|
+
if (dirs.some(d => ['domain', 'application', 'infrastructure', 'interface'].includes(d))) {
|
|
351
|
+
analysis.architectures.push({
|
|
352
|
+
repo: repo.repo,
|
|
353
|
+
pattern: 'clean-architecture',
|
|
354
|
+
evidence: dirs.filter(d =>
|
|
355
|
+
['domain', 'application', 'infrastructure', 'interface'].includes(d)
|
|
356
|
+
),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check for Hexagonal Architecture
|
|
361
|
+
if (dirs.some(d => ['adapters', 'ports', 'core', 'hexagon'].includes(d))) {
|
|
362
|
+
analysis.architectures.push({
|
|
363
|
+
repo: repo.repo,
|
|
364
|
+
pattern: 'hexagonal',
|
|
365
|
+
evidence: dirs.filter(d => ['adapters', 'ports', 'core', 'hexagon'].includes(d)),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check for DDD patterns
|
|
370
|
+
if (
|
|
371
|
+
dirs.some(d =>
|
|
372
|
+
['aggregates', 'entities', 'valueobjects', 'repositories', 'services'].includes(
|
|
373
|
+
d.toLowerCase()
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
) {
|
|
377
|
+
analysis.patterns.push({
|
|
378
|
+
repo: repo.repo,
|
|
379
|
+
pattern: 'domain-driven-design',
|
|
380
|
+
evidence: dirs,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check for monorepo patterns
|
|
385
|
+
if (
|
|
386
|
+
dirs.includes('packages') ||
|
|
387
|
+
dirs.includes('apps') ||
|
|
388
|
+
files.includes('pnpm-workspace.yaml')
|
|
389
|
+
) {
|
|
390
|
+
analysis.patterns.push({
|
|
391
|
+
repo: repo.repo,
|
|
392
|
+
pattern: 'monorepo',
|
|
393
|
+
evidence: dirs.filter(d => ['packages', 'apps', 'libs'].includes(d)),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Analyze package.json for technologies
|
|
398
|
+
if (repo.files['package.json']) {
|
|
399
|
+
try {
|
|
400
|
+
const pkg = JSON.parse(repo.files['package.json']);
|
|
401
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
402
|
+
|
|
403
|
+
// Detect frameworks
|
|
404
|
+
if (deps['react']) analysis.technologies.push({ repo: repo.repo, tech: 'react' });
|
|
405
|
+
if (deps['vue']) analysis.technologies.push({ repo: repo.repo, tech: 'vue' });
|
|
406
|
+
if (deps['@angular/core']) analysis.technologies.push({ repo: repo.repo, tech: 'angular' });
|
|
407
|
+
if (deps['express']) analysis.technologies.push({ repo: repo.repo, tech: 'express' });
|
|
408
|
+
if (deps['fastify']) analysis.technologies.push({ repo: repo.repo, tech: 'fastify' });
|
|
409
|
+
if (deps['next']) analysis.technologies.push({ repo: repo.repo, tech: 'nextjs' });
|
|
410
|
+
if (deps['typescript']) analysis.technologies.push({ repo: repo.repo, tech: 'typescript' });
|
|
411
|
+
|
|
412
|
+
// Detect testing frameworks
|
|
413
|
+
if (deps['jest']) analysis.configurations.push({ repo: repo.repo, config: 'jest' });
|
|
414
|
+
if (deps['vitest']) analysis.configurations.push({ repo: repo.repo, config: 'vitest' });
|
|
415
|
+
if (deps['mocha']) analysis.configurations.push({ repo: repo.repo, config: 'mocha' });
|
|
416
|
+
|
|
417
|
+
// Detect linting/formatting
|
|
418
|
+
if (deps['eslint']) analysis.configurations.push({ repo: repo.repo, config: 'eslint' });
|
|
419
|
+
if (deps['prettier']) analysis.configurations.push({ repo: repo.repo, config: 'prettier' });
|
|
420
|
+
if (deps['biome']) analysis.configurations.push({ repo: repo.repo, config: 'biome' });
|
|
421
|
+
} catch {
|
|
422
|
+
// Ignore JSON parse errors
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Analyze Cargo.toml for Rust patterns
|
|
427
|
+
if (repo.files['Cargo.toml']) {
|
|
428
|
+
const cargo = repo.files['Cargo.toml'];
|
|
429
|
+
if (cargo.includes('[workspace]')) {
|
|
430
|
+
analysis.patterns.push({ repo: repo.repo, pattern: 'rust-workspace' });
|
|
431
|
+
}
|
|
432
|
+
if (cargo.includes('tokio')) analysis.technologies.push({ repo: repo.repo, tech: 'tokio' });
|
|
433
|
+
if (cargo.includes('actix')) analysis.technologies.push({ repo: repo.repo, tech: 'actix' });
|
|
434
|
+
if (cargo.includes('axum')) analysis.technologies.push({ repo: repo.repo, tech: 'axum' });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Analyze pyproject.toml for Python patterns
|
|
438
|
+
if (repo.files['pyproject.toml']) {
|
|
439
|
+
const pyproj = repo.files['pyproject.toml'];
|
|
440
|
+
if (pyproj.includes('fastapi'))
|
|
441
|
+
analysis.technologies.push({ repo: repo.repo, tech: 'fastapi' });
|
|
442
|
+
if (pyproj.includes('django'))
|
|
443
|
+
analysis.technologies.push({ repo: repo.repo, tech: 'django' });
|
|
444
|
+
if (pyproj.includes('flask')) analysis.technologies.push({ repo: repo.repo, tech: 'flask' });
|
|
445
|
+
if (pyproj.includes('pytest'))
|
|
446
|
+
analysis.configurations.push({ repo: repo.repo, config: 'pytest' });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Extract README insights
|
|
450
|
+
if (repo.files['README.md']) {
|
|
451
|
+
const readme = repo.files['README.md'];
|
|
452
|
+
|
|
453
|
+
// Check for badges that indicate good practices
|
|
454
|
+
if (readme.includes('coverage')) {
|
|
455
|
+
analysis.suggestions.push({
|
|
456
|
+
repo: repo.repo,
|
|
457
|
+
suggestion: 'code-coverage',
|
|
458
|
+
description: 'Implements code coverage tracking',
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
if (readme.includes('CI/CD') || readme.includes('Actions')) {
|
|
462
|
+
analysis.suggestions.push({
|
|
463
|
+
repo: repo.repo,
|
|
464
|
+
suggestion: 'ci-cd',
|
|
465
|
+
description: 'Has CI/CD pipeline configured',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Generate improvement suggestions based on analysis
|
|
472
|
+
if (analysis.architectures.length > 0) {
|
|
473
|
+
const archCounts = {};
|
|
474
|
+
for (const arch of analysis.architectures) {
|
|
475
|
+
archCounts[arch.pattern] = (archCounts[arch.pattern] || 0) + 1;
|
|
476
|
+
}
|
|
477
|
+
const mostCommon = Object.entries(archCounts).sort((a, b) => b[1] - a[1])[0];
|
|
478
|
+
if (mostCommon) {
|
|
479
|
+
analysis.suggestions.push({
|
|
480
|
+
type: 'architecture',
|
|
481
|
+
suggestion: `Consider using ${mostCommon[0]} pattern`,
|
|
482
|
+
count: mostCommon[1],
|
|
483
|
+
repos: analysis.architectures.filter(a => a.pattern === mostCommon[0]).map(a => a.repo),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (analysis.technologies.length > 0) {
|
|
489
|
+
const techCounts = {};
|
|
490
|
+
for (const tech of analysis.technologies) {
|
|
491
|
+
techCounts[tech.tech] = (techCounts[tech.tech] || 0) + 1;
|
|
492
|
+
}
|
|
493
|
+
const popular = Object.entries(techCounts)
|
|
494
|
+
.sort((a, b) => b[1] - a[1])
|
|
495
|
+
.slice(0, 3);
|
|
496
|
+
for (const [tech, count] of popular) {
|
|
497
|
+
analysis.suggestions.push({
|
|
498
|
+
type: 'technology',
|
|
499
|
+
suggestion: `Consider using ${tech}`,
|
|
500
|
+
count,
|
|
501
|
+
repos: analysis.technologies.filter(t => t.tech === tech).map(t => t.repo),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return analysis;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Save reference repositories analysis to steering/references/
|
|
511
|
+
* @param {object[]} repos - Fetched repository data
|
|
512
|
+
* @param {object} analysis - Analysis results
|
|
513
|
+
* @param {string} projectPath - Target project path
|
|
514
|
+
* @returns {string} Created file path
|
|
515
|
+
*/
|
|
516
|
+
async function saveReferenceRepos(repos, analysis, projectPath) {
|
|
517
|
+
const refsDir = path.join(projectPath, 'steering', 'references');
|
|
518
|
+
await fs.ensureDir(refsDir);
|
|
519
|
+
|
|
520
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
521
|
+
const filename = `github-references-${timestamp}.md`;
|
|
522
|
+
|
|
523
|
+
// Build markdown content
|
|
524
|
+
let content = `# GitHub Reference Repositories
|
|
525
|
+
|
|
526
|
+
> Analyzed on ${new Date().toISOString()}
|
|
527
|
+
|
|
528
|
+
## Referenced Repositories
|
|
529
|
+
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
for (const repo of repos) {
|
|
533
|
+
if (repo.error) {
|
|
534
|
+
content += `### ❌ ${repo.source}\n\n`;
|
|
535
|
+
content += `Error: ${repo.error}\n\n`;
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
content += `### ${repo.metadata.name || repo.repo}\n\n`;
|
|
540
|
+
content += `- **URL**: ${repo.url}\n`;
|
|
541
|
+
content += `- **Language**: ${repo.metadata.language || 'Unknown'}\n`;
|
|
542
|
+
content += `- **Stars**: ${repo.metadata.stars || 0}\n`;
|
|
543
|
+
if (repo.metadata.description) {
|
|
544
|
+
content += `- **Description**: ${repo.metadata.description}\n`;
|
|
545
|
+
}
|
|
546
|
+
if (repo.metadata.topics && repo.metadata.topics.length > 0) {
|
|
547
|
+
content += `- **Topics**: ${repo.metadata.topics.join(', ')}\n`;
|
|
548
|
+
}
|
|
549
|
+
if (repo.metadata.license) {
|
|
550
|
+
content += `- **License**: ${repo.metadata.license}\n`;
|
|
551
|
+
}
|
|
552
|
+
content += '\n';
|
|
553
|
+
|
|
554
|
+
// Structure
|
|
555
|
+
if (repo.structure.length > 0) {
|
|
556
|
+
content += '**Directory Structure:**\n\n';
|
|
557
|
+
content += '```\n';
|
|
558
|
+
for (const item of repo.structure.slice(0, 20)) {
|
|
559
|
+
content += `${item.type === 'dir' ? '📁' : '📄'} ${item.name}\n`;
|
|
560
|
+
}
|
|
561
|
+
if (repo.structure.length > 20) {
|
|
562
|
+
content += `... and ${repo.structure.length - 20} more items\n`;
|
|
563
|
+
}
|
|
564
|
+
content += '```\n\n';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Analysis section
|
|
569
|
+
content += `## Analysis Results
|
|
570
|
+
|
|
571
|
+
### Architecture Patterns Detected
|
|
572
|
+
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
if (analysis.architectures.length > 0) {
|
|
576
|
+
for (const arch of analysis.architectures) {
|
|
577
|
+
content += `- **${arch.pattern}** in \`${arch.repo}\`\n`;
|
|
578
|
+
content += ` - Evidence: ${arch.evidence.join(', ')}\n`;
|
|
579
|
+
}
|
|
580
|
+
} else {
|
|
581
|
+
content += '_No specific architecture patterns detected_\n';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
content += `\n### Design Patterns
|
|
585
|
+
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
if (analysis.patterns.length > 0) {
|
|
589
|
+
for (const pattern of analysis.patterns) {
|
|
590
|
+
content += `- **${pattern.pattern}** in \`${pattern.repo}\`\n`;
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
content += '_No specific design patterns detected_\n';
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
content += `\n### Technologies Used
|
|
597
|
+
|
|
598
|
+
`;
|
|
599
|
+
|
|
600
|
+
if (analysis.technologies.length > 0) {
|
|
601
|
+
const techByRepo = {};
|
|
602
|
+
for (const tech of analysis.technologies) {
|
|
603
|
+
if (!techByRepo[tech.repo]) techByRepo[tech.repo] = [];
|
|
604
|
+
techByRepo[tech.repo].push(tech.tech);
|
|
605
|
+
}
|
|
606
|
+
for (const [repoName, techs] of Object.entries(techByRepo)) {
|
|
607
|
+
content += `- **${repoName}**: ${techs.join(', ')}\n`;
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
content += '_No specific technologies detected_\n';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
content += `\n### Configurations
|
|
614
|
+
|
|
615
|
+
`;
|
|
616
|
+
|
|
617
|
+
if (analysis.configurations.length > 0) {
|
|
618
|
+
const configByRepo = {};
|
|
619
|
+
for (const config of analysis.configurations) {
|
|
620
|
+
if (!configByRepo[config.repo]) configByRepo[config.repo] = [];
|
|
621
|
+
configByRepo[config.repo].push(config.config);
|
|
622
|
+
}
|
|
623
|
+
for (const [repoName, configs] of Object.entries(configByRepo)) {
|
|
624
|
+
content += `- **${repoName}**: ${configs.join(', ')}\n`;
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
content += '_No specific configurations detected_\n';
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
content += `\n## Improvement Suggestions
|
|
631
|
+
|
|
632
|
+
Based on the referenced repositories, consider the following improvements:
|
|
633
|
+
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
if (analysis.suggestions.length > 0) {
|
|
637
|
+
let i = 1;
|
|
638
|
+
for (const suggestion of analysis.suggestions) {
|
|
639
|
+
if (suggestion.type === 'architecture') {
|
|
640
|
+
content += `${i}. **Architecture**: ${suggestion.suggestion}\n`;
|
|
641
|
+
content += ` - Found in ${suggestion.count} repository(ies): ${suggestion.repos.join(', ')}\n\n`;
|
|
642
|
+
} else if (suggestion.type === 'technology') {
|
|
643
|
+
content += `${i}. **Technology**: ${suggestion.suggestion}\n`;
|
|
644
|
+
content += ` - Used by ${suggestion.count} repository(ies): ${suggestion.repos.join(', ')}\n\n`;
|
|
645
|
+
} else {
|
|
646
|
+
content += `${i}. **${suggestion.suggestion}**: ${suggestion.description}\n`;
|
|
647
|
+
content += ` - Found in: ${suggestion.repo}\n\n`;
|
|
648
|
+
}
|
|
649
|
+
i++;
|
|
650
|
+
}
|
|
651
|
+
} else {
|
|
652
|
+
content += '_No specific suggestions generated_\n';
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
content += `
|
|
656
|
+
---
|
|
657
|
+
*Generated by MUSUBI SDD - GitHub Reference Analysis*
|
|
658
|
+
`;
|
|
659
|
+
|
|
660
|
+
await fs.writeFile(path.join(refsDir, filename), content);
|
|
661
|
+
return filename;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Detect specification format from content and filename
|
|
666
|
+
*/
|
|
667
|
+
function detectSpecFormat(content, source) {
|
|
668
|
+
const ext = path.extname(source).toLowerCase();
|
|
669
|
+
if (ext === '.json') return 'json';
|
|
670
|
+
if (ext === '.yaml' || ext === '.yml') return 'yaml';
|
|
671
|
+
if (ext === '.md') return 'markdown';
|
|
672
|
+
if (ext === '.rst') return 'rst';
|
|
673
|
+
if (ext === '.html') return 'html';
|
|
674
|
+
|
|
675
|
+
// Try to detect from content
|
|
676
|
+
if (content.trim().startsWith('{')) return 'json';
|
|
677
|
+
if (content.includes('openapi:') || content.includes('swagger:')) return 'openapi';
|
|
678
|
+
if (content.includes('asyncapi:')) return 'asyncapi';
|
|
679
|
+
if (content.includes('# ')) return 'markdown';
|
|
680
|
+
|
|
681
|
+
return 'text';
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Extract summary from specification content
|
|
686
|
+
*/
|
|
687
|
+
function extractSpecSummary(content) {
|
|
688
|
+
// Extract first heading and description
|
|
689
|
+
const lines = content.split('\n').slice(0, 50);
|
|
690
|
+
let title = '';
|
|
691
|
+
let description = '';
|
|
692
|
+
|
|
693
|
+
for (const line of lines) {
|
|
694
|
+
if (!title && line.startsWith('# ')) {
|
|
695
|
+
title = line.replace('# ', '').trim();
|
|
696
|
+
} else if (title && !description && line.trim() && !line.startsWith('#')) {
|
|
697
|
+
description = line.trim().slice(0, 200);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return { title, description };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Save external specification reference to steering/specs/
|
|
707
|
+
*/
|
|
708
|
+
async function saveSpecReference(specResult, projectPath) {
|
|
709
|
+
const specsDir = path.join(projectPath, 'steering', 'specs');
|
|
710
|
+
await fs.ensureDir(specsDir);
|
|
711
|
+
|
|
712
|
+
// Create spec reference file
|
|
713
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
714
|
+
const safeName = specResult.metadata.summary?.title
|
|
715
|
+
? specResult.metadata.summary.title
|
|
716
|
+
.toLowerCase()
|
|
717
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
718
|
+
.slice(0, 50)
|
|
719
|
+
: 'external-spec';
|
|
720
|
+
const filename = `${safeName}-${timestamp}.md`;
|
|
721
|
+
|
|
722
|
+
const refContent = `# External Specification Reference
|
|
723
|
+
|
|
724
|
+
## Source Information
|
|
725
|
+
|
|
726
|
+
- **Type**: ${specResult.type}
|
|
727
|
+
- **Source**: ${specResult.source}
|
|
728
|
+
- **Format**: ${specResult.metadata.format || 'unknown'}
|
|
729
|
+
- **Fetched**: ${specResult.metadata.fetchedAt || specResult.metadata.readAt || 'N/A'}
|
|
730
|
+
|
|
731
|
+
## Summary
|
|
732
|
+
|
|
733
|
+
${specResult.metadata.summary?.title ? `**Title**: ${specResult.metadata.summary.title}` : ''}
|
|
734
|
+
${specResult.metadata.summary?.description ? `\n**Description**: ${specResult.metadata.summary.description}` : ''}
|
|
735
|
+
|
|
736
|
+
## Integration Notes
|
|
737
|
+
|
|
738
|
+
This specification is used as a reference for:
|
|
739
|
+
- Requirements analysis
|
|
740
|
+
- Architecture design
|
|
741
|
+
- API design
|
|
742
|
+
- Compliance validation
|
|
743
|
+
|
|
744
|
+
## Original Content
|
|
745
|
+
|
|
746
|
+
\`\`\`${specResult.metadata.format || 'text'}
|
|
747
|
+
${specResult.content?.slice(0, 5000) || 'Content not available'}${specResult.content?.length > 5000 ? '\n\n... (truncated, see original source)' : ''}
|
|
748
|
+
\`\`\`
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
*Generated by MUSUBI SDD - External Specification Reference*
|
|
752
|
+
`;
|
|
753
|
+
|
|
754
|
+
await fs.writeFile(path.join(specsDir, filename), refContent);
|
|
755
|
+
return filename;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Language recommendation engine
|
|
760
|
+
* @param {object} requirements - User's answers about app types, performance, expertise
|
|
761
|
+
* @returns {Array} Recommended languages with reasons
|
|
762
|
+
*/
|
|
763
|
+
function recommendLanguages(requirements) {
|
|
764
|
+
const { appTypes, performanceNeeds, teamExpertise } = requirements;
|
|
765
|
+
const scores = {};
|
|
766
|
+
const reasons = {};
|
|
767
|
+
|
|
768
|
+
// Initialize scores
|
|
769
|
+
const allLangs = [
|
|
770
|
+
'javascript',
|
|
771
|
+
'python',
|
|
772
|
+
'rust',
|
|
773
|
+
'go',
|
|
774
|
+
'java',
|
|
775
|
+
'csharp',
|
|
776
|
+
'cpp',
|
|
777
|
+
'swift',
|
|
778
|
+
'ruby',
|
|
779
|
+
'php',
|
|
780
|
+
];
|
|
781
|
+
for (const lang of allLangs) {
|
|
782
|
+
scores[lang] = 0;
|
|
783
|
+
reasons[lang] = [];
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Score by application type
|
|
787
|
+
const appTypeScores = {
|
|
788
|
+
'web-frontend': { javascript: 10, reason: 'Best ecosystem for web frontend' },
|
|
789
|
+
'web-backend': {
|
|
790
|
+
javascript: 6,
|
|
791
|
+
python: 7,
|
|
792
|
+
go: 8,
|
|
793
|
+
rust: 7,
|
|
794
|
+
java: 7,
|
|
795
|
+
csharp: 6,
|
|
796
|
+
ruby: 5,
|
|
797
|
+
php: 5,
|
|
798
|
+
reason: 'Strong backend frameworks',
|
|
799
|
+
},
|
|
800
|
+
cli: { rust: 9, go: 9, python: 6, reason: 'Fast startup, single binary' },
|
|
801
|
+
desktop: { rust: 7, csharp: 8, cpp: 7, swift: 6, java: 6, reason: 'Native GUI support' },
|
|
802
|
+
mobile: { swift: 9, java: 8, javascript: 6, reason: 'Mobile platform support' },
|
|
803
|
+
data: { python: 10, rust: 6, reason: 'Rich data science ecosystem' },
|
|
804
|
+
ml: { python: 10, rust: 5, cpp: 5, reason: 'ML/AI libraries and frameworks' },
|
|
805
|
+
embedded: { rust: 10, cpp: 9, reason: 'Memory safety, no runtime' },
|
|
806
|
+
game: { cpp: 9, csharp: 8, rust: 6, reason: 'Game engine support' },
|
|
807
|
+
systems: { rust: 10, go: 8, cpp: 9, reason: 'Systems programming' },
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
for (const appType of appTypes || []) {
|
|
811
|
+
const typeScores = appTypeScores[appType];
|
|
812
|
+
if (typeScores) {
|
|
813
|
+
for (const [lang, score] of Object.entries(typeScores)) {
|
|
814
|
+
if (typeof score === 'number') {
|
|
815
|
+
scores[lang] += score;
|
|
816
|
+
if (!reasons[lang].includes(typeScores.reason)) {
|
|
817
|
+
reasons[lang].push(typeScores.reason);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Score by performance needs
|
|
825
|
+
if (performanceNeeds === 'high') {
|
|
826
|
+
scores.rust += 8;
|
|
827
|
+
scores.go += 6;
|
|
828
|
+
scores.cpp += 7;
|
|
829
|
+
reasons.rust.push('High performance, zero-cost abstractions');
|
|
830
|
+
reasons.go.push('Fast compilation, efficient runtime');
|
|
831
|
+
} else if (performanceNeeds === 'rapid') {
|
|
832
|
+
scores.python += 5;
|
|
833
|
+
scores.javascript += 5;
|
|
834
|
+
scores.ruby += 4;
|
|
835
|
+
reasons.python.push('Rapid development, extensive libraries');
|
|
836
|
+
reasons.javascript.push('Fast iteration, universal runtime');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Boost by team expertise
|
|
840
|
+
for (const lang of teamExpertise || []) {
|
|
841
|
+
scores[lang] += 5;
|
|
842
|
+
reasons[lang].push('Team has expertise');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Sort and return top recommendations
|
|
846
|
+
const sorted = Object.entries(scores)
|
|
847
|
+
.filter(([, score]) => score > 0)
|
|
848
|
+
.sort((a, b) => b[1] - a[1])
|
|
849
|
+
.slice(0, 3);
|
|
850
|
+
|
|
851
|
+
const langInfo = {
|
|
852
|
+
javascript: { name: 'JavaScript/TypeScript', emoji: '🟨' },
|
|
853
|
+
python: { name: 'Python', emoji: '🐍' },
|
|
854
|
+
rust: { name: 'Rust', emoji: '🦀' },
|
|
855
|
+
go: { name: 'Go', emoji: '🐹' },
|
|
856
|
+
java: { name: 'Java/Kotlin', emoji: '☕' },
|
|
857
|
+
csharp: { name: 'C#/.NET', emoji: '💜' },
|
|
858
|
+
cpp: { name: 'C/C++', emoji: '⚙️' },
|
|
859
|
+
swift: { name: 'Swift', emoji: '🍎' },
|
|
860
|
+
ruby: { name: 'Ruby', emoji: '💎' },
|
|
861
|
+
php: { name: 'PHP', emoji: '🐘' },
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
return sorted.map(([lang]) => ({
|
|
865
|
+
value: lang,
|
|
866
|
+
name: langInfo[lang].name,
|
|
867
|
+
emoji: langInfo[lang].emoji,
|
|
868
|
+
reason: reasons[lang].slice(0, 2).join('; ') || 'General purpose',
|
|
869
|
+
score: scores[lang],
|
|
870
|
+
}));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
module.exports = {
|
|
874
|
+
fetchExternalSpec,
|
|
875
|
+
parseGitHubRepo,
|
|
876
|
+
fetchGitHubRepo,
|
|
877
|
+
fetchGitHubRepos,
|
|
878
|
+
analyzeReposForImprovements,
|
|
879
|
+
saveReferenceRepos,
|
|
880
|
+
detectSpecFormat,
|
|
881
|
+
extractSpecSummary,
|
|
882
|
+
saveSpecReference,
|
|
883
|
+
recommendLanguages,
|
|
884
|
+
};
|