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.
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Tech Article Generator
3
+ *
4
+ * Generates publication-ready technical articles for various platforms.
5
+ *
6
+ * Requirement: IMP-6.2-006-02
7
+ *
8
+ * @module enterprise/tech-article
9
+ */
10
+
11
+ const fs = require('fs').promises;
12
+ const path = require('path');
13
+
14
+ /**
15
+ * Supported platforms
16
+ */
17
+ const PLATFORM = {
18
+ QIITA: 'qiita',
19
+ ZENN: 'zenn',
20
+ MEDIUM: 'medium',
21
+ DEVTO: 'devto',
22
+ GENERIC: 'generic'
23
+ };
24
+
25
+ /**
26
+ * Article type enum
27
+ */
28
+ const ARTICLE_TYPE = {
29
+ TUTORIAL: 'tutorial',
30
+ DEEP_DIVE: 'deep-dive',
31
+ ANNOUNCEMENT: 'announcement',
32
+ COMPARISON: 'comparison',
33
+ HOW_TO: 'how-to',
34
+ CASE_STUDY: 'case-study'
35
+ };
36
+
37
+ /**
38
+ * Tech Article Generator
39
+ */
40
+ class TechArticleGenerator {
41
+ /**
42
+ * Create a new TechArticleGenerator
43
+ * @param {Object} config - Configuration options
44
+ */
45
+ constructor(config = {}) {
46
+ this.config = {
47
+ outputDir: config.outputDir || 'docs/articles',
48
+ defaultPlatform: config.defaultPlatform || PLATFORM.GENERIC,
49
+ defaultLanguage: config.defaultLanguage || 'ja',
50
+ includeTableOfContents: config.includeTableOfContents !== false,
51
+ includeFrontMatter: config.includeFrontMatter !== false,
52
+ ...config
53
+ };
54
+
55
+ this.templates = this.loadTemplates();
56
+ }
57
+
58
+ /**
59
+ * Load platform templates
60
+ * @returns {Object} Templates by platform
61
+ */
62
+ loadTemplates() {
63
+ return {
64
+ [PLATFORM.QIITA]: {
65
+ frontMatter: (meta) => `---
66
+ title: "${meta.title}"
67
+ tags: [${meta.tags.map(t => `"${t}"`).join(', ')}]
68
+ private: false
69
+ ---`,
70
+ codeBlock: (lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``,
71
+ note: (text, type = 'info') => `:::note ${type}\n${text}\n:::`,
72
+ link: (text, url) => `[${text}](${url})`
73
+ },
74
+ [PLATFORM.ZENN]: {
75
+ frontMatter: (meta) => `---
76
+ title: "${meta.title}"
77
+ emoji: "${meta.emoji || '๐Ÿ“'}"
78
+ type: "${meta.articleType || 'tech'}"
79
+ topics: [${meta.tags.map(t => `"${t}"`).join(', ')}]
80
+ published: true
81
+ ---`,
82
+ codeBlock: (lang, code, filename) => filename
83
+ ? `\`\`\`${lang}:${filename}\n${code}\n\`\`\``
84
+ : `\`\`\`${lang}\n${code}\n\`\`\``,
85
+ note: (text, type = 'info') => `:::message ${type === 'warn' ? 'alert' : ''}\n${text}\n:::`,
86
+ link: (text, url) => `[${text}](${url})`
87
+ },
88
+ [PLATFORM.MEDIUM]: {
89
+ frontMatter: () => '', // Medium doesn't use front matter
90
+ codeBlock: (lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``,
91
+ note: (text) => `> **Note:** ${text}`,
92
+ link: (text, url) => `[${text}](${url})`
93
+ },
94
+ [PLATFORM.DEVTO]: {
95
+ frontMatter: (meta) => `---
96
+ title: "${meta.title}"
97
+ published: true
98
+ description: "${meta.description || ''}"
99
+ tags: ${meta.tags.slice(0, 4).join(', ')}
100
+ cover_image: ${meta.coverImage || ''}
101
+ ---`,
102
+ codeBlock: (lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``,
103
+ note: (text, type = 'info') => `{% ${type === 'warn' ? 'warning' : type} %}\n${text}\n{% end${type === 'warn' ? 'warning' : type} %}`,
104
+ link: (text, url) => `[${text}](${url})`
105
+ },
106
+ [PLATFORM.GENERIC]: {
107
+ frontMatter: (meta) => `---
108
+ title: "${meta.title}"
109
+ date: "${meta.date || new Date().toISOString()}"
110
+ author: "${meta.author || 'MUSUBI SDD'}"
111
+ tags: [${meta.tags.map(t => `"${t}"`).join(', ')}]
112
+ ---`,
113
+ codeBlock: (lang, code) => `\`\`\`${lang}\n${code}\n\`\`\``,
114
+ note: (text) => `> **Note:** ${text}`,
115
+ link: (text, url) => `[${text}](${url})`
116
+ }
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Generate article from template
122
+ * @param {Object} content - Article content
123
+ * @param {Object} options - Generation options
124
+ * @returns {Promise<Object>} Generated article info
125
+ */
126
+ async generate(content, options = {}) {
127
+ const platform = options.platform || this.config.defaultPlatform;
128
+ const template = this.templates[platform] || this.templates[PLATFORM.GENERIC];
129
+
130
+ const metadata = {
131
+ title: content.title || 'Untitled Article',
132
+ description: content.description || '',
133
+ tags: content.tags || [],
134
+ author: content.author || 'MUSUBI SDD',
135
+ date: new Date().toISOString(),
136
+ emoji: content.emoji || '๐Ÿ“',
137
+ articleType: content.articleType || ARTICLE_TYPE.TUTORIAL,
138
+ coverImage: content.coverImage || ''
139
+ };
140
+
141
+ const sections = [];
142
+
143
+ // Front matter
144
+ if (this.config.includeFrontMatter) {
145
+ sections.push(template.frontMatter(metadata));
146
+ sections.push('');
147
+ }
148
+
149
+ // Title (for platforms that don't include it in front matter)
150
+ if (platform === PLATFORM.MEDIUM) {
151
+ sections.push(`# ${metadata.title}`);
152
+ sections.push('');
153
+ }
154
+
155
+ // Introduction
156
+ if (content.introduction) {
157
+ sections.push(content.introduction);
158
+ sections.push('');
159
+ }
160
+
161
+ // Table of Contents
162
+ if (this.config.includeTableOfContents && content.sections) {
163
+ sections.push('## ็›ฎๆฌก');
164
+ sections.push('');
165
+ content.sections.forEach((section, idx) => {
166
+ sections.push(`${idx + 1}. [${section.title}](#${this.slugify(section.title)})`);
167
+ });
168
+ sections.push('');
169
+ }
170
+
171
+ // Main sections
172
+ if (content.sections) {
173
+ for (const section of content.sections) {
174
+ sections.push(`## ${section.title}`);
175
+ sections.push('');
176
+
177
+ if (section.content) {
178
+ sections.push(section.content);
179
+ sections.push('');
180
+ }
181
+
182
+ // Code examples
183
+ if (section.codeExamples) {
184
+ for (const example of section.codeExamples) {
185
+ if (example.description) {
186
+ sections.push(example.description);
187
+ sections.push('');
188
+ }
189
+ sections.push(template.codeBlock(example.language || 'javascript', example.code, example.filename));
190
+ sections.push('');
191
+ }
192
+ }
193
+
194
+ // Notes
195
+ if (section.notes) {
196
+ for (const note of section.notes) {
197
+ sections.push(template.note(note.text, note.type));
198
+ sections.push('');
199
+ }
200
+ }
201
+
202
+ // Subsections
203
+ if (section.subsections) {
204
+ for (const sub of section.subsections) {
205
+ sections.push(`### ${sub.title}`);
206
+ sections.push('');
207
+ if (sub.content) {
208
+ sections.push(sub.content);
209
+ sections.push('');
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ // Benchmarks
217
+ if (content.benchmarks) {
218
+ sections.push('## ใƒ™ใƒณใƒใƒžใƒผใ‚ฏ็ตๆžœ');
219
+ sections.push('');
220
+ sections.push('| ้ …็›ฎ | ๅ€ค |');
221
+ sections.push('|------|-----|');
222
+ for (const [key, value] of Object.entries(content.benchmarks)) {
223
+ sections.push(`| ${key} | ${value} |`);
224
+ }
225
+ sections.push('');
226
+ }
227
+
228
+ // Conclusion
229
+ if (content.conclusion) {
230
+ sections.push('## ใพใจใ‚');
231
+ sections.push('');
232
+ sections.push(content.conclusion);
233
+ sections.push('');
234
+ }
235
+
236
+ // References
237
+ if (content.references && content.references.length > 0) {
238
+ sections.push('## ๅ‚่€ƒๆ–‡็Œฎ');
239
+ sections.push('');
240
+ for (const ref of content.references) {
241
+ sections.push(`- ${template.link(ref.title, ref.url)}`);
242
+ }
243
+ sections.push('');
244
+ }
245
+
246
+ // Footer
247
+ if (content.footer) {
248
+ sections.push('---');
249
+ sections.push('');
250
+ sections.push(content.footer);
251
+ }
252
+
253
+ const article = sections.join('\n');
254
+ const filePath = await this.saveArticle(article, metadata, platform);
255
+
256
+ return {
257
+ article,
258
+ metadata,
259
+ filePath,
260
+ platform,
261
+ wordCount: this.countWords(article),
262
+ readingTime: this.estimateReadingTime(article)
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Generate article from experiment report
268
+ * @param {Object} experimentReport - Experiment report data
269
+ * @param {Object} options - Generation options
270
+ * @returns {Promise<Object>} Generated article info
271
+ */
272
+ async generateFromExperiment(experimentReport, options = {}) {
273
+ const content = {
274
+ title: options.title || `ๅฎŸ้จ“ใƒฌใƒใƒผใƒˆ: ${experimentReport.metadata?.title || 'Unknown'}`,
275
+ description: options.description || 'ๅฎŸ้จ“็ตๆžœใจ่ฆณๅฏŸใฎใƒฌใƒใƒผใƒˆ',
276
+ tags: options.tags || ['experiment', 'test', 'report'],
277
+ introduction: options.introduction || this.generateExperimentIntroduction(experimentReport),
278
+ sections: this.generateExperimentSections(experimentReport),
279
+ benchmarks: this.extractBenchmarks(experimentReport),
280
+ conclusion: options.conclusion || this.generateExperimentConclusion(experimentReport),
281
+ references: options.references || []
282
+ };
283
+
284
+ return this.generate(content, options);
285
+ }
286
+
287
+ /**
288
+ * Generate introduction from experiment
289
+ * @param {Object} report - Experiment report
290
+ * @returns {string} Introduction text
291
+ */
292
+ generateExperimentIntroduction(report) {
293
+ const summary = report.summary || {};
294
+ return `ๆœฌ่จ˜ไบ‹ใงใฏใ€${report.metadata?.title || 'ใƒ†ใ‚นใƒˆ'}ใฎๅฎŸ้จ“็ตๆžœใ‚’ๅ ฑๅ‘Šใ—ใพใ™ใ€‚` +
295
+ `ๅˆ่จˆ${summary.total || 0}ไปถใฎใƒ†ใ‚นใƒˆใ‚’ๅฎŸ่กŒใ—ใ€` +
296
+ `${summary.passRate || '0%'}ใฎใƒ‘ใ‚น็އใ‚’้”ๆˆใ—ใพใ—ใŸใ€‚`;
297
+ }
298
+
299
+ /**
300
+ * Generate sections from experiment
301
+ * @param {Object} report - Experiment report
302
+ * @returns {Array} Sections
303
+ */
304
+ generateExperimentSections(report) {
305
+ const sections = [];
306
+
307
+ // Summary section
308
+ sections.push({
309
+ title: 'ๅฎŸ้จ“ใ‚ตใƒžใƒชใƒผ',
310
+ content: `
311
+ | ๆŒ‡ๆจ™ | ๅ€ค |
312
+ |------|-----|
313
+ | ็ทใƒ†ใ‚นใƒˆๆ•ฐ | ${report.summary?.total || 0} |
314
+ | ๆˆๅŠŸ | ${report.summary?.passed || 0} |
315
+ | ๅคฑๆ•— | ${report.summary?.failed || 0} |
316
+ | ใ‚นใ‚ญใƒƒใƒ— | ${report.summary?.skipped || 0} |
317
+ | ใƒ‘ใ‚น็އ | ${report.summary?.passRate || '0%'} |
318
+ `.trim()
319
+ });
320
+
321
+ // Metrics section (if available)
322
+ if (report.metrics && Object.keys(report.metrics).length > 0) {
323
+ sections.push({
324
+ title: 'ใƒกใƒˆใƒชใ‚ฏใ‚น',
325
+ content: this.formatMetricsSection(report.metrics)
326
+ });
327
+ }
328
+
329
+ // Observations section
330
+ if (report.observations && report.observations.length > 0) {
331
+ sections.push({
332
+ title: '่ฆณๅฏŸ็ตๆžœ',
333
+ content: report.observations.map(o => `- ${o}`).join('\n')
334
+ });
335
+ }
336
+
337
+ return sections;
338
+ }
339
+
340
+ /**
341
+ * Format metrics section
342
+ * @param {Object} metrics - Metrics object
343
+ * @returns {string} Formatted content
344
+ */
345
+ formatMetricsSection(metrics) {
346
+ const lines = [];
347
+
348
+ if (metrics.performance) {
349
+ lines.push('### ใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚น');
350
+ lines.push('');
351
+ lines.push('| ๆŒ‡ๆจ™ | ๅ€ค |');
352
+ lines.push('|------|-----|');
353
+ for (const [key, value] of Object.entries(metrics.performance)) {
354
+ lines.push(`| ${key} | ${value} |`);
355
+ }
356
+ lines.push('');
357
+ }
358
+
359
+ if (metrics.coverage) {
360
+ lines.push('### ใ‚ซใƒใƒฌใƒƒใ‚ธ');
361
+ lines.push('');
362
+ lines.push('| ็จฎๅˆฅ | ๅ€ค |');
363
+ lines.push('|------|-----|');
364
+ for (const [key, value] of Object.entries(metrics.coverage)) {
365
+ lines.push(`| ${key} | ${value} |`);
366
+ }
367
+ }
368
+
369
+ return lines.join('\n');
370
+ }
371
+
372
+ /**
373
+ * Extract benchmarks from report
374
+ * @param {Object} report - Experiment report
375
+ * @returns {Object} Benchmarks
376
+ */
377
+ extractBenchmarks(report) {
378
+ const benchmarks = {};
379
+
380
+ if (report.metrics?.performance) {
381
+ Object.assign(benchmarks, report.metrics.performance);
382
+ }
383
+
384
+ if (report.summary?.duration) {
385
+ benchmarks['Total Duration'] = `${report.summary.duration}ms`;
386
+ }
387
+
388
+ return Object.keys(benchmarks).length > 0 ? benchmarks : null;
389
+ }
390
+
391
+ /**
392
+ * Generate conclusion from experiment
393
+ * @param {Object} report - Experiment report
394
+ * @returns {string} Conclusion text
395
+ */
396
+ generateExperimentConclusion(report) {
397
+ const summary = report.summary || {};
398
+ const passRate = parseFloat(summary.passRate) || 0;
399
+
400
+ if (passRate >= 95) {
401
+ return 'ๅฎŸ้จ“ใฏ้žๅธธใซๆˆๅŠŸ่ฃใซๅฎŒไบ†ใ—ใพใ—ใŸใ€‚ใ™ในใฆใฎๅ“่ณชๅŸบๆบ–ใ‚’ๆบ€ใŸใ—ใฆใ„ใพใ™ใ€‚';
402
+ } else if (passRate >= 80) {
403
+ return 'ๅฎŸ้จ“ใฏๆฆ‚ใญๆˆๅŠŸใ—ใพใ—ใŸใŒใ€ใ„ใใคใ‹ใฎๆ”นๅ–„็‚นใŒ่ฆ‹ใคใ‹ใ‚Šใพใ—ใŸใ€‚';
404
+ } else if (passRate >= 50) {
405
+ return 'ๅฎŸ้จ“็ตๆžœใฏๆททๅˆ็š„ใงใ—ใŸใ€‚้‡ๅคงใชๅ•้กŒใŒใ„ใใคใ‹ๆคœๅ‡บใ•ใ‚Œใพใ—ใŸใ€‚';
406
+ } else {
407
+ return 'ๅฎŸ้จ“ใงใฏๅคšใใฎๅ•้กŒใŒๆคœๅ‡บใ•ใ‚Œใพใ—ใŸใ€‚ๆ นๆœฌ็š„ใช่ฆ‹็›ดใ—ใŒๅฟ…่ฆใงใ™ใ€‚';
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Save article to file
413
+ * @param {string} content - Article content
414
+ * @param {Object} metadata - Article metadata
415
+ * @param {string} platform - Target platform
416
+ * @returns {Promise<string>} File path
417
+ */
418
+ async saveArticle(content, metadata, platform) {
419
+ await this.ensureOutputDir();
420
+
421
+ const slug = this.slugify(metadata.title);
422
+ const timestamp = new Date().toISOString().split('T')[0];
423
+ const fileName = `${timestamp}-${slug}-${platform}.md`;
424
+ const filePath = path.join(this.config.outputDir, fileName);
425
+
426
+ await fs.writeFile(filePath, content, 'utf-8');
427
+ return filePath;
428
+ }
429
+
430
+ /**
431
+ * Ensure output directory exists
432
+ * @returns {Promise<void>}
433
+ */
434
+ async ensureOutputDir() {
435
+ await fs.mkdir(this.config.outputDir, { recursive: true });
436
+ }
437
+
438
+ /**
439
+ * Convert string to slug
440
+ * @param {string} text - Input text
441
+ * @returns {string} Slug
442
+ */
443
+ slugify(text) {
444
+ return text
445
+ .toLowerCase()
446
+ .replace(/[^\w\s-]/g, '')
447
+ .replace(/[\s_-]+/g, '-')
448
+ .replace(/^-+|-+$/g, '')
449
+ .substring(0, 50);
450
+ }
451
+
452
+ /**
453
+ * Count words in text
454
+ * @param {string} text - Input text
455
+ * @returns {number} Word count
456
+ */
457
+ countWords(text) {
458
+ // Count Japanese characters + English words
459
+ const japanese = (text.match(/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]/g) || []).length;
460
+ const english = (text.match(/\b\w+\b/g) || []).length;
461
+ return japanese + english;
462
+ }
463
+
464
+ /**
465
+ * Estimate reading time
466
+ * @param {string} text - Article text
467
+ * @returns {string} Reading time estimate
468
+ */
469
+ estimateReadingTime(text) {
470
+ const words = this.countWords(text);
471
+ // Average reading speed: 400 characters/words per minute for Japanese
472
+ const minutes = Math.ceil(words / 400);
473
+ return `็ด„${minutes}ๅˆ†`;
474
+ }
475
+
476
+ /**
477
+ * Get platform template
478
+ * @param {string} platform - Platform name
479
+ * @returns {Object} Template
480
+ */
481
+ getTemplate(platform) {
482
+ return this.templates[platform] || this.templates[PLATFORM.GENERIC];
483
+ }
484
+
485
+ /**
486
+ * Register custom template
487
+ * @param {string} platform - Platform name
488
+ * @param {Object} template - Template object
489
+ */
490
+ registerTemplate(platform, template) {
491
+ this.templates[platform] = template;
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Create a new TechArticleGenerator instance
497
+ * @param {Object} config - Configuration options
498
+ * @returns {TechArticleGenerator}
499
+ */
500
+ function createTechArticleGenerator(config = {}) {
501
+ return new TechArticleGenerator(config);
502
+ }
503
+
504
+ module.exports = {
505
+ TechArticleGenerator,
506
+ createTechArticleGenerator,
507
+ PLATFORM,
508
+ ARTICLE_TYPE
509
+ };