#!/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_EVIDENCE_INDEX_OUTPUT || path.join(OUTPUTS, `evidence-index-${Date.now()}`)); const summary = buildEvidenceIndex({ root: ROOT, outputsDir: path.resolve(args.outputs || OUTPUTS) }); fs.mkdirSync(outputDir, { recursive: true }); const jsonPath = path.join(outputDir, 'evidence-index-summary.json'); const reportPath = path.join(outputDir, 'evidence-index-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, total: summary.total, realEvidence: summary.counts.real_evidence || 0, smokeOrLocal: summary.counts.smoke_or_local || 0, notProof: summary.counts.not_business_proof || 0 }, null, 2)); } function buildEvidenceIndex({ root, outputsDir }) { const dirs = fs.existsSync(outputsDir) ? fs.readdirSync(outputsDir, { withFileTypes: true }).filter(item => item.isDirectory()).map(item => path.join(outputsDir, item.name)) : []; const entries = dirs.flatMap(dir => classifyDir(root, dir)).sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))); return { generatedAt: new Date().toISOString(), root, outputsDir, total: entries.length, counts: countBy(entries, 'proofLevel'), byType: countBy(entries, 'type'), entries }; } function classifyDir(root, dir) { const entries = []; const aggregate = readJson(path.join(dir, 'aggregate-summary.json')); if (aggregate) entries.push(classifyOvernight(root, dir, aggregate)); const status = readJson(path.join(dir, 'optimization-status-summary.json')); if (status) entries.push(classifyStatus(root, dir, status)); const readiness = readJson(path.join(dir, 'long-run-readiness-summary.json')); if (readiness) entries.push(classifyReadiness(root, dir, readiness)); const pipeline = readJson(path.join(dir, 'optimization-pipeline-summary.json')); if (pipeline) entries.push(classifyPipeline(root, dir, pipeline)); const completion = readJson(path.join(dir, 'optimization-completion-summary.json')); if (completion) entries.push(classifyOptimizationCompletion(root, dir, completion)); const handoff = readJson(path.join(dir, 'optimization-handoff-summary.json')); if (handoff) entries.push(classifyHandoff(root, dir, handoff)); const proofGap = readJson(path.join(dir, 'proof-gap-request-summary.json')); if (proofGap) entries.push(classifyProofGap(root, dir, proofGap)); const proofGapClosure = readJson(path.join(dir, 'proof-gap-closure-summary.json')); if (proofGapClosure) entries.push(classifyProofGapClosure(root, dir, proofGapClosure)); const proofGapOperatorPack = readJson(path.join(dir, 'proof-gap-operator-pack-summary.json')); if (proofGapOperatorPack) entries.push(classifyProofGapOperatorPack(root, dir, proofGapOperatorPack)); const realProofIntakeBundle = readJson(path.join(dir, 'real-proof-intake-bundle-summary.json')); if (realProofIntakeBundle) entries.push(classifyRealProofIntakeBundle(root, dir, realProofIntakeBundle)); const realProofClosureWorkOrder = readJson(path.join(dir, 'real-proof-closure-work-order-summary.json')); if (realProofClosureWorkOrder) entries.push(classifyRealProofClosureWorkOrder(root, dir, realProofClosureWorkOrder)); const realGapMaterialAudit = readJson(path.join(dir, 'real-gap-material-audit-summary.json')); if (realGapMaterialAudit) entries.push(classifyRealGapMaterialAudit(root, dir, realGapMaterialAudit)); const localSeedMaterialIndex = readJson(path.join(dir, 'local-seed-material-index-summary.json')); if (localSeedMaterialIndex) entries.push(classifyLocalSeedMaterialIndex(root, dir, localSeedMaterialIndex)); const localSeedToIntakeWorklist = readJson(path.join(dir, 'local-seed-to-intake-worklist-summary.json')); if (localSeedToIntakeWorklist) entries.push(classifyLocalSeedToIntakeWorklist(root, dir, localSeedToIntakeWorklist)); const experienceTranscriptIndex = readJson(path.join(dir, 'experience-transcript-index-summary.json')); if (experienceTranscriptIndex) entries.push(classifyExperienceTranscriptIndex(root, dir, experienceTranscriptIndex)); const feedbackLoopClosure = readJson(path.join(dir, 'feedback-loop-closure-summary.json')); if (feedbackLoopClosure) entries.push(classifyFeedbackLoopClosure(root, dir, feedbackLoopClosure)); const homepageEvidenceReadiness = readJson(path.join(dir, 'homepage-evidence-readiness-summary.json')); if (homepageEvidenceReadiness) entries.push(classifyHomepageEvidenceReadiness(root, dir, homepageEvidenceReadiness)); const proofGapSoftwareForm = readJson(path.join(dir, 'tihao-proof-gap-software-form-summary.json')); if (proofGapSoftwareForm) entries.push(classifyProofGapSoftwareForm(root, dir, proofGapSoftwareForm)); const latestFormIndex = readJson(path.join(dir, 'latest-form-index-summary.json')); if (latestFormIndex) entries.push(classifyLatestFormIndex(root, dir, latestFormIndex)); const roundDeposition = readJson(path.join(dir, 'round-deposition-summary.json')); if (roundDeposition) entries.push(classifyRoundDeposition(root, dir, roundDeposition)); const businessProofProgress = readJson(path.join(dir, 'business-proof-progress-summary.json')); if (businessProofProgress) entries.push(classifyBusinessProofProgress(root, dir, businessProofProgress)); const businessProofNextActions = readJson(path.join(dir, 'business-proof-next-actions-summary.json')); if (businessProofNextActions) entries.push(classifyBusinessProofNextActions(root, dir, businessProofNextActions)); const businessExecutionIndex = readJson(path.join(dir, 'business-execution-index-summary.json')); if (businessExecutionIndex) entries.push(classifyBusinessExecutionIndex(root, dir, businessExecutionIndex)); const intakeReadiness = readJson(path.join(dir, 'intake-readiness-summary.json')); if (intakeReadiness) entries.push(classifyIntakeReadiness(root, dir, intakeReadiness)); const intakeFieldChecklist = readJson(path.join(dir, 'intake-field-checklist-summary.json')); if (intakeFieldChecklist) entries.push(classifyIntakeFieldChecklist(root, dir, intakeFieldChecklist)); if (isDataIntakePack(dir)) entries.push(classifyDataIntakePack(root, dir)); if (isVideoIntakePack(dir)) entries.push(classifyVideoIntakePack(root, dir)); const preflight = readJson(path.join(dir, 'live-preflight-summary.json')); if (preflight) entries.push(classifyPreflight(root, dir, preflight)); const review = readJson(path.join(dir, 'review-metrics-summary.json')); if (review) entries.push(classifyReview(root, dir, review)); const customerEffect = readJson(path.join(dir, 'customer-effect-summary.json')) || readJson(path.join(dir, 'customer-effect-audit', 'customer-effect-summary.json')); if (customerEffect) entries.push(classifyCustomerEffect(root, dir, customerEffect, { pipeline })); const history = readJson(path.join(dir, 'historical-dataset-audit.json')); if (history) entries.push(classifyHistory(root, dir, history)); const video = readJson(path.join(dir, 'video-hit-rate-summary.json')); if (video) entries.push(classifyVideo(root, dir, video)); const videoRuntimePreflight = readJson(path.join(dir, 'video-hit-rate-preflight-summary.json')); if (videoRuntimePreflight) entries.push(classifyVideoRuntimePreflight(root, dir, videoRuntimePreflight)); const videoResourceReadiness = readJson(path.join(dir, 'video-resource-readiness-summary.json')); if (videoResourceReadiness) entries.push(classifyVideoResourceReadiness(root, dir, videoResourceReadiness)); if (fs.existsSync(path.join(dir, 'intake-readiness-repair-actions.csv'))) { entries.push(classifyRepairActions(root, dir, 'intake-repair-actions', 'intake-readiness-repair-actions.csv')); } if (fs.existsSync(path.join(dir, 'video-resource-repair-actions.csv'))) { entries.push(classifyRepairActions(root, dir, 'video-resource-repair-actions', 'video-resource-repair-actions.csv')); } if (fs.existsSync(path.join(dir, 'homepage-evidence-repair-actions.csv'))) { entries.push(classifyRepairActions(root, dir, 'homepage-evidence-repair-actions', 'homepage-evidence-repair-actions.csv')); } return entries; } function classifyOvernight(root, dir, aggregate) { const liveProof = Boolean(aggregate.manifest?.liveEnabled) && aggregate.acceptance?.overallPass === true && Number(aggregate.failureCount || 0) === 0 && Number(aggregate.acceptance?.failedGateCount ?? aggregate.failedGateCount ?? 0) === 0; return { type: 'overnight-quality', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: liveProof ? 'real_evidence' : aggregate.manifest?.liveEnabled ? 'not_business_proof' : 'smoke_or_local', status: aggregate.acceptance?.overallPass ? 'pass' : 'not_passed', liveEnabled: Boolean(aggregate.manifest?.liveEnabled), runCount: aggregate.runCount || 0, failureCount: aggregate.failureCount || 0, failedGateCount: aggregate.acceptance?.failedGateCount ?? aggregate.failedGateCount ?? 0, summary: `fixtures=${aggregate.fixtureCount || 0}, variants=${aggregate.variantCount || 0}, runs=${aggregate.runCount || 0}`, next: liveProof ? '可作为 live 自动化证据;仍需人工复核和客户选择数据证明真实业务效果。' : aggregate.manifest?.liveEnabled ? 'live 产物未通过全部门槛,不能作为业务证明。' : 'sample/smoke 只能证明结构和本地门槛。' }; } function classifyStatus(root, dir, status) { return { type: 'optimization-status', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: status.complete ? 'real_evidence' : 'not_business_proof', status: status.complete ? 'complete' : 'incomplete', summary: `passed=${status.counts?.passed || 0}, ready_not_proven=${status.counts?.ready_not_proven || 0}, blocked_by_external_data=${status.counts?.blocked_by_external_data || 0}`, next: status.complete ? '状态审计已完成。' : '仍有 ready_not_proven 或 blocked_by_external_data,不能宣布长期目标完成。' }; } function classifyReadiness(root, dir, readiness) { return { type: 'long-run-readiness', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: readiness.ready ? 'smoke_or_local' : 'not_business_proof', status: readiness.ready ? 'ready' : 'not_ready', mode: readiness.mode, summary: `mode=${readiness.mode}, fail=${readiness.counts?.fail || 0}, warn=${readiness.counts?.warn || 0}`, next: readiness.ready ? '可作为启动前置检查;不等同于 live 或客户效果证明。' : '先补齐失败项,再启动对应长跑。' }; } function classifyPipeline(root, dir, pipeline) { const allStepsPass = Array.isArray(pipeline.results) && pipeline.results.length > 0 && pipeline.results.every(item => item.status === 'pass' || item.status === 'planned'); const closure = pipeline.proofGapClosure || {}; const closureComplete = Boolean(closure.complete); const readyForClaim = Boolean(pipeline.readyForClaim) && closureComplete; return { type: 'optimization-pipeline', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: allStepsPass ? 'smoke_or_local' : 'not_business_proof', status: allStepsPass ? (pipeline.dryRun ? 'planned' : 'pass') : 'not_passed', summary: `steps=${pipeline.results?.length || 0}, pass=${pipeline.counts?.pass || 0}, planned=${pipeline.counts?.planned || 0}, fail=${pipeline.counts?.fail || 0}, nextActions=${pipeline.nextActions?.length || 0}, readyForClaim=${readyForClaim}, closureComplete=${closureComplete}, closureOpen=${closure.openCount ?? 'unknown'}`, next: readyForClaim ? 'pipeline 已跑通并且 proof-gap:closure complete=true;仍需回看 customer-effect、video A/B 和 live/provider 证据细项。' : 'pipeline 只能证明编排链路或局部审计完成;proof-gap:closure 未关闭前不能宣称业务效果。' }; } function classifyOptimizationCompletion(root, dir, completion) { const readyForClaim = Boolean(completion.complete && completion.readyForClaim && Number(completion.proofGapOpenCount || 0) === 0); return { type: 'optimization-completion', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: readyForClaim ? 'real_evidence' : 'not_business_proof', status: readyForClaim ? 'ready_for_claim' : 'incomplete', summary: `complete=${Boolean(completion.complete)}, readyForClaim=${Boolean(completion.readyForClaim)}, proofGapOpen=${completion.proofGapOpenCount ?? 'unknown'}, blockers=${completion.blockingReasons?.length ?? 'unknown'}`, next: readyForClaim ? '完成度审计允许进入业务效果声明;仍需回看具体客户效果、视频 A/B 和 live/provider 证据。' : '完成度审计显示仍有阻塞项,不能宣称提号率提升、客户效果达标或人工补号量下降。' }; } function classifyDataIntakePack(root, dir) { return { type: 'data-intake-pack', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: 'ready', summary: 'history template, manual review template, and README are present', next: '交给商务填写真实历史 Brief、客户选择、历史人工补号基线和本轮人工补号量;模板本身不证明业务效果。' }; } function classifyVideoIntakePack(root, dir) { return { type: 'video-intake-pack', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: 'ready', summary: 'video resource template and README are present', next: '交给商务补齐真实参考视频和候选视频的 URL、封面、ASR、帧图或正文证据;模板本身不证明视频 A/B 通过。' }; } function classifyHandoff(root, dir, handoff) { return { type: 'optimization-handoff', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: handoff.complete ? 'complete_reported' : 'incomplete_reported', summary: `ready=${handoff.readyCapabilities?.length || 0}, readyNotProven=${handoff.readyNotProven?.length || 0}, blockers=${handoff.externalBlockers?.length || 0}`, next: handoff.complete ? '交接摘要声称完成时必须回看 optimization:status 和客户效果证据。' : '可用于下一轮 AI 或商务交接;不证明客户效果。' }; } function classifyProofGap(root, dir, proofGap) { return { type: 'proof-gap-request', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: proofGap.unresolvedCount ? 'gaps_requested' : 'no_gaps_reported', summary: `gaps=${proofGap.rows?.length || 0}, unresolved=${proofGap.unresolvedCount || 0}`, next: '可作为商务/下一轮 AI 补真实证明数据的请求清单;不证明业务效果。' }; } function classifyProofGapClosure(root, dir, closure) { return { type: 'proof-gap-closure', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: closure.complete ? 'real_evidence' : 'not_business_proof', status: closure.complete ? 'all_closed' : 'open_gaps', summary: `closed=${closure.closedCount || 0}, open=${closure.openCount || 0}`, next: closure.complete ? '所有真实证明缺口均已关闭;仍需回看 optimization:status 是否 complete。' : '仍有 proof gap 未关闭,不能宣布长期优化目标完成。' }; } function classifyProofGapOperatorPack(root, dir, pack) { return { type: 'proof-gap-operator-pack', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', complete: Boolean(pack.complete), directCustomerProof: false, summary: `openCount=${Number(pack.openCount || 0)}, rowCount=${Number(pack.rowCount || 0)}`, next: '按负责人、填写文件、复验命令和边界补齐真实证明;不证明客户效果。' }; } function classifyRealProofIntakeBundle(root, dir, bundle) { return { type: 'real-proof-intake-bundle', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: bundle.complete ? 'all_real_proof_closed' : 'materials_requested', complete: Boolean(bundle.complete), directCustomerProof: false, summary: `proofOpen=${Number(bundle.proofOpenCount || 0)}, fillRows=${Number(bundle.fillRowCount || 0)}, proofGaps=${Number(bundle.proofGapRowCount || 0)}, commands=${Number(bundle.recheckCommandCount || 0)}`, next: bundle.complete ? '真实材料总包显示 proofOpenCount=0;仍需回看 customer-effect、video A/B 和 live/provider 证据细项。' : '按总包中的模板、字段缺口、修复清单和复验命令补齐真实材料;不证明客户效果。' }; } function classifyRealProofClosureWorkOrder(root, dir, summary) { return { type: 'real-proof-closure-work-order', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: summary.complete ? 'all_work_orders_closed' : 'work_orders_open', complete: Boolean(summary.complete), directCustomerProof: false, canCloseProofGap: false, summary: `workOrders=${Number(summary.workOrderCount || 0)}, owners=${Number(summary.ownerGroupCount || 0)}, proofOpen=${summary.sourceState?.proofGapOpenCount ?? 'unknown'}, intakeFailures=${summary.sourceState?.intakeFailureCount ?? 'unknown'}, realCandidateRows=${summary.sourceState?.realCandidateRows ?? 'unknown'}`, next: summary.complete ? '闭环工单显示无待补项;仍需回看 proof-gap:closure 和 optimization:completion 是否允许完成声明。' : '按负责人附件逐字段补真实历史 Brief、真实候选视频、客户选择和人工补号量;工单本身不证明客户效果。' }; } function classifyRealGapMaterialAudit(root, dir, summary) { return { type: 'real-gap-material-audit', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: summary.canCloseProofGap ? 'claimable' : 'open_gaps_have_materials', complete: Boolean(summary.complete), directCustomerProof: false, canCloseProofGap: false, summary: `gaps=${Number(summary.gapCount || 0)}, materialFound=${Number(summary.materialFoundCount || 0)}, open=${Number(summary.openGapCount || 0)}`, next: 'Use this audit to prove project materials were found for the three open gaps; do not use it as customer-effect, video A/B, or manual-supplement business proof.' }; } function classifyProofGapSoftwareForm(root, dir, form) { return { type: 'proof-gap-software-form', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: form.passed ? 'smoke_or_local' : 'not_business_proof', status: form.passed ? 'pass' : 'not_passed', summary: `rows=${form.rowCount || 0}, duplicateId=${form.duplicateIdCount || 0}, header=${Boolean(form.headerMatches)}, rowWidth=${Boolean(form.rowWidthOk)}`, next: '用于软件端/商务补证任务表;不证明客户效果或命中率提升。' }; } function classifyLatestFormIndex(root, dir, index) { return { type: 'latest-form-index', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: index.passed ? 'smoke_or_local' : 'not_business_proof', status: index.passed ? 'pass' : 'not_passed', summary: `actions=${index.nextActions?.actionCount || 0}, proofOpen=${index.nextActions?.proofOpenCount ?? 'unknown'}, primaryForm=${Boolean(index.primaryForm?.csv)}, clientList=${Boolean(index.clientList?.csv)}`, next: '用于快速定位最新补证表单、候选名单和负责人行动文件;不证明客户效果或命中率提升。' }; } function classifyRoundDeposition(root, dir, roundDeposition) { return { type: 'round-deposition', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: roundDeposition.passed ? 'smoke_or_local' : 'not_business_proof', status: roundDeposition.passed ? 'pass' : 'not_passed', summary: `pass=${roundDeposition.counts?.pass || 0}, warn=${roundDeposition.counts?.warn || 0}, fail=${roundDeposition.counts?.fail || 0}`, next: roundDeposition.passed ? '可证明本轮交接沉淀完整;仍不证明客户效果或命中率提升。' : '先补齐实施日志、证据台账、状态审计或交接摘要,再进入下一轮。' }; } function classifyBusinessProofProgress(root, dir, progress) { return { type: 'business-proof-progress', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: progress.complete ? 'all_steps_pass' : 'in_progress', summary: `pass=${progress.counts?.pass || 0}, fail=${progress.counts?.fail || 0}, pending=${progress.counts?.pending || 0}, blocked=${progress.counts?.blocked_by_external_data || 0}`, next: progress.complete ? '15 步补证进度均为 pass;仍需回看 proof-gap:closure 和 customer-effect:audit 是否真实通过。' : '用于实时同步补证步骤进度;不证明客户效果或命中率提升。' }; } function classifyBusinessProofNextActions(root, dir, nextActions) { return { type: 'business-proof-next-actions', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: nextActions.complete ? 'no_actions_needed' : 'actions_open', summary: `actions=${nextActions.actions?.length || 0}, progressComplete=${Boolean(nextActions.sourceState?.progressComplete)}, proofGapOpen=${nextActions.sourceState?.proofGapOpenCount ?? 'unknown'}`, next: nextActions.complete ? '行动队列显示无待办;仍需回看 proof-gap:closure 和 customer-effect:audit 是否真实通过。' : '用于把 blocked/fail/pending 转成可分派任务;不证明客户效果或命中率提升。' }; } function classifyBusinessExecutionIndex(root, dir, index) { return { type: 'business-execution-index', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: index.passed ? (index.proofOpenCount > 0 ? 'actions_open' : 'ready_for_claim_audit') : 'not_passed', summary: `rows=${index.rowCount || 0}, actions=${index.actionRowCount || 0}, repairs=${index.repairRowCount || 0}, proofOpen=${index.proofOpenCount ?? 'unknown'}`, next: '用于商务集中定位补证表单、负责人文件和修复清单;不证明客户效果或命中率提升。' }; } function classifyLocalSeedMaterialIndex(root, dir, summary) { return { type: 'local-seed-material-index', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: summary.existingMaterialCount > 0 ? 'seed_materials_available' : 'seed_materials_not_found', complete: false, directCustomerProof: false, canCloseProofGap: false, summary: `materials=${summary.existingMaterialCount || 0}/${summary.materialCount || 0}, liveCandidates=${summary.promisingLiveAggregateCount || 0}, canCloseProofGap=false`, next: '可用于补真实 Brief、候选池和视频资源模板;不能替代客户选择、人工补号量、真实候选视频或 proof-gap:closure。' }; } function classifyLocalSeedToIntakeWorklist(root, dir, summary) { return { type: 'local-seed-to-intake-worklist', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: Number(summary.counts?.historyDraftRows || 0) > 0 ? 'seed_worklist_ready' : 'seed_worklist_empty', complete: false, directCustomerProof: false, canCloseProofGap: false, summary: `historyDraft=${summary.counts?.historyDraftRows || 0}, references=${summary.counts?.referenceSeedRows || 0}, candidates=${summary.counts?.candidateSeedRows || 0}, videoWorklist=${summary.counts?.videoWorklistRows || 0}, canCloseProofGap=false`, next: '可作为商务/投放把本地 DHA 种子转写进真实 intake 模板的补表清单;不能替代客户选择、人工补号量、真实候选视频或 proof-gap:closure。' }; } function classifyExperienceTranscriptIndex(root, dir, summary) { return { type: 'experience-transcript-index', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: Number(summary.transcriptCount || 0) > 0 ? 'experience_seed_available' : 'experience_seed_missing', complete: false, directCustomerProof: false, canCloseProofGap: false, summary: `transcripts=${summary.transcriptCount || 0}, rules=${summary.coveredRuleCount || 0}/${summary.ruleCount || 0}, canCloseProofGap=false`, next: '可用于追踪提号经验来源和校准规则实现;不能替代真实历史 Brief、客户选择、人工补号量或 customer-effect:audit。' }; } function classifyFeedbackLoopClosure(root, dir, closure) { return { type: 'feedback-loop-closure', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: closure.directCustomerProof ? 'real_evidence' : closure.passed ? 'smoke_or_local' : 'not_business_proof', status: closure.directCustomerProof ? 'direct_customer_proof' : closure.passed ? 'local_loop_passed' : 'missing_or_open', summary: `negative=${closure.negativeFeedbackCount || 0}, blocked=${closure.blockedCreatorCount || 0}, leaked=${closure.leakedCount || 0}, directCustomerProof=${Boolean(closure.directCustomerProof)}`, next: closure.directCustomerProof ? '反馈二轮闭环已有真实客户反馈和真实二轮结果;仍需 customer-effect:audit 证明客户效果。' : '用于证明反馈剔除/降权闭环是否执行;缺真实客户反馈或真实二轮结果时不能证明客户效果。' }; } function classifyHomepageEvidenceReadiness(root, dir, readiness) { return { type: 'homepage-evidence-readiness', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: readiness.ready ? 'smoke_or_local' : 'not_business_proof', status: readiness.ready ? 'ready' : 'not_ready', summary: `candidates=${readiness.counts?.candidates || 0}, providerEvidence=${readiness.counts?.providerEvidenceCreators || 0}, posts=${readiness.counts?.creatorsWithPosts || 0}, failureCount=${readiness.failureCount || 0}`, next: readiness.ready ? '可作为主页近期内容证据进入强推荐复核;仍需真实人工复核和客户选择证明命中率。' : '先补齐最近 10/20 篇内容、封面、标题、互动、发布时间和风险信号,不能把 fallback 当成强推荐证据。' }; } function classifyIntakeReadiness(root, dir, intakeReadiness) { const ready = intakeReadiness.acceptance?.overallReady === true && Number(intakeReadiness.failureCount || 0) === 0; return { type: 'intake-readiness', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: ready ? 'smoke_or_local' : 'not_business_proof', status: ready ? 'ready' : 'not_ready', summary: `history=${intakeReadiness.acceptance?.historyReady ? 'ready' : 'not_ready'}, video=${intakeReadiness.acceptance?.videoReady ? 'ready' : 'not_ready'}, review=${intakeReadiness.acceptance?.reviewMetricsReady ? 'ready' : 'not_ready'}, customerEffect=${intakeReadiness.acceptance?.customerEffectReady ? 'ready' : 'not_ready'}, failureCount=${intakeReadiness.failureCount || 0}`, next: ready ? '可进入 history:audit、video:resource-readiness、review:metrics 和 customer-effect:audit;仍不等同于业务效果证明。' : '先把模板占位替换为真实历史 Brief、真实视频资源、客户选择和本轮人工补号量。' }; } function classifyIntakeFieldChecklist(root, dir, checklist) { return { type: 'intake-field-checklist', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'smoke_or_local', status: Number(checklist.itemCount || 0) > 0 ? 'actions_open' : 'empty', summary: `items=${checklist.itemCount || 0}, missingRequired=${checklist.missingRequiredCount || 0}, placeholders=${checklist.placeholderCount || 0}`, next: '用于指导商务/投放逐字段补齐真实历史 Brief、人工复核、客户选择和视频资源;不证明客户效果。' }; } function classifyPreflight(root, dir, preflight) { const failureCount = Array.isArray(preflight.checks) ? preflight.checks.filter(item => item.status === 'fail').length : 0; const warnCount = Array.isArray(preflight.checks) ? preflight.checks.filter(item => item.status === 'warn').length : 0; return { type: 'live-preflight', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: preflight.readyForLiveAcceptance ? 'smoke_or_local' : 'not_business_proof', status: preflight.readyForLiveAcceptance ? 'ready' : 'not_ready', readyForVideoAb: Boolean(preflight.readyForVideoAb), summary: `live=${preflight.readyForLiveAcceptance ? 'ready' : 'not_ready'}, videoAb=${preflight.readyForVideoAb ? 'ready' : 'not_ready'}, fail=${failureCount}, warn=${warnCount}`, next: preflight.readyForLiveAcceptance ? preflight.readyForVideoAb ? '可作为 live/video A/B 启动前置证据;仍不等同于业务效果证明。' : '可启动 live 子集;视频 A/B 还需补齐视频分析配置。' : '缺 sessionToken、company 或 provider 关键配置时,不能启动 live 长跑。' }; } function classifyReview(root, dir, review) { return { type: 'review-metrics', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: review.acceptance?.customerSelectedRatePass === true ? 'real_evidence' : 'not_business_proof', status: review.acceptance?.overallPass ? 'pass' : 'not_passed', summary: `rows=${review.total || 0}, usable=${pct(review.businessUsableRate)}, negative=${pct(review.offTargetHardFailRate)}, selected=${review.selectedCount ? pct(review.customerSelectedRate) : 'unmeasured'}`, next: review.selectedCount ? '可用于客户选择效果复盘。' : '缺客户选择字段时不能证明客户选中率。' }; } function classifyCustomerEffect(root, dir, customerEffect, context = {}) { const effectProof = customerEffect.acceptance?.overallPass === true && customerEffect.acceptance?.customerSelectedRate30Pass === true && customerEffect.acceptance?.referenceCustomerSelectedRate40Pass !== false && customerEffect.acceptance?.historyReadyForCustomerEffectProof === true && customerEffect.acceptance?.manualSupplementBaselineAvailable === true && customerEffect.acceptance?.currentManualSupplementAvailable === true && customerEffect.acceptance?.manualSupplementReduction50Pass === true; const pipelineBlocker = getPipelineScopedCustomerEffectBlocker(context.pipeline); const acceptedProof = effectProof && !pipelineBlocker; return { type: 'customer-effect', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: acceptedProof ? 'real_evidence' : 'not_business_proof', status: customerEffect.acceptance?.overallPass ? 'pass' : 'not_passed', pipelineScoped: Boolean(context.pipeline), pipelineClaimable: context.pipeline ? !pipelineBlocker : null, summary: `selected=${customerEffect.acceptance?.customerSelectedRateMeasured ? pct(customerEffect.customerSelectedRate) : 'unmeasured'}, reference=${customerEffect.acceptance?.referenceCustomerSelectedRateMeasured ? pct(customerEffect.referenceCustomerSelectedRate) : 'unmeasured'}, supplementReduction=${customerEffect.manualSupplementReductionRate === null || customerEffect.manualSupplementReductionRate === undefined ? 'unmeasured' : pct(customerEffect.manualSupplementReductionRate)}${pipelineBlocker ? `, pipelineBlocked=${pipelineBlocker.reasons.join('+')}` : ''}`, next: acceptedProof ? '可作为客户效果证明:客户选中率、参考链路和人工补号减少门槛已通过。' : pipelineBlocker ? '该 customer-effect 位于未完成或不可声明的 optimization-pipeline 中;必须先刷新当前 proof-gap closure 和 completion,不能用旧 pipeline 内嵌产物证明客户效果完成。' : '缺客户选择、历史补号基线、本轮补号量或减少率未达标时,不能证明客户效果完成。' }; } function getPipelineScopedCustomerEffectBlocker(pipeline) { if (!pipeline) return null; const openCount = Number(pipeline.proofGapClosure?.openCount || 0); const closureComplete = pipeline.proofGapClosure?.complete === true && openCount === 0; if (pipeline.readyForClaim === true && closureComplete) return null; const reasons = []; if (pipeline.readyForClaim !== true) reasons.push('readyForClaim=false'); if (pipeline.proofGapClosure?.complete !== true) reasons.push('proofGapClosure.complete=false'); if (openCount !== 0) reasons.push(`proofGapClosure.openCount=${openCount}`); return { reasons }; } function classifyHistory(root, dir, history) { const missingCustomerEffectProof = getHistoryCustomerEffectProofMissingRequirements(history); const readyForCustomerEffectProof = missingCustomerEffectProof.length === 0; const readyForLongRun = readyForCustomerEffectProof || isHistoryReadyForLongRun(history); return { type: 'historical-dataset', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: readyForCustomerEffectProof ? 'real_evidence' : readyForLongRun ? 'smoke_or_local' : 'not_business_proof', status: readyForLongRun ? 'ready_for_long_run' : 'not_ready', summary: `briefs=${history.briefCount || 0}, categories=${history.categoryCount || 0}, customerDecision=${history.withCustomerDecision || 0}, missing=${missingCustomerEffectProof.length}`, missingProofRequirements: missingCustomerEffectProof, next: readyForCustomerEffectProof ? '可用于客户效果证明前置数据。' : '缺客户选择、拒绝原因、人工补号量基线或可审计明细时不能证明客户效果。' }; } function isHistoryReadyForLongRun(history) { if (!history) return false; const acceptance = history.acceptance || {}; return Boolean( acceptance.readyForLongRun === true && acceptance.parseOk === true && acceptance.minBriefsMet === true && acceptance.allHaveBriefText === true && acceptance.allHaveManualFinalList === true && acceptance.allHaveCustomerDecision === true && acceptance.allHaveFeedbackReason === true && acceptance.categoryCoverageMet === true && Number(history.briefCount || 0) >= 5 && Array.isArray(history.items) && history.items.length >= 5 && Number(history.missingCriticalCount || 0) === 0 ); } 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 classifyVideo(root, dir, video) { const videoProof = video.acceptance?.passed === true && video.acceptance?.realReferenceResourceLoaded === true && video.acceptance?.realVideoResourceLoaded === true && video.acceptance?.coverOrFrameOrAsrLoaded === true && video.acceptance?.evidenceCardsNotPending === true && video.acceptance?.evidenceCardsHaveSignals === true && video.acceptance?.strongNotDegraded === true && video.acceptance?.top10EvidenceImproved === true; return { type: 'video-ab', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: videoProof ? 'real_evidence' : 'not_business_proof', status: video.acceptance?.passed ? 'pass' : 'not_passed', summary: `strongDelta=${video.delta?.strong ?? 'n/a'}, evidenceDelta=${video.delta?.top10EvidenceHitCandidates ?? 'n/a'}`, next: videoProof ? '可作为视频证据提升的 A/B 证明。' : '不能宣称视频分析提升命中率。' }; } function classifyVideoRuntimePreflight(root, dir, preflight) { return { type: 'video-ab-runtime-preflight', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: 'not_business_proof', status: preflight.readyForVideoAb ? 'ready_to_run' : 'not_ready', summary: `readyForVideoAb=${Boolean(preflight.readyForVideoAb)}, failureCount=${preflight.failureCount || 0}, generatedBy=${preflight.proofContext?.generatedBy || 'unknown'}`, next: preflight.readyForVideoAb ? '只表示运行环境可启动 acceptance:video-ab;必须生成 video-hit-rate-summary.json 才能进入 proof-gap closure。' : '补齐运行时凭证、company、VOC social provider 和视频分析 provider 后再运行 acceptance:video-ab,并生成 live video-hit-rate-summary.json;preflight 失败不证明视频提升。' }; } function renderReport(summary) { const latest = pickLatestUseful(summary.entries); const lines = [ '# 提号优化证据台账', '', `- 生成时间:${summary.generatedAt}`, `- 扫描目录:${summary.outputsDir}`, `- 证据条目:${summary.total}`, '', '## 汇总', '', ...Object.entries(summary.counts).map(([key, value]) => `- ${key}: ${value}`), '', '## 最新可用证据', '', '| 类型 | 证明等级 | 状态 | 目录 | 摘要 | 下一步 |', '| --- | --- | --- | --- | --- | --- |', ...latest.map(item => row(item)), '', '## 全部条目', '', '| 类型 | 证明等级 | 状态 | 更新时间 | 目录 | 摘要 | 下一步 |', '| --- | --- | --- | --- | --- | --- | --- |', ...summary.entries.map(item => row(item, true)), '', '## 说明', '', '- `real_evidence` 表示该产物包含真实 live、客户选择、历史数据或视频 A/B 证据,且关键门槛通过。', '- `smoke_or_local` 表示可证明结构、门槛或启动前置条件,但不能证明真实客户效果。', '- `not_business_proof` 表示该产物明确显示仍缺真实数据或状态未完成。' ]; return lines.join('\n'); } function classifyVideoResourceReadiness(root, dir, readiness) { return { type: 'video-resource-readiness', dir: rel(root, dir), updatedAt: mtime(dir), proofLevel: readiness.acceptance?.readyForVideoAbPreflight ? 'smoke_or_local' : 'not_business_proof', status: readiness.acceptance?.readyForVideoAbPreflight ? 'ready' : 'not_ready', summary: `realReference=${readiness.counts?.realReferenceRows || 0}, realCandidate=${readiness.counts?.realCandidateRows || 0}, videoUrl=${readiness.counts?.videoUrlRows || 0}, failureCount=${readiness.failureCount || 0}`, next: readiness.acceptance?.readyForVideoAbPreflight ? '可作为视频 A/B 启动前资源证明;仍需运行 acceptance:video-ab 证明 provider、证据卡和 Top10 非退化。' : '先补齐真实参考视频、候选视频、视频 URL、封面/ASR/帧图或正文证据。' }; } function classifyRepairActions(root, dir, type, fileName) { const csvPath = path.join(dir, fileName); const actionCount = countCsvRows(csvPath); const isVideo = type === 'video-resource-repair-actions'; const isHomepage = type === 'homepage-evidence-repair-actions'; return { type, dir: rel(root, dir), updatedAt: fs.statSync(csvPath).mtime.toISOString(), proofLevel: 'smoke_or_local', status: actionCount > 0 ? 'actions_open' : 'empty', summary: `csv=${fileName}, actions=${actionCount}`, next: isHomepage ? '用于指导商务/投放补齐主页近期内容、封面、发布时间、互动和风险信号;不证明客户效果。' : isVideo ? '用于指导商务/投放补齐真实视频 URL、封面、ASR、帧图或正文证据;不证明视频 A/B 或命中率提升。' : '用于指导商务补齐真实历史 Brief、客户选择、人工补号基线和复核字段;不证明客户效果。' }; } function pickLatestUseful(entries) { const byType = new Map(); for (const entry of entries) { if (!byType.has(entry.type)) byType.set(entry.type, entry); } return [...byType.values()]; } function row(item, includeTime = false) { const cells = includeTime ? [item.type, item.proofLevel, item.status, item.updatedAt, item.dir, item.summary, item.next] : [item.type, item.proofLevel, item.status, item.dir, item.summary, item.next]; return `| ${cells.map(escapeCell).join(' | ')} |`; } function readJson(file) { if (!fs.existsSync(file)) return null; try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; } } function isDataIntakePack(dir) { return fs.existsSync(path.join(dir, 'history-data-template.csv')) && fs.existsSync(path.join(dir, 'manual-review-template.csv')) && fs.existsSync(path.join(dir, 'README.md')); } function isVideoIntakePack(dir) { return fs.existsSync(path.join(dir, 'video-resource-template.csv')) && fs.existsSync(path.join(dir, 'README.md')); } function countBy(entries, key) { return entries.reduce((acc, item) => { acc[item[key]] = (acc[item[key]] || 0) + 1; return acc; }, {}); } function mtime(dir) { return fs.statSync(dir).mtime.toISOString(); } function rel(root, file) { return path.relative(root, file).replace(/\\/g, '/'); } function pct(value) { return `${Math.round(Number(value || 0) * 100)}%`; } function countCsvRows(file) { const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '').trim(); if (!text) return 0; return Math.max(0, text.split(/\r?\n/).length - 1); } 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 escapeCell(value) { return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' '); } function withBom(text) { return `\uFEFF${text}`; } if (require.main === module) main(); module.exports = { buildEvidenceIndex, classifyDir };