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.
- 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/orchestration/builtin-skills.js +425 -0
- package/src/templates/agents/claude-code/skills/design-reviewer/SKILL.md +1135 -0
- package/src/templates/agents/claude-code/skills/requirements-reviewer/SKILL.md +997 -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
- package/src/validators/design-reviewer.js +1300 -0
- package/src/validators/requirements-reviewer.js +1019 -0
|
@@ -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
|
+
};
|