| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.resolve(__dirname, '..');
- const OUTPUTS = path.join(ROOT, 'outputs');
- function main() {
- const args = parseArgs(process.argv.slice(2));
- const outputDir = path.resolve(args.output || process.env.TIHAO_BUSINESS_PROOF_PROGRESS_OUTPUT || path.join(OUTPUTS, `business-proof-progress-${Date.now()}`));
- const summary = buildBusinessProofProgress({
- root: ROOT,
- outputsDir: path.resolve(args.outputs || OUTPUTS)
- });
- fs.mkdirSync(outputDir, { recursive: true });
- const jsonPath = path.join(outputDir, 'business-proof-progress-summary.json');
- const reportPath = path.join(outputDir, 'business-proof-progress-report.md');
- fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
- fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
- console.log(JSON.stringify({
- outputDir,
- json: jsonPath,
- report: reportPath,
- complete: summary.complete,
- pass: summary.counts.pass,
- blocked: summary.counts.blocked_by_external_data,
- pending: summary.counts.pending,
- fail: summary.counts.fail
- }, null, 2));
- if (!summary.complete && args.strict) process.exitCode = 1;
- }
- function buildBusinessProofProgress({ root, outputsDir }) {
- const dataPackDir = latestDir(outputsDir, 'data-intake-pack');
- const videoPackDir = latestDir(outputsDir, 'video-intake-pack');
- const intake = readJson(latestFile(outputsDir, 'intake-readiness-summary.json'));
- const videoReadiness = readJson(latestFile(outputsDir, 'video-resource-readiness-summary.json'));
- const longrun = readJson(latestFile(outputsDir, 'long-run-readiness-summary.json'));
- const closure = readJson(latestFile(outputsDir, 'proof-gap-closure-summary.json'));
- const handoff = readJson(latestFile(outputsDir, 'optimization-handoff-summary.json'));
- const evidence = readJson(latestFile(outputsDir, 'evidence-index-summary.json'));
- const round = readJson(latestFile(outputsDir, 'round-deposition-summary.json'));
- const status = readJson(latestFile(outputsDir, 'optimization-status-summary.json'));
- const history = readJson(latestFile(outputsDir, 'historical-dataset-audit.json'));
- const review = readJson(latestFile(outputsDir, 'review-metrics-summary.json'));
- const customerEffect = readJson(latestFile(outputsDir, 'customer-effect-summary.json'));
- const pipeline = readJson(latestFile(outputsDir, 'optimization-pipeline-summary.json'));
- const proofGapOperatorPack = readJson(latestFile(outputsDir, 'proof-gap-operator-pack-summary.json'));
- const businessExecutionIndex = readJson(latestFile(outputsDir, 'business-execution-index-summary.json'));
- const operatorPackOwnerArtifacts = normalizeOperatorPackOwnerArtifacts(
- proofGapOperatorPack?.ownerArtifacts || businessExecutionIndex?.operatorPackOwnerArtifacts || []
- );
- const operatorPackOwnerArtifactCount = Number(proofGapOperatorPack?.ownerArtifactCount || operatorPackOwnerArtifacts.length || 0);
- const closureRow = (id) => Array.isArray(closure?.rows) ? closure.rows.find(row => row.id === id) : null;
- const intakeOverallReady = intake?.acceptance?.overallReady === true && Number(intake?.failureCount || 0) === 0;
- const videoReady = videoReadiness?.acceptance?.readyForVideoAbPreflight === true && Number(videoReadiness?.failureCount || 0) === 0;
- const historyMissingRequirements = getHistoryCustomerEffectProofMissingRequirements(history);
- const historyReady = historyMissingRequirements.length === 0;
- const reviewReady = review?.acceptance?.overallPass === true;
- const customerEffectReady = customerEffect?.acceptance?.overallPass === true;
- const videoAbStatus = statusItem(status, 'video-ab-proof');
- const videoAbPassed = videoAbStatus?.status === 'passed' && closureRow('video-ab-live-proof')?.status === 'closed';
- const videoAbEvidence = videoAbStatus
- ? `status=${videoAbStatus.status}, closure=${closureRow('video-ab-live-proof')?.status || 'missing'}, evidence=${videoAbStatus.evidence || ''}`
- : (closureRow('video-ab-live-proof')?.evidence || 'missing video-ab status row');
- const rows = [
- step(1, '商务', '生成统一收集包', dataPackDir ? 'pass' : 'pending', dataPackDir ? rel(root, dataPackDir) : 'missing data-intake-pack', '没有统一模板时先运行 data:intake-template。'),
- step(2, '商务/投放', '生成视频资源收集包', videoPackDir ? 'pass' : 'pending', videoPackDir ? rel(root, videoPackDir) : 'missing video-intake-pack', '没有视频模板时先运行 video:intake-template。'),
- step(3, '商务', '填真实历史 Brief', intake?.acceptance?.historyReady ? 'pass' : 'blocked_by_external_data', intake ? `historyReady=${Boolean(intake.acceptance?.historyReady)}, issues=${intake.history?.issues?.length || 0}` : 'missing intake-readiness', '补 5-10 个真实 Brief、人工名单、客户选择、拒绝原因和历史人工补号量基线。'),
- step(4, '商务/投放', '填真实视频资源', intake?.acceptance?.videoReady || videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `realCandidate=${videoReadiness.counts?.realCandidateRows || 0}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补真实参考视频和真实候选视频 URL、封面、ASR、帧图或正文证据。'),
- step(5, '技术/AI', '真实材料预审', intakeOverallReady ? 'pass' : 'fail', intake ? `overallReady=${Boolean(intake.acceptance?.overallReady)}, failureCount=${intake.failureCount || 0}` : 'missing intake-readiness', '先修 intake readiness 失败项,不能跳过预审。'),
- step(6, '技术/AI', '历史数据导入', history ? 'pass' : 'pending', history ? 'historical-dataset-audit.json present' : 'missing historical-dataset-audit.json', '真实历史 CSV 到位后运行 history:from-csv。'),
- step(7, '技术/AI', '历史数据审计', historyReady ? 'pass' : 'blocked_by_external_data', history ? `readyForCustomerEffectProof=${Boolean(history.acceptance?.readyForCustomerEffectProof)}, missing=${historyMissingRequirements.length}` : 'missing historical-dataset-audit.json', '运行 history:audit --strict,直到可支撑客户效果证明。'),
- step(8, '技术/AI', '视频资源就绪审计', videoReady ? 'pass' : 'blocked_by_external_data', videoReadiness ? `readyForVideoAbPreflight=${Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight)}, failureCount=${videoReadiness.failureCount || 0}` : 'missing video-resource-readiness', '补齐真实候选视频后重跑 video:resource-readiness --strict。'),
- step(9, '技术/AI', '长跑前门禁', longrun?.ready ? 'pass' : 'fail', longrun ? `ready=${Boolean(longrun.ready)}, fail=${longrun.counts?.fail || 0}` : 'missing long-run-readiness', 'longrun:readiness ready=true 前不要启动 overnight。'),
- step(10, '技术/AI', '运行策略矩阵', pipeline?.readyForClaim ? 'pass' : 'pending', pipeline ? `readyForClaim=${Boolean(pipeline.readyForClaim)}, pass=${pipeline.counts?.pass || 0}, fail=${pipeline.counts?.fail || 0}` : 'missing optimization-pipeline-summary.json', '真实材料和长跑门禁通过后再跑 optimization:pipeline。'),
- step(11, '技术/AI', '视频 A/B 验收', videoAbPassed ? 'pass' : 'pending', videoAbEvidence, '只有真实视频 A/B 通过且 status 审计为 passed 时才可用于视频提升结论。'),
- step(12, '商务', '人工复核标注', intake?.acceptance?.reviewMetricsReady ? 'pass' : 'blocked_by_external_data', intake ? `reviewMetricsReady=${Boolean(intake.acceptance?.reviewMetricsReady)}, customerEffectReady=${Boolean(intake.acceptance?.customerEffectReady)}` : 'missing intake-readiness', '补商务复核标签、负样本归因、客户选择和本轮人工补号量。'),
- step(13, '技术/AI', '商务复核指标', reviewReady ? 'pass' : 'blocked_by_external_data', review ? `overallPass=${Boolean(review.acceptance?.overallPass)}` : 'missing review-metrics-summary.json', '已标注 CSV 到位后运行 review:metrics --strict。'),
- step(14, '技术/AI', '客户效果审计', customerEffectReady ? 'pass' : 'blocked_by_external_data', customerEffect ? `overallPass=${Boolean(customerEffect.acceptance?.overallPass)}` : 'missing customer-effect-summary.json', '客户选择、历史基线和本轮人工补号量到位后运行 customer-effect:audit --strict。'),
- step(15, '技术/AI', '刷新证据和交接', round?.passed && handoff && evidence ? 'pass' : 'pending', `round=${Boolean(round?.passed)}, handoff=${Boolean(handoff)}, evidence=${Boolean(evidence)}, operatorPackOwnerArtifactCount=${operatorPackOwnerArtifactCount}`, '每轮结束刷新 evidence:index、proof-gap:closure、handoff:summary 和 round:deposition。')
- ];
- const counts = countRows(rows);
- return {
- generatedAt: new Date().toISOString(),
- root,
- outputsDir,
- complete: rows.every(row => row.status === 'pass'),
- counts,
- observed: {
- intakeReady: intakeOverallReady,
- intakeFailureCount: intake ? Number(intake.failureCount || 0) : null,
- videoReady,
- videoFailureCount: videoReadiness ? Number(videoReadiness.failureCount || 0) : null,
- longrunReady: Boolean(longrun?.ready),
- longrunFailCount: longrun ? Number(longrun.counts?.fail || 0) : null,
- proofGapOpenCount: closure ? Number(closure.openCount || 0) : null,
- handoffComplete: Boolean(handoff?.complete),
- videoAbProofStatus: videoAbStatus?.status || null,
- proofGapOperatorPackOwnerArtifactCount: operatorPackOwnerArtifactCount,
- businessExecutionOperatorPackOwnerArtifactCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactCount || 0),
- businessExecutionOperatorPackOwnerArtifactRowCount: Number(businessExecutionIndex?.operatorPackOwnerArtifactRowCount || 0),
- evidenceCounts: evidence?.counts || {}
- },
- operatorPackOwnerArtifactCount,
- operatorPackOwnerArtifacts,
- guardrails: [
- '本进度表只同步补证执行状态,不能把 sample、smoke、模板、provider fallback 或接口 200 计为客户效果证明。',
- 'blocked_by_external_data 表示需要真实历史 Brief、真实视频资源、商务复核或客户选择数据。',
- '只有全部 15 步为 pass,且 customer-effect:audit 与 proof-gap:closure 通过后,才可进入长期完成判断。',
- 'proof-gap by-owner attachments are dispatch files only; they do not prove customer effect.',
- '不得在任何输入、报告或日志中写入 sessionToken、Authorization、模型 token 或 npm token。'
- ],
- rows
- };
- }
- function statusItem(status, id) {
- return Array.isArray(status?.items) ? status.items.find(item => item.id === id) : null;
- }
- function getHistoryCustomerEffectProofMissingRequirements(history) {
- if (!history) return ['missing historical-dataset-audit.json'];
- const missing = [];
- const acceptance = history.acceptance || {};
- if (acceptance.readyForCustomerEffectProof !== true) missing.push('acceptance.readyForCustomerEffectProof=true');
- if (Number(history.briefCount || 0) < 5) missing.push('briefCount>=5');
- if (acceptance.parseOk !== true) missing.push('acceptance.parseOk=true');
- if (acceptance.minBriefsMet !== true) missing.push('acceptance.minBriefsMet=true');
- if (acceptance.allHaveBriefText !== true) missing.push('acceptance.allHaveBriefText=true');
- if (acceptance.allHaveManualFinalList !== true) missing.push('acceptance.allHaveManualFinalList=true');
- if (acceptance.allHaveCustomerDecision !== true) missing.push('acceptance.allHaveCustomerDecision=true');
- if (acceptance.allHaveFeedbackReason !== true) missing.push('acceptance.allHaveFeedbackReason=true');
- if (acceptance.allHaveManualSupplementBaseline !== true) missing.push('acceptance.allHaveManualSupplementBaseline=true');
- if (acceptance.categoryCoverageMet !== true) missing.push('acceptance.categoryCoverageMet=true');
- if (Number(history.missingCriticalCount || 0) !== 0) missing.push('missingCriticalCount=0');
- if (!Array.isArray(history.items) || history.items.length < 5) missing.push('items.length>=5');
- if (Number(history.withManualFinalList || 0) < 5) missing.push('withManualFinalList>=5');
- if (Number(history.withCustomerDecision || 0) < 5) missing.push('withCustomerDecision>=5');
- if (Number(history.withRejectionReason || 0) < 1) missing.push('withRejectionReason>=1');
- if (Number(history.withManualSupplementBaseline || 0) < 5) missing.push('withManualSupplementBaseline>=5');
- return uniqueStrings(missing);
- }
- function step(order, owner, title, status, evidence, next) {
- return { order, owner, title, status, evidence, next };
- }
- function normalizeOperatorPackOwnerArtifacts(ownerArtifacts) {
- if (!Array.isArray(ownerArtifacts)) return [];
- return ownerArtifacts.map(item => ({
- owner: item.owner || '',
- actionCount: Number(item.actionCount || 0),
- topPriority: Number(item.topPriority || 0),
- markdown: String(item.markdown || '').replace(/\\/g, '/'),
- csv: String(item.csv || '').replace(/\\/g, '/'),
- gapIds: Array.isArray(item.gapIds) ? item.gapIds : [],
- evidenceRowCount: Number(item.evidenceRowCount || 0),
- nextActionCount: Number(item.nextActionCount || 0)
- }));
- }
- function renderReport(summary) {
- const lines = [
- '# 提号补证 15 步进度表',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- 是否完成:${summary.complete ? '是' : '否'}`,
- `- pass:${summary.counts.pass || 0}`,
- `- fail:${summary.counts.fail || 0}`,
- `- pending:${summary.counts.pending || 0}`,
- `- blocked_by_external_data:${summary.counts.blocked_by_external_data || 0}`,
- '',
- '## 当前观测',
- '',
- `- intakeReady:${summary.observed.intakeReady ? 'true' : 'false'}`,
- `- intakeFailureCount:${summary.observed.intakeFailureCount ?? 'unknown'}`,
- `- videoReady:${summary.observed.videoReady ? 'true' : 'false'}`,
- `- videoFailureCount:${summary.observed.videoFailureCount ?? 'unknown'}`,
- `- longrunReady:${summary.observed.longrunReady ? 'true' : 'false'}`,
- `- longrunFailCount:${summary.observed.longrunFailCount ?? 'unknown'}`,
- `- proofGapOpenCount:${summary.observed.proofGapOpenCount ?? 'unknown'}`,
- `- handoffComplete:${summary.observed.handoffComplete ? 'true' : 'false'}`,
- `- videoAbProofStatus:${summary.observed.videoAbProofStatus ?? 'unknown'}`,
- `- proofGapOperatorPackOwnerArtifactCount:${summary.observed.proofGapOperatorPackOwnerArtifactCount ?? 'unknown'}`,
- `- businessExecutionOperatorPackOwnerArtifactCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactCount ?? 'unknown'}`,
- `- businessExecutionOperatorPackOwnerArtifactRowCount:${summary.observed.businessExecutionOperatorPackOwnerArtifactRowCount ?? 'unknown'}`,
- '',
- '## 15 步进度',
- '',
- '| 顺序 | 负责人 | 目标 | 状态 | 证据 | 下一步 |',
- '| --- | --- | --- | --- | --- | --- |',
- ...summary.rows.map(row => `| ${row.order} | ${escapeCell(row.owner)} | ${escapeCell(row.title)} | ${row.status} | ${escapeCell(row.evidence)} | ${escapeCell(row.next)} |`),
- '',
- '## proof-gap 操作包负责人附件',
- '',
- '| 负责人 | 动作数 | Markdown | CSV | 缺口 |',
- '| --- | ---: | --- | --- | --- |',
- ...summary.operatorPackOwnerArtifacts.map(item => `| ${escapeCell(item.owner)} | ${item.actionCount} | ${escapeCell(item.markdown)} | ${escapeCell(item.csv)} | ${escapeCell((item.gapIds || []).join(';'))} |`),
- '',
- '## 边界',
- '',
- ...summary.guardrails.map(item => `- ${item}`)
- ];
- return lines.join('\n');
- }
- function latestDir(outputsDir, prefix) {
- if (!fs.existsSync(outputsDir)) return '';
- const dirs = fs.readdirSync(outputsDir, { withFileTypes: true })
- .filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
- .map(entry => path.join(outputsDir, entry.name))
- .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
- return dirs[0] || '';
- }
- function latestFile(outputsDir, fileName) {
- if (!fs.existsSync(outputsDir)) return '';
- return fs.readdirSync(outputsDir, { withFileTypes: true })
- .filter(entry => entry.isDirectory())
- .map(entry => path.join(outputsDir, entry.name, fileName))
- .filter(fileExists)
- .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || '';
- }
- function readJson(file) {
- if (!file || !fs.existsSync(file)) return null;
- try {
- return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
- } catch {
- return null;
- }
- }
- function fileExists(file) {
- return Boolean(file) && fs.existsSync(file);
- }
- function countRows(rows) {
- return rows.reduce((acc, row) => {
- acc[row.status] = (acc[row.status] || 0) + 1;
- return acc;
- }, {});
- }
- function uniqueStrings(values) {
- return [...new Set((values || []).filter(Boolean).map(value => String(value)))];
- }
- 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 rel(root, file) {
- return path.relative(root, file).replace(/\\/g, '/');
- }
- function escapeCell(value) {
- return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- if (require.main === module) main();
- module.exports = {
- buildBusinessProofProgress,
- renderReport
- };
|