| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- #!/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_PROOF_GAP_OUTPUT || path.join(OUTPUTS, `proof-gap-request-${Date.now()}`));
- const summary = buildProofGapRequest({
- root: ROOT,
- handoffPath: args.handoff ? path.resolve(args.handoff) : latestFile(OUTPUTS, 'optimization-handoff-summary.json'),
- evidencePath: args.evidence ? path.resolve(args.evidence) : latestFile(OUTPUTS, 'evidence-index-summary.json'),
- videoReadinessPath: args.videoReadiness ? path.resolve(args.videoReadiness) : latestFile(OUTPUTS, 'video-resource-readiness-summary.json'),
- closurePath: args.closure ? path.resolve(args.closure) : latestFile(OUTPUTS, 'proof-gap-closure-summary.json')
- });
- fs.mkdirSync(outputDir, { recursive: true });
- const jsonPath = path.join(outputDir, 'proof-gap-request-summary.json');
- const reportPath = path.join(outputDir, 'proof-gap-request-report.md');
- const csvPath = path.join(outputDir, 'proof-gap-request-table.csv');
- fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
- fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
- writeCsv(csvPath, summary.rows);
- console.log(JSON.stringify({
- outputDir,
- json: jsonPath,
- report: reportPath,
- csv: csvPath,
- complete: summary.complete,
- gapCount: summary.rows.length,
- unresolvedCount: summary.unresolvedCount
- }, null, 2));
- }
- function buildProofGapRequest({ root, handoffPath, evidencePath, videoReadinessPath, closurePath }) {
- const handoff = readJson(handoffPath) || {};
- const evidence = readJson(evidencePath) || {};
- const videoReadiness = readJson(videoReadinessPath) || {};
- const closure = readJson(closurePath) || {};
- const rows = buildRows({ handoff, evidence, videoReadiness, closure });
- return {
- generatedAt: new Date().toISOString(),
- root,
- complete: false,
- sources: {
- handoff: rel(root, handoffPath),
- evidence: rel(root, evidencePath),
- videoReadiness: rel(root, videoReadinessPath),
- closure: rel(root, closurePath)
- },
- observed: {
- statusCounts: handoff.statusCounts || {},
- evidenceCounts: evidence.counts || handoff.evidenceCounts || {},
- videoReadinessReady: Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight),
- videoReadinessFailureCount: videoReadiness.failureCount ?? null,
- proofGapClosureComplete: Boolean(closure.complete),
- proofGapClosureOpenCount: closure.openCount ?? null
- },
- unresolvedCount: rows.filter(row => row.status !== 'ready').length,
- rows,
- guardrails: [
- '不要在 CSV、报告或聊天中写入 sessionToken、模型 token、Authorization、npm token 或内部鉴权参数。',
- 'sample、smoke、模板、dry-run 和 provider fallback 不能作为客户效果证明。',
- '视频资源就绪只代表可以启动视频 A/B,不能替代 acceptance:video-ab。',
- '客户效果必须由 customer-effect:audit 证明,缺客户选择或人工补号基线时不得宣称完成。'
- ]
- };
- }
- function buildRows({ handoff, evidence, videoReadiness, closure }) {
- const statusCounts = handoff.statusCounts || {};
- const videoReady = Boolean(videoReadiness.acceptance?.readyForVideoAbPreflight);
- const realCandidateRows = Number(videoReadiness.counts?.realCandidateRows || 0);
- const rows = [
- row({
- id: 'historical-dataset',
- owner: '商务',
- status: 'missing_real_data',
- gap: '真实历史 Brief、人工最终名单、客户选择、拒绝原因和历史人工补号量基线未完成独立审计。',
- required: '按 data-intake-pack 填写 history-data-template.csv,并包含 5-10 个真实历史 Brief。',
- templateCommand: 'npm run data:intake-template -- --output outputs\\data-intake-pack-latest',
- templateArtifact: 'outputs\\data-intake-pack-latest\\history-data-template.csv;outputs\\data-intake-pack-latest\\manual-review-template.csv',
- verify: 'npm run history:from-csv -- --input <历史数据CSV> --output <history-dataset目录>;npm run history:audit -- --input <history-dataset目录> --output <历史审计输出目录> --strict',
- pass: 'history:audit 显示 readyForCustomerEffectProof=true,且客户选择、拒绝原因、历史人工补号量基线齐全。'
- }),
- row({
- id: 'video-real-candidate',
- owner: '商务/投放',
- status: videoReady ? 'ready' : 'missing_real_data',
- gap: videoReady ? '视频 A/B 前置资源已具备。' : `视频资源 readiness 未通过;当前真实候选视频行=${realCandidateRows}。`,
- required: '补齐至少一个真实参考视频和一个真实候选视频,提供视频 URL,并补封面、ASR、帧图或正文证据之一。',
- templateCommand: 'npm run video:intake-template -- --output outputs\\video-intake-pack-latest',
- templateArtifact: 'outputs\\video-intake-pack-latest\\video-resource-template.csv',
- verify: 'npm run video:resource-readiness -- --input <video-resource-template.csv> --output <视频资源审计输出目录> --strict',
- pass: 'readyForVideoAbPreflight=true,failureCount=0。'
- }),
- row({
- id: 'video-ab-live-proof',
- owner: '技术/AI',
- status: 'ready_not_proven',
- gap: '视频 A/B 脚本和 smoke 已就绪,但缺真实 provider + 真实视频资源下的 live 结果。',
- required: '配置真实 sessionToken、company、VOC social provider、视频分析 provider、doubao-seed-2-0-pro 模型和真实参考视频链接。',
- templateCommand: 'npm run video:intake-template -- --output outputs\\video-intake-pack-latest',
- templateArtifact: 'outputs\\video-intake-pack-latest\\video-resource-template.csv',
- verify: 'npm run acceptance:video-ab',
- pass: '真实参考资源、真实视频 URL、封面/ASR/帧图、provider ok、证据卡、Top10 非退化、强推荐不下降全部通过。'
- }),
- row({
- id: 'live-provider-overnight-proof',
- owner: '技术/AI',
- status: 'blocked_by_external_data',
- gap: '真实 live/provider 长跑尚无完整通过证明。',
- required: '准备有效 sessionToken、company、provider 配置、真实历史数据和长跑策略矩阵。',
- templateCommand: 'npm run data:intake-template -- --output outputs\\data-intake-pack-latest',
- templateArtifact: 'outputs\\data-intake-pack-latest\\history-data-template.csv',
- verify: 'npm run longrun:readiness;npm run optimization:pipeline 或 overnight live 矩阵命令',
- pass: 'liveEnabled=true,failureCount=0,failedGateCount=0,overallPass=true,且严格 provider 门禁按需开启。'
- }),
- row({
- id: 'manual-review-and-customer-effect',
- owner: '商务',
- status: 'blocked_by_external_data',
- gap: '缺本轮商务复核、客户最终选择和本轮人工补号量,无法证明客户效果。',
- required: '按 manual-review-template.csv 标注人工复核标签、客户选择、归因类型、反馈原因、本轮人工补号量。',
- templateCommand: 'npm run data:intake-template -- --output outputs\\data-intake-pack-latest',
- templateArtifact: 'outputs\\data-intake-pack-latest\\manual-review-template.csv',
- verify: 'npm run review:metrics -- --input <已标注CSV> --output <复核指标输出目录> --strict;npm run customer-effect:audit -- --review-csv <已标注CSV> --history-audit <historical-dataset-audit.json> --current-manual-supplement-count <本轮人工补号量> --output <客户效果输出目录> --strict',
- pass: '客户选中率、参考链路客户选中率、历史人工补号基线、本轮人工补号量和人工补号减少率全部通过。'
- })
- ];
- if (Number(statusCounts.ready_not_proven || 0) === 0 && Number(statusCounts.blocked_by_external_data || 0) === 0) {
- rows.push(row({
- id: 'status-audit',
- owner: 'AI',
- status: 'ready',
- gap: '状态审计没有 ready_not_proven 或 blocked_by_external_data。',
- required: '维持当前证据链。',
- templateCommand: '无需模板',
- templateArtifact: '无',
- verify: 'npm run optimization:status',
- pass: 'complete=true。'
- }));
- }
- return applyClosureStatuses(rows, closure);
- }
- function applyClosureStatuses(rows, closure) {
- const closureRows = Array.isArray(closure?.rows) ? closure.rows : [];
- if (!closureRows.length) return rows;
- const closedById = new Map(
- closureRows
- .filter(row => row && row.status === 'closed' && row.id)
- .map(row => [row.id, row])
- );
- return rows.map(item => {
- const closed = closedById.get(item.id);
- if (!closed) return item;
- return {
- ...item,
- status: 'ready',
- gap: `proof-gap:closure 已关闭:${closed.evidence || item.gap}`,
- required: `${item.required} 当前 closure 状态为 closed,保留本行作为证据链入口。`
- };
- });
- }
- function row(value) {
- return {
- id: value.id,
- owner: value.owner,
- status: value.status,
- gap: value.gap,
- required: value.required,
- templateCommand: value.templateCommand || '',
- templateArtifact: value.templateArtifact || '',
- verify: value.verify,
- pass: value.pass
- };
- }
- function renderReport(summary) {
- const lines = [
- '# 提号真实证明缺口请求包',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- 是否完成长期目标:否`,
- `- 未解决缺口数:${summary.unresolvedCount}`,
- '',
- '## 当前观测',
- '',
- `- status.passed:${summary.observed.statusCounts.passed || 0}`,
- `- status.ready_not_proven:${summary.observed.statusCounts.ready_not_proven || 0}`,
- `- status.blocked_by_external_data:${summary.observed.statusCounts.blocked_by_external_data || 0}`,
- `- evidence.real_evidence:${summary.observed.evidenceCounts.real_evidence || 0}`,
- `- evidence.smoke_or_local:${summary.observed.evidenceCounts.smoke_or_local || 0}`,
- `- evidence.not_business_proof:${summary.observed.evidenceCounts.not_business_proof || 0}`,
- `- video.readyForVideoAbPreflight:${summary.observed.videoReadinessReady ? 'true' : 'false'}`,
- `- video.failureCount:${summary.observed.videoReadinessFailureCount ?? 'unknown'}`,
- `- closure.complete:${summary.observed.proofGapClosureComplete ? 'true' : 'false'}`,
- `- closure.openCount:${summary.observed.proofGapClosureOpenCount ?? 'unknown'}`,
- '',
- '## 请求明细',
- '',
- '| ID | 负责人 | 状态 | 缺口 | 需要补齐 | 模板命令 | 模板产物 | 验证命令 | 通过标准 |',
- '| --- | --- | --- | --- | --- | --- | --- | --- | --- |',
- ...summary.rows.map(item => `| ${item.id} | ${item.owner} | ${item.status} | ${escapeCell(item.gap)} | ${escapeCell(item.required)} | \`${escapeCell(item.templateCommand)}\` | ${escapeCell(item.templateArtifact)} | \`${escapeCell(item.verify)}\` | ${escapeCell(item.pass)} |`),
- '',
- '## 边界',
- '',
- ...summary.guardrails.map(item => `- ${item}`)
- ];
- return lines.join('\n');
- }
- function writeCsv(file, rows) {
- const header = ['缺口ID', '负责人', '状态', '当前缺口', '需要补齐', '模板命令', '模板产物', '验证命令', '通过标准'];
- const body = rows.map(item => [item.id, item.owner, item.status, item.gap, item.required, item.templateCommand, item.templateArtifact, item.verify, item.pass]);
- fs.writeFileSync(file, withBom([header, ...body].map(row => row.map(csvCell).join(',')).join('\n')), 'utf8');
- }
- 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(file => fs.existsSync(file))
- .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 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 file ? path.relative(root, file).replace(/\\/g, '/') : '';
- }
- function escapeCell(value) {
- return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
- }
- function csvCell(value) {
- const text = String(value ?? '');
- return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- if (require.main === module) main();
- module.exports = {
- buildProofGapRequest,
- renderReport
- };
|