| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- const fs = require('fs');
- const path = require('path');
- const HYPOTHESIS_LABELS = {
- H1: '品类机会',
- H2: '用户需求',
- H3: '产品缺口',
- H4: '价格认知',
- H5: '渠道内容',
- H6: '竞品策略',
- H7: '定位表达',
- H8: '增长行动'
- };
- const PLATFORM_NAMES = {
- xiaohongshu: '小红书',
- xhs: '小红书',
- douyin: '抖音',
- amazon: 'Amazon',
- tiktok: 'TikTok',
- instagram: 'Instagram'
- };
- const PLATFORM_PURPOSES = {
- xiaohongshu: ['获客', '建信任', '转化', '复购'],
- xhs: ['获客', '建信任', '转化', '复购'],
- douyin: ['曝光', '互动', '转化', '复购'],
- amazon: ['转化', '留评', 'Listing 优化', '复购'],
- tiktok: ['曝光', '种草', '互动', '转化'],
- instagram: ['品牌感知', '种草', '互动', '转化']
- };
- const THEME_EXPLANATIONS = {
- 痛点: '用户正在规避闲置、无效、焦虑或使用失败风险,应优先转成产品改进和 FAQ。',
- 场景: '需求由具体生活时刻触发,适合拆成场景化内容、组合装或使用说明。',
- 决策: '用户需要明确选择标准,应把专业参数翻译成能下单的判断表。',
- 反馈: '使用反馈能暴露观察周期、体感边界和售后解释需求。',
- 信任: '信任门槛来自适用边界、合规表达和不夸大的风险提示。',
- 价格: '价格接受度与规格、试用成本、复购压力和效果确定性绑定。',
- 竞品: '竞品/替代方案能暴露现有方案的赢点、输点和差异化切口。',
- 话术: '用户原话可转成标题、详情页模块、客服脚本和内容 AB 测试。'
- };
- function parseArgs(argv) {
- const args = {};
- for (let i = 0; i < argv.length; i++) {
- const token = argv[i];
- if (!token.startsWith('--')) continue;
- const eq = token.indexOf('=');
- if (eq >= 0) {
- args[token.slice(2, eq)] = token.slice(eq + 1);
- } else {
- const key = token.slice(2);
- const next = argv[i + 1];
- if (next && !next.startsWith('--')) {
- args[key] = next;
- i++;
- } else {
- args[key] = true;
- }
- }
- }
- return args;
- }
- function usage() {
- return [
- 'Usage:',
- ' node scripts/tools/platform-mini-report-generator.js --input <normalized-dir|_merged.json|comments-flat.jsonl> --output <out-dir> [--project <name>] [--category <name>] [--platform xiaohongshu|douyin|amazon|tiktok|instagram] [--data-nature <text>] [--owner <name>] [--date YYYY-MM-DD]',
- '',
- 'Outputs:',
- ' platform-mini-report.md',
- ' platform-mini-report.json'
- ].join('\n');
- }
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- }
- function readJson(filePath) {
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
- }
- function readJsonl(filePath) {
- return fs.readFileSync(filePath, 'utf8')
- .split(/\r?\n/)
- .map(line => line.trim())
- .filter(Boolean)
- .map(line => JSON.parse(line));
- }
- function asArray(value) {
- if (!value) return [];
- if (Array.isArray(value)) return value;
- return [value];
- }
- function uniq(values) {
- return [...new Set(values.filter(value => value !== undefined && value !== null && value !== ''))];
- }
- function cleanText(value) {
- return String(value || '').replace(/\s+/g, ' ').trim();
- }
- function truncate(value, length = 72) {
- const text = cleanText(value);
- return text.length > length ? `${text.slice(0, length - 1)}…` : text;
- }
- function resolveInput(inputPath) {
- const absolute = path.resolve(inputPath);
- const stat = fs.statSync(absolute);
- if (stat.isDirectory()) {
- const merged = path.join(absolute, '_merged.json');
- const flat = path.join(absolute, 'comments-flat.jsonl');
- if (fs.existsSync(merged)) return { type: 'merged', filePath: merged, data: readJson(merged) };
- if (fs.existsSync(flat)) return { type: 'jsonl', filePath: flat, data: readJsonl(flat) };
- throw new Error(`no _merged.json or comments-flat.jsonl found in ${absolute}`);
- }
- if (absolute.endsWith('.jsonl')) return { type: 'jsonl', filePath: absolute, data: readJsonl(absolute) };
- return { type: 'merged', filePath: absolute, data: readJson(absolute) };
- }
- function normalizeInput(resolved) {
- if (resolved.type === 'jsonl') {
- return { metadata: {}, items: [], comments: resolved.data, sources: [{ file: path.basename(resolved.filePath), platform: 'jsonl', commentCount: resolved.data.length }] };
- }
- const data = resolved.data;
- if (Array.isArray(data)) return { metadata: {}, items: [], comments: data, sources: [] };
- return { metadata: data.metadata || {}, items: asArray(data.items), comments: asArray(data.comments), sources: asArray(data.sources) };
- }
- function countBy(records, getter) {
- const counts = new Map();
- records.forEach(record => {
- const key = getter(record) || '未标注';
- counts.set(key, (counts.get(key) || 0) + 1);
- });
- return [...counts.entries()]
- .map(([label, count]) => ({ label, count }))
- .sort((a, b) => b.count - a.count || String(a.label).localeCompare(String(b.label)));
- }
- function scoreEvidence(record) {
- return Number(record.likeCount || 0) + Number(record.replyCount || 0) * 2 + cleanText(record.text || record.content).length / 80;
- }
- function platformKey(value) {
- const raw = String(value || '').toLowerCase();
- if (raw.includes('xiaohongshu') || raw === 'xhs' || raw.includes('小红书')) return 'xiaohongshu';
- if (raw.includes('douyin') || raw.includes('抖音')) return 'douyin';
- if (raw.includes('amazon')) return 'amazon';
- if (raw.includes('tiktok')) return 'tiktok';
- if (raw.includes('instagram')) return 'instagram';
- return raw || 'unknown';
- }
- function displayPlatform(value) {
- const key = platformKey(value);
- return PLATFORM_NAMES[key] || value || '未标注平台';
- }
- function evidenceCard(record) {
- return {
- id: record.id || record.commentId,
- source: `${displayPlatform(record.platform)} / ${record.keyword || '未标注关键词'} / ${record.batch || 'P0'} / ${record.commentId || record.id || 'no-id'}`,
- platform: record.platform,
- keyword: record.keyword,
- batch: record.batch || 'P0',
- text: cleanText(record.text || record.content),
- hypothesisTags: asArray(record.hypothesisTags),
- theme: record.theme || '未标注',
- likeCount: Number(record.likeCount || 0),
- replyCount: Number(record.replyCount || 0),
- url: record.url,
- parentId: record.parentId,
- commentId: record.commentId || record.id
- };
- }
- function selectEvidence(records, limit = 6) {
- const seen = new Set();
- return [...records]
- .filter(record => cleanText(record.text || record.content))
- .sort((a, b) => scoreEvidence(b) - scoreEvidence(a))
- .filter(record => {
- const key = cleanText(record.text || record.content).slice(0, 80);
- if (seen.has(key)) return false;
- seen.add(key);
- return true;
- })
- .slice(0, limit)
- .map(evidenceCard);
- }
- function highInteractionCount(items) {
- return items.filter(item => Number(item.likeCount || 0) > 500 || Number(item.commentCount || 0) > 100 || Number(item.raw?.metrics?.collectCount || 0) > 300).length;
- }
- function buildStats(normalized, comments) {
- const items = normalized.items;
- const keywords = uniq([...items.map(item => item.keyword), ...comments.map(comment => comment.keyword)]);
- const platforms = uniq([...items.map(item => item.platform), ...comments.map(comment => comment.platform)]);
- return {
- platformCount: platforms.length,
- keywordCount: keywords.length,
- itemCount: items.length,
- commentCount: comments.length,
- highInteractionItemCount: highInteractionCount(items),
- keywords,
- platforms,
- hypothesisTags: uniq(comments.flatMap(comment => asArray(comment.hypothesisTags))),
- batches: uniq(comments.map(comment => comment.batch))
- };
- }
- function buildHypothesisCoverage(comments) {
- return Object.keys(HYPOTHESIS_LABELS).map(tag => {
- const records = comments.filter(comment => asArray(comment.hypothesisTags).includes(tag));
- return {
- tag,
- label: HYPOTHESIS_LABELS[tag],
- count: records.length,
- status: records.length >= 3 ? '已覆盖' : records.length > 0 ? '弱覆盖' : '未覆盖',
- usage: inferHypothesisUsage(tag)
- };
- });
- }
- function inferHypothesisUsage(tag) {
- const usage = {
- H1: '用于判断品类机会、进入动机和核心阻力',
- H2: '用于提炼用户痛点、场景和真实需求',
- H3: '用于定位产品缺口、体验问题和详情页改进',
- H4: '用于判断价格、规格、周期和价值感',
- H5: '用于识别平台内容形式、种草路径和渠道触点',
- H6: '用于分析竞品替代、对比和差异化空间',
- H7: '用于抽取定位表达、标题和话术偏好',
- H8: '用于输出下一轮行动、验证指标和优先级'
- };
- return usage[tag] || '用于支撑报告判断';
- }
- function buildThemeClusters(comments) {
- const total = Math.max(comments.length, 1);
- return countBy(comments, comment => comment.theme).slice(0, 8).map(item => {
- const records = comments.filter(comment => (comment.theme || '未标注') === item.label);
- const evidence = selectEvidence(records, 1)[0];
- return {
- theme: item.label,
- count: item.count,
- ratio: `${Math.round(item.count / total * 100)}%`,
- quote: evidence ? evidence.text : '',
- source: evidence ? evidence.source : '',
- explanation: THEME_EXPLANATIONS[item.label] || '该主题需要人工结合原声继续解释。'
- };
- });
- }
- function evidenceStrength(comments, coverage) {
- const covered = coverage.filter(item => item.status === '已覆盖').length;
- if (comments.length >= 100 && covered >= 8) return '高';
- if (comments.length >= 30 && covered >= 4) return '中';
- return '低';
- }
- function buildConclusion(context, themes, comments, coverage) {
- if (!comments.length) return '当前没有可引用 VOC,不能生成核心结论。';
- const topThemes = themes.slice(0, 3).map(item => `${item.theme}(${item.count})`).join('、');
- const covered = coverage.filter(item => item.status !== '未覆盖').map(item => item.tag).join(', ');
- return `${context.platformName} ${comments.length} 条可追溯 VOC 显示,${context.category || '该品类'} 当前最稳定的用户信号集中在 ${topThemes || '未标注主题'};这些证据覆盖 ${covered || '暂无 H1-H8'},适合先形成单平台阶段性判断,再进入下一轮采集或报告定稿。`;
- }
- function buildOpportunities(themes, evidenceCards) {
- const selected = themes.slice(0, 5);
- return selected.map((theme, index) => {
- const evidence = evidenceCards.find(card => card.theme === theme.theme) || {};
- return {
- opportunity: `${theme.theme}机会`,
- evidence: evidence.text ? truncate(evidence.text, 42) : truncate(theme.quote, 42),
- action: opportunityAction(theme.theme),
- priority: index < 3 ? 'P0' : 'P1'
- };
- });
- }
- function opportunityAction(theme) {
- const actions = {
- 痛点: '把高频痛点转成详情页 FAQ、客服脚本和反向避坑内容。',
- 场景: '围绕高触发场景设计内容专题、组合装或使用指南。',
- 决策: '输出选择标准表,把专业参数翻译成用户能判断的语言。',
- 反馈: '补充观察周期、适应期和售后解释,降低误解与退货。',
- 信任: '强化适用/不适用人群、风险边界和合规表达。',
- 价格: '设计试用装、月装和每天成本解释,降低首次购买压力。',
- 竞品: '提炼竞品漏洞,形成差异化卖点或替代理由。',
- 话术: '把用户原话改写成标题、开头和内容 AB 测试。'
- };
- return actions[theme] || '把该主题对应原声转成一个可验证动作。';
- }
- function buildContentRecommendations(context, themes, evidenceCards) {
- const purposes = PLATFORM_PURPOSES[context.platformKey] || ['获客', '建信任', '转化', '复购'];
- return themes.slice(0, 5).map((theme, index) => {
- const evidence = evidenceCards.find(card => card.theme === theme.theme) || {};
- return {
- angle: theme.theme,
- title: titleForTheme(context.category, theme.theme, context.platformName),
- source: evidence.source || theme.source || '待关联证据',
- purpose: purposes[index % purposes.length]
- };
- });
- }
- function titleForTheme(category, theme, platformName) {
- const subject = category || '这个产品';
- const titles = {
- 痛点: `《${subject}到底解决什么问题?先看这些真实顾虑》`,
- 场景: `《什么时候才需要${subject}?把场景讲清楚》`,
- 决策: `《选${subject}别只看参数:先看这几条标准》`,
- 反馈: `《用${subject}多久观察一次?真实反馈怎么判断》`,
- 信任: `《${subject}适合谁、不适合谁,一次说清楚》`,
- 价格: `《先买试用装还是月装?${subject}新手这样选》`,
- 竞品: `《${subject}和常见替代方案怎么选?真实对比清单》`,
- 话术: `《把${subject}参数翻译成生活语言:${platformName}用户更爱看这个》`
- };
- return titles[theme] || `《${subject}用户最关心的 ${theme} 问题》`;
- }
- function buildRisks(context, comments) {
- const synthetic = String(context.dataNature || '').includes('样例') || String(context.dataNature || '').includes('合成');
- return {
- dataBoundary: synthetic ? `本报告使用${context.dataNature},不能代表真实市场规模、真实互动分布或真实品牌口碑。` : `本报告仅代表当前输入数据和采集窗口,不能直接外推为全平台或全市场结论。`,
- noiseSource: `${context.platformName} 内容可能包含商业合作、测评号、重复评论、算法推荐和主观体验偏差。`,
- cannotConclude: '不能把用户原声直接写成未经验证的功效、医学、安全或全市场结论。',
- nextValidation: comments.length < 100 ? '建议补足更多关键词和评论样本,再进行跨平台交叉验证。' : '建议用竞品词、差评词和成交评价做交叉验证。'
- };
- }
- function buildActions(context, opportunities) {
- return opportunities.slice(0, 4).map((item, index) => ({
- action: item.action,
- owner: ['产品经理', '内容运营', '电商/客服', '项目负责人'][index] || '项目负责人',
- deadline: ['1 周', '1 周', '2 周', '2 周'][index] || '2 周',
- metric: actionMetric(item.opportunity)
- }));
- }
- function actionMetric(opportunity) {
- if (opportunity.includes('价格')) return '试用装转月装率、咨询转化率';
- if (opportunity.includes('信任')) return 'FAQ 点击率、客服重复问题下降';
- if (opportunity.includes('话术')) return '收藏率、评论追问率、标题点击率';
- if (opportunity.includes('场景')) return '场景内容互动率、搜索词覆盖数';
- return '有效评论数、转化线索数、负反馈下降';
- }
- function buildReport(normalized, args) {
- const comments = normalized.comments.filter(comment => cleanText(comment.text || comment.content));
- const stats = buildStats(normalized, comments);
- const inferredPlatform = args.platform || stats.platforms[0] || normalized.metadata.platform || 'unknown';
- const context = {
- project: args.project || normalized.metadata.project || 'platform-mini-report',
- category: args.category || normalized.metadata.category || '',
- platformKey: platformKey(inferredPlatform),
- platformName: displayPlatform(inferredPlatform),
- dataNature: args['data-nature'] || args.dataNature || '未标注',
- owner: args.owner || 'VOC Skills 课程 demo',
- date: args.date || new Date().toISOString().slice(0, 10),
- generatedAt: new Date().toISOString()
- };
- const coverage = buildHypothesisCoverage(comments);
- const themes = buildThemeClusters(comments);
- const evidenceCards = selectEvidence(comments, Number(args['evidence-limit'] || 6));
- const strength = evidenceStrength(comments, coverage);
- const conclusion = buildConclusion(context, themes, comments, coverage);
- const opportunities = buildOpportunities(themes, evidenceCards);
- const contentRecommendations = buildContentRecommendations(context, themes, evidenceCards);
- const risks = buildRisks(context, comments);
- const actions = buildActions(context, opportunities);
- const audit = {
- generatedAt: context.generatedAt,
- commentCount: comments.length,
- evidenceCardCount: evidenceCards.length,
- warningCount: 0,
- uncoveredHypothesisCount: coverage.filter(item => item.status === '未覆盖').length,
- emptyConclusion: !comments.length,
- hasTraceableEvidence: evidenceCards.every(card => card.source && card.text)
- };
- if (!comments.length) audit.warningCount++;
- if (!audit.hasTraceableEvidence) audit.warningCount++;
- return { metadata: context, stats, conclusion, evidenceStrength: strength, hypothesisCoverage: coverage, themes, evidenceCards, opportunities, contentRecommendations, risks, actions, audit };
- }
- function renderMarkdown(report) {
- const meta = report.metadata;
- const lines = [];
- lines.push(`# Platform VOC Mini Report:${meta.platformName}${meta.category ? meta.category : ''}`);
- lines.push('');
- lines.push(`> 本报告由 \`platform-mini-report-generator\` 基于 normalized VOC 数据自动生成,所有关键结论必须回溯到 VOC 证据卡。`);
- lines.push('');
- lines.push('## 0. 报告信息');
- lines.push('');
- lines.push('| 字段 | 填写 |');
- lines.push('|---|---|');
- lines.push(`| 项目/品类 | ${meta.project}${meta.category ? ` / ${meta.category}` : ''} |`);
- lines.push(`| 分析平台 | ${meta.platformName} |`);
- lines.push(`| 分析关键词 | ${report.stats.keywords.join('、') || '未标注'} |`);
- lines.push(`| 样本范围 | ${report.stats.itemCount} 条内容 / ${report.stats.commentCount} 条 VOC |`);
- lines.push(`| 数据性质 | ${meta.dataNature} |`);
- lines.push(`| 完成人 | ${meta.owner} |`);
- lines.push(`| 日期 | ${meta.date} |`);
- lines.push('');
- lines.push('## 1. 一句话结论');
- lines.push('');
- lines.push(`- **核心结论**:${report.conclusion}`);
- lines.push(`- **证据强度**:${report.evidenceStrength}`);
- lines.push(`- **最关键证据**:${report.evidenceCards[0] ? `“${report.evidenceCards[0].text}”(${report.evidenceCards[0].source})` : '暂无可引用证据'}`);
- lines.push('');
- lines.push('## 2. 数据概况');
- lines.push('');
- lines.push('| 指标 | 数值 | 说明 |');
- lines.push('|---|---:|---|');
- lines.push(`| 关键词数 | ${report.stats.keywordCount} | ${report.stats.keywords.join('、') || '未标注'} |`);
- lines.push(`| 内容数 | ${report.stats.itemCount} | normalized items |`);
- lines.push(`| VOC 数 | ${report.stats.commentCount} | normalized comments |`);
- lines.push(`| 高互动内容数 | ${report.stats.highInteractionItemCount} | 点赞 > 500 / 收藏 > 300 / 评论 > 100 |`);
- lines.push(`| 覆盖假设数 | ${report.hypothesisCoverage.filter(item => item.status !== '未覆盖').length} | H1-H8 覆盖 |`);
- lines.push(`| 主要场景/主题 | ${report.themes.slice(0, 3).map(item => item.theme).join('、') || '未标注'} | 按 theme 聚合 |`);
- lines.push('');
- lines.push('### 2.1 H1-H8 假设覆盖速查');
- lines.push('');
- lines.push('| 假设 | 覆盖判断 | VOC 数 | 本报告中的使用方式 |');
- lines.push('|---|---|---:|---|');
- report.hypothesisCoverage.forEach(item => lines.push(`| ${item.tag} ${item.label} | ${item.status} | ${item.count} | ${item.usage} |`));
- lines.push('');
- lines.push('## 3. 高频主题聚类');
- lines.push('');
- lines.push('| 主题 | 占比/频次 | 典型原声 | 业务解释 |');
- lines.push('|---|---:|---|---|');
- report.themes.forEach(item => lines.push(`| ${item.theme} | ${item.ratio} / ${item.count} | “${truncate(item.quote, 52)}” | ${item.explanation} |`));
- lines.push('');
- lines.push('## 4. VOC 原声证据卡');
- lines.push('');
- report.evidenceCards.forEach((item, index) => {
- lines.push(`### 证据 ${index + 1}:${truncate(item.text, 32)}`);
- lines.push('');
- lines.push(`- **来源**:${item.source}`);
- lines.push(`- **原声**:“${item.text}”`);
- lines.push(`- **标签**:${item.hypothesisTags.join(', ') || '未标注'}`);
- lines.push(`- **解释**:这条原声可支撑「${item.theme}」相关判断。`);
- lines.push('');
- });
- lines.push('## 5. 产品/内容机会');
- lines.push('');
- lines.push('| 机会点 | 对应 VOC | 建议动作 | 优先级 |');
- lines.push('|---|---|---|---|');
- report.opportunities.forEach(item => lines.push(`| ${item.opportunity} | “${item.evidence}” | ${item.action} | ${item.priority} |`));
- lines.push('');
- lines.push(`## 6. ${meta.platformName}内容建议`);
- lines.push('');
- lines.push('| 内容角度 | 推荐标题 | 证据来源 | 目的 |');
- lines.push('|---|---|---|---|');
- report.contentRecommendations.forEach(item => lines.push(`| ${item.angle} | ${item.title} | ${item.source} | ${item.purpose} |`));
- lines.push('');
- lines.push('## 7. 风险与边界');
- lines.push('');
- lines.push(`- **数据边界**:${report.risks.dataBoundary}`);
- lines.push(`- **噪声来源**:${report.risks.noiseSource}`);
- lines.push(`- **不能推出的结论**:${report.risks.cannotConclude}`);
- lines.push(`- **下一步需要补采/验证**:${report.risks.nextValidation}`);
- lines.push('');
- lines.push('## 8. 结课行动清单');
- lines.push('');
- lines.push('| 行动 | 负责人 | 截止时间 | 验证指标 |');
- lines.push('|---|---|---|---|');
- report.actions.forEach(item => lines.push(`| ${item.action} | ${item.owner} | ${item.deadline} | ${item.metric} |`));
- lines.push('');
- lines.push('## 9. 生成审计');
- lines.push('');
- lines.push(`- **证据卡数**:${report.audit.evidenceCardCount}`);
- lines.push(`- **警告数**:${report.audit.warningCount}`);
- lines.push(`- **未覆盖假设数**:${report.audit.uncoveredHypothesisCount}`);
- lines.push(`- **证据可追溯**:${report.audit.hasTraceableEvidence ? '是' : '否'}`);
- return `${lines.join('\n')}\n`;
- }
- function main() {
- const args = parseArgs(process.argv.slice(2));
- if (args.help || !args.input || !args.output) {
- console.log(usage());
- process.exit(args.help ? 0 : 1);
- }
- const resolved = resolveInput(args.input);
- const normalized = normalizeInput(resolved);
- const report = buildReport(normalized, args);
- const outputDir = path.resolve(args.output);
- ensureDir(outputDir);
- fs.writeFileSync(path.join(outputDir, 'platform-mini-report.json'), JSON.stringify(report, null, 2) + '\n', 'utf8');
- fs.writeFileSync(path.join(outputDir, 'platform-mini-report.md'), renderMarkdown(report), 'utf8');
- console.log(JSON.stringify({
- outputDir,
- files: ['platform-mini-report.md', 'platform-mini-report.json'],
- platform: report.metadata.platformName,
- commentCount: report.stats.commentCount,
- evidenceCards: report.audit.evidenceCardCount,
- warnings: report.audit.warningCount,
- uncoveredHypothesisCount: report.audit.uncoveredHypothesisCount
- }, null, 2));
- }
- if (require.main === module) {
- try {
- main();
- } catch (error) {
- console.error(error.message);
- process.exit(1);
- }
- }
- module.exports = {
- parseArgs,
- buildReport,
- renderMarkdown,
- selectEvidence,
- buildThemeClusters,
- buildHypothesisCoverage
- };
|