#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); function main() { const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-experience-transcript-index-')); const transcriptsDir = path.join(temp, 'transcripts'); const output = path.join(temp, 'index'); fs.mkdirSync(transcriptsDir, { recursive: true }); fs.writeFileSync(path.join(transcriptsDir, '黄圆-AI提号经验逐字稿.md'), [ '# 黄圆', '参考账号要拆类型和调性。', '小红书和蒲公英可以通过相似推荐链路继续找。', '抖音星图标签不准时,用招募和关键词兜底。', '看主页近10篇、20篇封面首图判断风格。', '先用预算、CPE、CP1、CPM、粉丝、女粉和城市做第一轮过滤。', 'Brief 之外还要看产品、参考账号和客户聊天记录,客户说的隐藏要求要并入。' ].join('\n'), 'utf8'); fs.writeFileSync(path.join(transcriptsDir, '邓发-AI提号经验逐字稿.md'), [ '# 邓发', '美容仪和脱毛仪要理解女性受众,不能推荐男性账号。', '内容质感看画面清晰、像素、排版,不要下沉。', '报价 300 元以上要看封面统一、背景干净和画面精致。', '近一个月停更、平均点赞百赞以下、重复评论和同一个人评论都要复核数据真实性。', '时尚穿搭要拆港风、成熟风,美妆护肤要拆妆教、测评和产品合集。', '封面不确定再点进去看视频内容,可以继续解析每一帧。', '最终要看客户选中率、降本增效和补号变化。', ['Authorization', 'Bearer abcdefghijklmnopqrstuvwxyz'].join(': ') ].join('\n'), 'utf8'); const result = spawnSync(process.execPath, [ 'scripts/experience-transcript-index.js', '--transcripts-dir', transcriptsDir, '--output', output, '--strict' ], { cwd: ROOT, encoding: 'utf8', shell: false }); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); assert(result.status === 0, 'experience transcript index should pass'); const summaryPath = path.join(output, 'experience-transcript-index-summary.json'); const reportPath = path.join(output, 'experience-transcript-index.md'); const csvPath = path.join(output, 'experience-transcript-index.csv'); const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); const report = fs.readFileSync(reportPath, 'utf8'); const csv = fs.readFileSync(csvPath, 'utf8'); assert(summary.proofLevel === 'not_business_proof', 'summary should not be business proof'); assert(summary.materialType === 'experience_seed', 'summary should classify as experience seed'); assert(summary.directCustomerProof === false, 'summary should not be direct customer proof'); assert(summary.canCloseProofGap === false, 'summary should not close proof gap'); assert(summary.complete === false, 'summary should not claim completion'); assert(summary.transcriptCount === 2, 'summary should index two transcripts'); assert(summary.coveredRuleCount === summary.ruleCount, 'fixture should cover all rules'); assert(summary.ruleCoverage.every(item => item.directCustomerProof === false), 'rules should keep proof boundary'); assert(summary.ruleCoverage.some(item => item.id === 'reference-type-tone' && item.status === 'covered'), 'reference rule should be covered'); assert(summary.ruleCoverage.some(item => item.id === 'customer-effect-goal' && item.status === 'covered'), 'customer effect boundary rule should be covered'); assert(report.includes('提号经验逐字稿索引'), 'report should render title'); assert(report.includes('canCloseProofGap:false'), 'report should render proof boundary'); assert(csv.includes('experience-rule'), 'csv should include rule rows'); assert(!JSON.stringify(summary).includes('Authorization'), 'summary should not expose secret-like transcript text'); assert(!report.includes('Authorization'), 'report should not expose secret-like transcript text'); assert(!csv.includes('Authorization'), 'csv should not expose secret-like transcript text'); console.log(JSON.stringify({ ok: true, transcriptCount: summary.transcriptCount, coveredRuleCount: summary.coveredRuleCount }, null, 2)); } function assert(condition, message) { if (!condition) throw new Error(message); } main();