#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { runTihaoSourcing } = require('../mcp/src/tools/tihao-brief-sourcing-run'); const { readFeedbackCsv } = require('./import-feedback-memory'); const { readMemory, applyFeedbackRecordsToMemory } = require('../mcp/src/features/tihao-sourcing/preference-memory'); const { buildFeedbackLoopClosureSummary } = require('./feedback-loop-closure-audit'); async function main() { const out = fs.mkdtempSync(path.join(os.tmpdir(), 'tihao-feedback-loop-closure-')); const memoryPath = path.join(out, 'memory.json'); const feedbackPath = path.join(out, 'feedback.csv'); const resultPath = path.join(out, 'second-round', 'tihao-sourcing-result.json'); const blockedNames = [ '618母婴好物清单', '精致生活小鹿', '办公室健康零食测评' ]; fs.writeFileSync(feedbackPath, [ 'brief编号,平台,博主名称,主页链接,人工复核标签,反馈原因,归因类型,影响范围,是否升级为团队规则,下一轮动作', 'flower-skincare,小红书,618母婴好物清单,https://www.xiaohongshu.com/user/profile/xhs-dha-003,跑偏,护肤 Brief 不应优先推荐母婴清单号,需求解析,客户品牌级,否,剔除账号', 'flower-skincare,小红书,精致生活小鹿,https://www.xiaohongshu.com/user/profile/xhs-beauty-002,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号', 'flower-skincare,小红书,办公室健康零食测评,https://www.xiaohongshu.com/user/profile/xhs-food-001,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号', 'flower-skincare,小红书,下沉硬广一号,https://www.xiaohongshu.com/user/profile/xhs-low-001,主页质感不符,封面下沉且主页质感不符,主页证据,团队规则,是,剔除账号' ].join('\n'), 'utf8'); const memory = applyFeedbackRecordsToMemory(readMemory(memoryPath), readFeedbackCsv(feedbackPath)); fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2), 'utf8'); const result = await runTihaoSourcing({ collectionMode: 'sample', briefText: [ '客户名称:花漾肌研', '平台与人数:小红书 6 人,抖音 2 人', '粉丝量范围:2万-50万', '预算范围:5000-18000', '偏好真实体验、素人感、成分拆解、敏感肌修护,不要硬广。' ].join('\n'), memory: memoryPath, output: path.join(out, 'second-round') }); assert(result.status === 'ok', 'second round sourcing should return ok'); assert(fs.existsSync(resultPath), 'second round result json should exist'); const summary = buildFeedbackLoopClosureSummary({ feedbackPath, memoryPath, resultPath, mode: 'sample', collectionMode: 'sample', generatedBy: 'feedback-loop-closure:smoke' }); assert(summary.passed === true, 'feedback loop closure should pass for sample fixture'); assert(summary.complete === false, 'sample fixture must not close direct customer proof'); assert(summary.directCustomerProof === false, 'sample fixture must not be direct customer proof'); assert(summary.proofLevel === 'smoke_or_local', 'sample fixture should be smoke/local proof'); assert(summary.negativeFeedbackCount === 4, 'summary should count negative feedback rows'); assert(summary.blockedCreatorCount === 4, 'summary should count blocked creators'); assert(summary.blockedInMemoryCount === 4, 'summary should count blocked creators written to memory'); assert(summary.leakedCount === 0, 'blocked creators should not leak into second-round client-ready list'); assert(summary.teamRuleSuggestionCount >= 1, 'repeated team feedback should create team rule suggestion'); for (const name of blockedNames) { assert(summary.rows.some(row => row.creatorName === name && row.inMemory && !row.leakedToSecondRound), `${name} should be blocked and absent from second round`); } const report = require('./feedback-loop-closure-audit').buildFeedbackLoopClosureSummary({ feedbackPath, memoryPath, resultPath, mode: 'sample', collectionMode: 'sample', generatedBy: 'feedback-loop-closure:smoke' }); assert(JSON.stringify(report).includes('directCustomerProof'), 'summary should expose proof boundary'); assert(!/sk-[A-Za-z0-9_-]{10,}/.test(JSON.stringify(summary)), 'summary should not contain model token patterns'); assert(!/r:[A-Za-z0-9]{20,}/.test(JSON.stringify(summary)), 'summary should not contain Parse sessionToken patterns'); console.log(JSON.stringify({ ok: true, outputDir: out, negativeFeedbackCount: summary.negativeFeedbackCount, leakedCount: summary.leakedCount, directCustomerProof: summary.directCustomerProof }, null, 2)); } function assert(condition, message) { if (!condition) throw new Error(message); } main().catch(error => { console.error(error && error.stack ? error.stack : String(error)); process.exit(1); });