| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const { spawnSync } = require('child_process');
- const ROOT = path.resolve(__dirname, '..');
- function main() {
- const args = parseArgs(process.argv.slice(2));
- const outputRoot = path.resolve(args.output || process.env.TIHAO_PIPELINE_OUTPUT || path.join(ROOT, 'outputs', `optimization-pipeline-${Date.now()}`));
- const dryRun = Boolean(args.dryRun);
- const strict = Boolean(args.strict);
- fs.mkdirSync(outputRoot, { recursive: true });
- const context = buildContext(args, outputRoot);
- const steps = buildSteps(context);
- const results = [];
- for (const step of steps) {
- if (dryRun) {
- results.push({ id: step.id, status: 'planned', command: step.command.join(' '), output: step.output || '' });
- continue;
- }
- const result = runStep(step);
- results.push(result);
- if (strict && result.status !== 'pass') break;
- }
- const closureResult = results.find(item => item.id === 'proof-gap-closure');
- const closureSummary = closureResult?.status === 'pass' ? readJson(closureResult.output) : null;
- const proofGapClosureClaimable = isClaimableProofGapClosure(closureSummary);
- const readyForClaim = results.every(item => item.status === 'pass') &&
- Boolean(context.inputs.reviewCsv) &&
- proofGapClosureClaimable;
- const summary = {
- generatedAt: new Date().toISOString(),
- outputRoot,
- dryRun,
- strict,
- inputs: context.inputs,
- counts: countStatuses(results),
- readyForClaim,
- proofGapClosure: closureSummary ? {
- complete: Boolean(closureSummary.complete),
- openCount: Number(closureSummary.openCount || 0),
- closedCount: Number(closureSummary.closedCount || 0),
- businessGapCount: Number(closureSummary.businessGapCount || 0),
- claimable: proofGapClosureClaimable
- } : null,
- results,
- nextActions: buildNextActions(results, context, { dryRun })
- };
- const jsonPath = path.join(outputRoot, 'optimization-pipeline-summary.json');
- const reportPath = path.join(outputRoot, 'optimization-pipeline-report.md');
- fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
- fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
- console.log(JSON.stringify({
- outputRoot,
- json: jsonPath,
- report: reportPath,
- counts: summary.counts,
- readyForClaim: summary.readyForClaim,
- nextActionCount: summary.nextActions.length
- }, null, 2));
- if (strict && !summary.readyForClaim) process.exitCode = 2;
- }
- function buildContext(args, outputRoot) {
- const dataPack = resolveOptional(args.dataPack || process.env.TIHAO_DATA_PACK);
- const videoPack = resolveOptional(args.videoPack || process.env.TIHAO_VIDEO_PACK);
- const historyCsv = resolveOptional(args.historyCsv || process.env.TIHAO_HISTORY_CSV || (dataPack ? path.join(dataPack, 'history-data-template.csv') : ''));
- const historyDir = resolveOptional(args.historyDir || process.env.TIHAO_HISTORY_DATASET);
- const preflight = resolveOptional(args.preflight || process.env.TIHAO_PREFLIGHT_SUMMARY);
- const reviewCsv = resolveOptional(args.reviewCsv || process.env.TIHAO_REVIEW_CSV || (dataPack ? path.join(dataPack, 'manual-review-template.csv') : ''));
- const videoCsv = resolveOptional(args.videoCsv || process.env.TIHAO_VIDEO_CSV || (videoPack ? path.join(videoPack, 'video-resource-template.csv') : ''));
- const currentManualSupplementCount = args.currentManualSupplementCount || process.env.TIHAO_CURRENT_MANUAL_SUPPLEMENT_COUNT || '';
- const historyMinBriefs = args.historyMinBriefs || process.env.TIHAO_HISTORY_MIN_BRIEFS || '';
- const generatedHistoryDir = path.join(outputRoot, 'history-dataset');
- const intakeOutput = path.join(outputRoot, 'intake-readiness');
- const historyAuditOutput = path.join(outputRoot, 'history-audit');
- const videoReadinessOutput = path.join(outputRoot, 'video-resource-readiness');
- const reviewOutput = path.join(outputRoot, 'review-metrics');
- const customerEffectOutput = path.join(outputRoot, 'customer-effect-audit');
- const closureOutput = path.join(outputRoot, 'proof-gap-closure');
- const readinessOutput = path.join(outputRoot, 'long-run-readiness');
- const evidenceOutput = path.join(outputRoot, 'evidence-index');
- return {
- outputRoot,
- historyDatasetDir: historyCsv ? generatedHistoryDir : historyDir,
- historyAuditJson: path.join(historyAuditOutput, 'historical-dataset-audit.json'),
- intakeReadinessJson: path.join(intakeOutput, 'intake-readiness-summary.json'),
- videoReadinessJson: path.join(videoReadinessOutput, 'video-resource-readiness-summary.json'),
- inputs: {
- dataPack,
- videoPack,
- historyCsv,
- historyDir,
- videoCsv,
- preflight,
- reviewCsv,
- currentManualSupplementCount,
- historyMinBriefs
- },
- paths: {
- generatedHistoryDir,
- intakeOutput,
- historyAuditOutput,
- videoReadinessOutput,
- reviewOutput,
- customerEffectOutput,
- closureOutput,
- readinessOutput,
- evidenceOutput
- }
- };
- }
- function buildSteps(context) {
- const steps = [];
- if (context.inputs.dataPack || context.inputs.videoPack || (context.inputs.historyCsv && context.inputs.reviewCsv && context.inputs.videoCsv)) {
- const intakeArgs = ['npm.cmd', 'run', 'intake:readiness', '--', '--output', context.paths.intakeOutput];
- if (context.inputs.dataPack) intakeArgs.push('--data-pack', context.inputs.dataPack);
- else {
- intakeArgs.push('--history-csv', context.inputs.historyCsv);
- intakeArgs.push('--review-csv', context.inputs.reviewCsv);
- }
- if (context.inputs.videoPack) intakeArgs.push('--video-pack', context.inputs.videoPack);
- else intakeArgs.push('--video-csv', context.inputs.videoCsv);
- if (context.inputs.historyMinBriefs) intakeArgs.push('--min-briefs', context.inputs.historyMinBriefs);
- steps.push(step('intake-readiness', intakeArgs, context.intakeReadinessJson));
- }
- if (context.inputs.historyCsv) {
- steps.push(step('history-from-csv', ['npm.cmd', 'run', 'history:from-csv', '--', '--input', context.inputs.historyCsv, '--output', context.paths.generatedHistoryDir], context.paths.generatedHistoryDir));
- }
- if (context.historyDatasetDir) {
- const historyAuditArgs = ['npm.cmd', 'run', 'history:audit', '--', '--input', context.historyDatasetDir, '--output', context.paths.historyAuditOutput, '--strict'];
- if (context.inputs.historyMinBriefs) historyAuditArgs.push('--min-briefs', context.inputs.historyMinBriefs);
- steps.push(step('history-audit', historyAuditArgs, context.historyAuditJson));
- }
- if (context.inputs.videoCsv) {
- steps.push(step('video-resource-readiness', ['npm.cmd', 'run', 'video:resource-readiness', '--', '--input', context.inputs.videoCsv, '--output', context.paths.videoReadinessOutput, '--strict'], context.videoReadinessJson));
- }
- const readinessArgs = ['npm.cmd', 'run', 'longrun:readiness', '--', '--mode', context.inputs.reviewCsv ? 'customer-effect' : 'full-matrix', '--output', context.paths.readinessOutput];
- if (context.inputs.preflight) readinessArgs.push('--preflight', context.inputs.preflight);
- if (context.historyDatasetDir) readinessArgs.push('--history', context.historyAuditJson);
- if (context.inputs.dataPack || context.inputs.videoPack || context.inputs.videoCsv) readinessArgs.push('--intake-readiness', context.intakeReadinessJson);
- if (context.inputs.videoCsv) readinessArgs.push('--video-readiness', context.videoReadinessJson);
- steps.push(step('longrun-readiness', readinessArgs, path.join(context.paths.readinessOutput, 'long-run-readiness-summary.json')));
- if (context.inputs.reviewCsv) {
- steps.push(step('review-metrics', ['npm.cmd', 'run', 'review:metrics', '--', '--input', context.inputs.reviewCsv, '--output', context.paths.reviewOutput, '--strict'], path.join(context.paths.reviewOutput, 'review-metrics-summary.json')));
- const effectArgs = ['npm.cmd', 'run', 'customer-effect:audit', '--', '--review-csv', context.inputs.reviewCsv, '--history-audit', context.historyAuditJson, '--output', context.paths.customerEffectOutput, '--strict'];
- if (context.inputs.currentManualSupplementCount) effectArgs.push('--current-manual-supplement-count', context.inputs.currentManualSupplementCount);
- steps.push(step('customer-effect-audit', effectArgs, path.join(context.paths.customerEffectOutput, 'customer-effect-summary.json')));
- }
- const closureArgs = ['npm.cmd', 'run', 'proof-gap:closure', '--', '--output', context.paths.closureOutput];
- if (context.historyDatasetDir) closureArgs.push('--history-audit', context.historyAuditJson);
- if (context.inputs.dataPack || context.inputs.videoPack || context.inputs.videoCsv) closureArgs.push('--intake-readiness', context.intakeReadinessJson);
- if (context.inputs.videoCsv) closureArgs.push('--video-readiness', context.videoReadinessJson);
- if (context.inputs.reviewCsv) closureArgs.push('--customer-effect', path.join(context.paths.customerEffectOutput, 'customer-effect-summary.json'));
- steps.push(step('proof-gap-closure', closureArgs, path.join(context.paths.closureOutput, 'proof-gap-closure-summary.json')));
- steps.push(step('evidence-index', ['npm.cmd', 'run', 'evidence:index', '--', '--output', context.paths.evidenceOutput], path.join(context.paths.evidenceOutput, 'evidence-index-summary.json')));
- return steps;
- }
- function step(id, command, output) {
- return { id, command, output };
- }
- function runStep(step) {
- const [command, ...args] = step.command;
- const result = process.platform === 'win32'
- ? spawnSync('cmd.exe', ['/d', '/s', '/c', step.command.map(cmdQuote).join(' ')], { cwd: ROOT, encoding: 'utf8' })
- : spawnSync(command, args, { cwd: ROOT, encoding: 'utf8' });
- return {
- id: step.id,
- command: step.command.join(' '),
- output: step.output,
- status: result.status === 0 ? 'pass' : 'fail',
- exitCode: result.status,
- stdout: safeTail(result.stdout),
- stderr: safeTail(result.stderr)
- };
- }
- function cmdQuote(value) {
- const text = String(value || '');
- if (!/[()\s&|<>^"]/u.test(text)) return text;
- return `"${text.replace(/"/g, '\\"')}"`;
- }
- function renderReport(summary) {
- const lines = [
- '# 提号优化 Pipeline 报告',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- 输出目录:${summary.outputRoot}`,
- `- dryRun:${summary.dryRun ? '是' : '否'}`,
- `- strict:${summary.strict ? '是' : '否'}`,
- `- 是否可宣称业务效果:${summary.readyForClaim ? '是' : '否'}`,
- `- proofGapClosure.complete:${summary.proofGapClosure?.complete ? 'true' : 'false'}`,
- `- proofGapClosure.openCount:${summary.proofGapClosure?.openCount ?? 'unknown'}`,
- '',
- '## 输入',
- '',
- ...Object.entries(summary.inputs).map(([key, value]) => `- ${key}: ${value || '未提供'}`),
- '',
- '## 步骤',
- '',
- '| 步骤 | 状态 | 输出 | 命令 |',
- '| --- | --- | --- | --- |',
- ...summary.results.map(item => `| ${escapeCell(item.id)} | ${item.status} | ${escapeCell(item.output || '')} | ${escapeCell(item.command)} |`),
- '',
- '## 下一步动作',
- '',
- summary.nextActions.length
- ? '| 优先级 | 负责人 | 动作 | 触发步骤 | 执行命令 | 预期产物 | 通过标准 |\n| --- | --- | --- | --- | --- | --- | --- |\n' + summary.nextActions.map(item => `| ${item.priority} | ${escapeCell(item.owner)} | ${escapeCell(item.title)} | ${escapeCell(item.step)} | \`${escapeCell(item.command)}\` | ${escapeCell(item.expectedArtifact)} | ${escapeCell(item.acceptance)} |`).join('\n')
- : '- 暂无后续动作。',
- '',
- '## 边界',
- '',
- '- pipeline 只编排已有脚本,不自动写入 token,也不默认启动真实 live 长跑。',
- '- 没有客户选择、历史人工补号基线或本轮人工补号量时,不能宣称客户效果完成。',
- '- `readyForClaim=false` 时,只能说明链路入口或局部验收完成。'
- ];
- return lines.join('\n');
- }
- function buildNextActions(results, context, { dryRun }) {
- if (dryRun) {
- return [{
- priority: 1,
- owner: '技术/AI',
- title: '确认输入后执行真实 pipeline',
- step: 'dry-run',
- command: rerunCommandWithoutDryRun(context),
- expectedArtifact: path.join(context.outputRoot, 'optimization-pipeline-summary.json'),
- acceptance: 'dryRun=false,且关键步骤不再是 planned。'
- }];
- }
- const actions = [];
- for (const result of results) {
- if (result.status === 'pass') continue;
- actions.push(actionForFailedStep(result.id, context, result));
- if (context.inputs.currentManualSupplementCount && result.id === 'customer-effect-audit') continue;
- }
- if (!context.inputs.currentManualSupplementCount && context.inputs.reviewCsv) {
- actions.push({
- priority: 2,
- owner: '商务',
- title: '补本轮人工补号量',
- step: 'customer-effect-audit',
- command: '补齐 --current-manual-supplement-count <本轮人工补号量> 后重跑 optimization:pipeline',
- expectedArtifact: '<客户效果输出目录>/customer-effect-summary.json',
- acceptance: 'customer-effect:audit 输出 manualSupplementReductionRate,且 strict 审计通过。'
- });
- }
- if (!actions.length && !results.every(item => item.status === 'pass')) {
- actions.push({
- priority: 99,
- owner: '技术/AI',
- title: '检查 pipeline 未通过步骤',
- step: 'pipeline',
- command: '打开 optimization-pipeline-report.md,按首个 fail 步骤处理。',
- expectedArtifact: 'optimization-pipeline-summary.json',
- acceptance: '所有 pipeline 步骤 status=pass。'
- });
- }
- const closureResult = results.find(item => item.id === 'proof-gap-closure');
- const closureSummary = closureResult?.status === 'pass' ? readJson(closureResult.output) : null;
- if (!actions.length && closureSummary && !isClaimableProofGapClosure(closureSummary)) {
- actions.push({
- priority: 2,
- owner: '技术/AI',
- title: '关闭真实证明缺口',
- step: 'proof-gap-closure',
- command: '按 proof-gap-closure-report.md 补齐 open 项后重跑 optimization:pipeline。',
- expectedArtifact: closureResult.output || '<输出目录>/proof-gap-closure/proof-gap-closure-summary.json',
- acceptance: 'proof-gap:closure complete=true、openCount=0、closedCount>=5,且 businessGapCount>=5。'
- });
- }
- if (!actions.length && results.every(item => item.status === 'pass') && !context.inputs.reviewCsv) {
- actions.push({
- priority: 3,
- owner: '商务',
- title: '补人工复核 CSV 后验证客户效果',
- step: 'review-metrics',
- command: 'npm run optimization:pipeline -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict',
- expectedArtifact: '<输出目录>/customer-effect-audit/customer-effect-summary.json',
- acceptance: 'review:metrics 和 customer-effect:audit 均通过。'
- });
- }
- return dedupeActions(actions).sort((a, b) => a.priority - b.priority);
- }
- function actionForFailedStep(stepId, context, result) {
- const base = {
- priority: 10,
- owner: '技术/AI',
- title: `修复 ${stepId}`,
- step: stepId,
- command: result.command,
- expectedArtifact: result.output || '',
- acceptance: `${stepId} status=pass。`
- };
- const map = {
- 'intake-readiness': {
- priority: 1,
- owner: '商务/投放',
- title: '修复真实材料收集包',
- command: '按 intake-readiness-report.md 的问题修正 data-pack/video-pack 后重跑 optimization:pipeline。',
- acceptance: 'intake-readiness overallReady=true 且 failureCount=0。'
- },
- 'history-from-csv': {
- priority: 2,
- owner: '商务',
- title: '修复历史数据 CSV',
- command: '检查 history-data-template.csv 表头、编码和必填字段后重跑 history:from-csv。',
- acceptance: 'history-dataset JSON 成功生成。'
- },
- 'history-audit': {
- priority: 3,
- owner: '商务',
- title: '补齐历史数据审计字段',
- command: '补齐真实 Brief、参考链接、人工名单、客户选择、拒绝原因和历史人工补号量基线后重跑 history:audit --strict。',
- acceptance: 'historical-dataset-audit readyForCustomerEffectProof=true。'
- },
- 'video-resource-readiness': {
- priority: 4,
- owner: '商务/投放',
- title: '补齐真实视频资源',
- command: '补齐真实参考视频、真实候选视频、视频 URL、封面、ASR、帧图或正文证据后重跑 video:resource-readiness --strict。',
- acceptance: 'video-resource-readiness readyForVideoAbPreflight=true。'
- },
- 'longrun-readiness': {
- priority: 5,
- owner: '技术/AI',
- title: '修复长跑前门禁',
- command: '根据 long-run-readiness-report.md 补 live preflight、history audit、intake readiness、video readiness 或 proof gap 后重跑 longrun:readiness。',
- acceptance: 'longrun:readiness ready=true 且 fail=0。'
- },
- 'review-metrics': {
- priority: 6,
- owner: '商务',
- title: '补齐人工复核标注',
- command: '补齐人工复核标签、客户选择和负样本归因后重跑 review:metrics --strict。',
- acceptance: 'review:metrics overallPass=true,负样本归因覆盖率=100%。'
- },
- 'customer-effect-audit': {
- priority: 7,
- owner: '商务',
- title: '补齐客户效果证明',
- command: '补齐客户最终选择、历史人工补号量基线和本轮人工补号量后重跑 customer-effect:audit --strict。',
- acceptance: 'customer-effect:audit overallPass=true,客户选中率和人工补号减少率达标。'
- },
- 'evidence-index': {
- priority: 20,
- owner: '技术/AI',
- title: '刷新证据台账',
- command: 'npm run evidence:index -- --output outputs\\evidence-index-latest',
- acceptance: 'evidence-index-summary.json 成功生成,且不把 smoke 当业务证明。'
- }
- };
- return { ...base, ...(map[stepId] || {}) };
- }
- function rerunCommandWithoutDryRun(context) {
- const parts = ['npm run optimization:pipeline --'];
- if (context.inputs.dataPack) parts.push(`--data-pack ${context.inputs.dataPack}`);
- if (context.inputs.videoPack) parts.push(`--video-pack ${context.inputs.videoPack}`);
- if (!context.inputs.dataPack && context.inputs.historyCsv) parts.push(`--history-csv ${context.inputs.historyCsv}`);
- if (!context.inputs.dataPack && context.inputs.reviewCsv) parts.push(`--review-csv ${context.inputs.reviewCsv}`);
- if (context.inputs.historyMinBriefs) parts.push(`--history-min-briefs ${context.inputs.historyMinBriefs}`);
- if (context.inputs.currentManualSupplementCount) parts.push(`--current-manual-supplement-count ${context.inputs.currentManualSupplementCount}`);
- parts.push(`--output ${context.outputRoot}`);
- if (context.strict) parts.push('--strict');
- return parts.join(' ');
- }
- function dedupeActions(actions) {
- const seen = new Set();
- return actions.filter(action => {
- const key = `${action.owner}|${action.title}|${action.step}`;
- if (seen.has(key)) return false;
- seen.add(key);
- return true;
- });
- }
- function countStatuses(results) {
- return results.reduce((acc, item) => {
- acc[item.status] = (acc[item.status] || 0) + 1;
- return acc;
- }, {});
- }
- function parseArgs(argv) {
- const args = {};
- for (let index = 0; index < argv.length; index += 1) {
- const raw = argv[index];
- if (!raw.startsWith('--')) continue;
- const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
- const next = argv[index + 1];
- if (!next || next.startsWith('--')) args[key] = true;
- else {
- args[key] = next;
- index += 1;
- }
- }
- return args;
- }
- function resolveOptional(value) {
- return value ? path.resolve(value) : '';
- }
- function safeTail(value) {
- return String(value || '').slice(-1200);
- }
- function readJson(file) {
- if (!file || !fs.existsSync(file)) return null;
- try {
- return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
- } catch {
- return null;
- }
- }
- function isClaimableProofGapClosure(summary) {
- return Boolean(
- summary?.complete === true &&
- Number(summary?.openCount || 0) === 0 &&
- Number(summary?.closedCount || 0) >= 5 &&
- Number(summary?.businessGapCount || 0) >= 5
- );
- }
- function escapeCell(value) {
- return String(value || '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- if (require.main === module) {
- try {
- main();
- } catch (error) {
- console.error(error && error.stack ? error.stack : String(error));
- process.exit(1);
- }
- }
- module.exports = {
- buildContext,
- buildSteps,
- isClaimableProofGapClosure
- };
|