| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822 |
- #!/usr/bin/env node
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.resolve(__dirname, '..');
- const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'real-proof-closure-work-order-latest');
- function main() {
- const args = parseArgs(process.argv.slice(2));
- const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs'));
- const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
- const summary = buildRealProofClosureWorkOrder({ root: ROOT, outputsDir });
- fs.mkdirSync(outputDir, { recursive: true });
- const ownerArtifacts = writeOwnerArtifacts(outputDir, summary.workOrders);
- summary.ownerArtifacts = ownerArtifacts;
- const summaryPath = path.join(outputDir, 'real-proof-closure-work-order-summary.json');
- const reportPath = path.join(outputDir, 'real-proof-closure-work-order.md');
- const csvPath = path.join(outputDir, 'real-proof-closure-work-order.csv');
- fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2), 'utf8');
- fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
- fs.writeFileSync(csvPath, withBom(renderCsv(summary.workOrders)), 'utf8');
- console.log(JSON.stringify({
- outputDir,
- summary: summaryPath,
- report: reportPath,
- csv: csvPath,
- ownerArtifacts: ownerArtifacts.length,
- passed: summary.passed,
- complete: summary.complete,
- workOrderCount: summary.workOrderCount,
- proofGapOpenCount: summary.sourceState.proofGapOpenCount,
- canCloseProofGap: summary.canCloseProofGap
- }, null, 2));
- if (args.strict && !summary.passed) process.exitCode = 1;
- }
- function buildRealProofClosureWorkOrder({ root, outputsDir }) {
- const sources = {
- intakeReadiness: latestSummary(outputsDir, 'intake-readiness-summary.json', 'intake-readiness-latest'),
- videoReadiness: latestSummary(outputsDir, 'video-resource-readiness-summary.json', 'video-resource-readiness-latest'),
- proofGapClosure: latestSummary(outputsDir, 'proof-gap-closure-summary.json', 'proof-gap-closure-latest'),
- optimizationCompletion: latestSummary(outputsDir, 'optimization-completion-summary.json', 'optimization-completion-latest'),
- localSeedWorklist: latestSummary(outputsDir, 'local-seed-to-intake-worklist-summary.json', 'local-seed-to-intake-worklist-latest'),
- nextActions: latestSummary(outputsDir, 'business-proof-next-actions-summary.json', 'business-proof-next-actions-latest')
- };
- const intake = sources.intakeReadiness.json || {};
- const video = sources.videoReadiness.json || {};
- const closure = sources.proofGapClosure.json || {};
- const completion = sources.optimizationCompletion.json || {};
- const seed = sources.localSeedWorklist.json || {};
- const nextActions = sources.nextActions.json || {};
- const workOrders = dedupeWorkOrders([
- ...workOrdersFromIntake(intake, seed),
- ...workOrdersFromVideoReadiness(video, seed),
- ...workOrdersFromProofGapClosure(closure, nextActions, seed),
- ...workOrdersFromOptimizationCompletion(completion)
- ]).sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
- const ownerGroups = groupByOwner(workOrders);
- const proofGapOpenCount = Number(closure.openCount ?? completion.proofGapOpenCount ?? nextActions.sourceState?.proofGapOpenCount ?? 0);
- const complete = proofGapOpenCount === 0 &&
- Boolean(closure.complete) &&
- Boolean(completion.complete) &&
- Boolean(completion.readyForClaim);
- const passed = workOrders.every(item =>
- item.owner &&
- item.targetFile &&
- item.targetFields.length &&
- item.requiredAction &&
- item.acceptanceCommand &&
- item.acceptance &&
- item.requiresHumanRealMaterial === true &&
- item.directCustomerProof === false &&
- item.canCloseProofGap === false
- ) && (
- complete ||
- workOrders.length >= Math.max(5, Number(intake.failureCount || 0), Number(closure.openCount || 0))
- );
- return {
- generatedAt: new Date().toISOString(),
- root,
- outputsDir,
- passed,
- complete,
- proofLevel: 'not_business_proof',
- materialType: 'real_proof_closure_work_order',
- directCustomerProof: false,
- canCloseProofGap: false,
- requiresHumanRealMaterial: true,
- workOrderCount: workOrders.length,
- ownerGroupCount: ownerGroups.length,
- seedSuggestionCount: workOrders.filter(item => item.seedSuggestion).length,
- targetFiles: Array.from(new Set(workOrders.map(item => item.targetFile).filter(Boolean))),
- ownerGroups,
- sourceState: {
- intakeOverallReady: Boolean(intake.acceptance?.overallReady),
- intakeFailureCount: Number(intake.failureCount || 0),
- videoReadyForAbPreflight: Boolean(video.acceptance?.readyForVideoAbPreflight),
- videoFailureCount: Number(video.failureCount || 0),
- realCandidateRows: Number(video.counts?.realCandidateRows ?? intake.video?.counts?.realCandidateRows ?? 0),
- proofGapComplete: Boolean(closure.complete),
- proofGapOpenCount,
- proofGapClosedCount: Number(closure.closedCount || 0),
- optimizationReadyForClaim: Boolean(completion.readyForClaim),
- optimizationBlockingReasonCount: Number(completion.blockingReasons?.length || completion.blockingReasonCount || 0),
- localSeedCanCloseProofGap: Boolean(seed.canCloseProofGap),
- localSeedDirectCustomerProof: Boolean(seed.directCustomerProof),
- localSeedHistoryDraftRows: countRows(seed.historyDraftRows, seed.counts?.historyDraftRows),
- localSeedCandidateSeedRows: countRows(seed.candidateSeedRows, seed.counts?.candidateSeedRows),
- localSeedVideoWorklistRows: countRows(seed.videoWorklistRows, seed.counts?.videoWorklistRows)
- },
- files: {
- summary: 'outputs/real-proof-closure-work-order-latest/real-proof-closure-work-order-summary.json',
- report: 'outputs/real-proof-closure-work-order-latest/real-proof-closure-work-order.md',
- csv: 'outputs/real-proof-closure-work-order-latest/real-proof-closure-work-order.csv',
- byOwnerDir: 'outputs/real-proof-closure-work-order-latest/by-owner'
- },
- sourceFiles: Object.fromEntries(Object.entries(sources).map(([key, value]) => [key, value.path ? rel(root, value.path) : ''])),
- guardrails: [
- '本工单只把未关闭证明缺口拆成可填写字段、行范围和复验命令,不写入正式 intake 模板。',
- '本工单不证明客户效果,也不证明提号率提升或人工补号量下降。',
- '本地种子和 PoC 候选只能作为转写线索;客户选择、拒绝原因、历史人工补号量和本轮人工补号量必须由商务补真实记录。',
- '真实候选视频必须由投放补视频 URL、封面、ASR/字幕、帧图或正文证据;参考视频不能替代候选视频。',
- 'proof-gap.openCount 不为 0 或 optimization-completion.readyForClaim=false 时,不得宣称提号率提升、客户效果达标或人工补号量下降。',
- '不得在工单、CSV、报告或命令中写入 sessionToken、鉴权头、模型 token 或 npm token。'
- ],
- workOrders
- };
- }
- function workOrdersFromIntake(intake, seed) {
- const issues = Array.isArray(intake.issues) ? intake.issues : [];
- return issues.map((issue, index) => {
- const spec = intakeSpec(issue, seed);
- return makeWorkOrder({
- id: `intake-${issue.section || 'unknown'}-${issue.type || index + 1}`,
- priority: spec.priority || index + 1,
- owner: spec.owner,
- source: 'intake-readiness',
- gapId: spec.gapId,
- section: issue.section || spec.section,
- issueType: issue.type || '',
- title: spec.title,
- targetFile: spec.targetFile,
- targetFields: spec.targetFields,
- rowSelector: spec.rowSelector,
- currentEvidence: issue.message || spec.currentEvidence,
- missingProofRequirements: spec.missingProofRequirements,
- seedSuggestion: spec.seedSuggestion,
- requiredAction: spec.requiredAction,
- acceptanceCommand: spec.acceptanceCommand,
- acceptance: spec.acceptance
- });
- });
- }
- function workOrdersFromVideoReadiness(video, seed) {
- const issues = Array.isArray(video.issues) ? video.issues : [];
- return issues.map((issue, index) => {
- const spec = videoSpec(issue, seed);
- return makeWorkOrder({
- id: `video-${issue.type || index + 1}`,
- priority: spec.priority || 30 + index,
- owner: spec.owner,
- source: 'video-resource-readiness',
- gapId: 'video-real-candidate',
- section: 'video',
- issueType: issue.type || '',
- title: spec.title,
- targetFile: 'outputs/video-intake-pack-latest/video-resource-template.csv',
- targetFields: spec.targetFields,
- rowSelector: spec.rowSelector,
- currentEvidence: issue.message || '',
- missingProofRequirements: [
- 'acceptance.readyForVideoAbPreflight=true',
- 'failureCount=0',
- 'counts.realCandidateRows>=1'
- ],
- seedSuggestion: videoSeedSuggestion(seed),
- requiredAction: spec.requiredAction,
- acceptanceCommand: 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict',
- acceptance: 'readyForVideoAbPreflight=true、failureCount=0、realCandidateRows>=1。'
- });
- });
- }
- function workOrdersFromProofGapClosure(closure, nextActions, seed) {
- const rows = Array.isArray(closure.rows) ? closure.rows : [];
- return rows
- .filter(row => row.status === 'open')
- .map((row, index) => {
- const spec = gapSpec(row, nextActions, seed);
- return makeWorkOrder({
- id: `gap-${row.id || index + 1}`,
- priority: spec.priority,
- owner: spec.owner,
- source: 'proof-gap-closure',
- gapId: row.id || '',
- section: spec.section,
- issueType: 'open-proof-gap',
- title: spec.title,
- targetFile: spec.targetFile,
- targetFields: spec.targetFields,
- rowSelector: spec.rowSelector,
- currentEvidence: row.evidence || '',
- missingProofRequirements: normalizeList(row.missingProofRequirements),
- seedSuggestion: spec.seedSuggestion,
- requiredAction: spec.requiredAction || row.next || '',
- acceptanceCommand: row.command || spec.acceptanceCommand,
- acceptance: spec.acceptance || row.required || ''
- });
- });
- }
- function workOrdersFromOptimizationCompletion(completion) {
- const reasons = Array.isArray(completion.blockingReasons) ? completion.blockingReasons : [];
- return reasons
- .filter(reason => ['optimization-pipeline', 'handoff-summary', 'business-proof-progress', 'longrun-readiness'].includes(reason.id))
- .map((reason, index) => makeWorkOrder({
- id: `completion-${reason.id || index + 1}`,
- priority: 80 + index,
- owner: '技术/AI',
- source: 'optimization-completion',
- gapId: reason.id || '',
- section: 'audit',
- issueType: 'blocked-gate',
- title: `复验完成门槛:${reason.title || reason.id}`,
- targetFile: 'outputs/optimization-completion-latest/optimization-completion-summary.json',
- targetFields: ['complete', 'readyForClaim', 'proofGapOpenCount', 'blockingReasons'],
- rowSelector: reason.id || '',
- currentEvidence: reason.evidence || '',
- missingProofRequirements: normalizeList(reason.missingProofRequirements),
- requiredAction: '真实材料补齐并通过对应审计后,刷新 optimization:completion 和 round:refresh;不要用 smoke 或旧 pipeline 结果替代。',
- acceptanceCommand: reason.command || 'npm run optimization:completion -- --output outputs\\optimization-completion-latest',
- acceptance: reason.required || 'complete=true、readyForClaim=true、proofGapOpenCount=0。'
- }));
- }
- function intakeSpec(issue, seed) {
- const section = issue.section || '';
- const type = issue.type || '';
- if (section === 'history') {
- const base = {
- owner: '商务',
- section,
- gapId: 'historical-dataset',
- targetFile: 'outputs/data-intake-pack-latest/history-data-template.csv',
- acceptanceCommand: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest',
- acceptance: 'historyReady=true、真实历史 Brief 数 >=5、无模板占位;随后 history:audit readyForCustomerEffectProof=true。',
- seedSuggestion: historySeedSuggestion(seed),
- missingProofRequirements: ['acceptance.historyReady=true', 'history:audit readyForCustomerEffectProof=true']
- };
- const specs = {
- 'min-briefs': {
- priority: 1,
- title: '补足真实历史 Brief 数量',
- targetFields: ['brief编号', '项目名称', '类目', '客户原始Brief'],
- rowSelector: '新增至少 4 行真实历史 Brief,使 briefCount>=5。',
- requiredAction: '补 5-10 个真实历史 Brief;DHA seed 只能作草稿,不足以关闭历史数据缺口。'
- },
- placeholder: {
- priority: 2,
- title: '替换历史数据模板占位内容',
- targetFields: ['整行'],
- rowSelector: '所有包含 example.com、示例、候选、待补的行。',
- requiredAction: '把示例项目、example.com 和待补文字替换为真实客户材料;不能用未知、0 或占位值凑数。'
- },
- reference: {
- priority: 3,
- title: '补真实参考账号或参考视频',
- targetFields: ['参考账号或视频'],
- rowSelector: 'history-data-template.csv 每个 Brief 行。',
- requiredAction: '填真实参考账号主页或参考视频链接;可参考本地 reference seed,但需人工确认可访问且属于真实业务材料。',
- seedSuggestion: referenceSeedSuggestion(seed)
- },
- 'manual-list': {
- priority: 4,
- title: '补人工最终名单博主和主页',
- targetFields: ['博主名称', '主页链接'],
- rowSelector: 'history-data-template.csv 每个 Brief 行。',
- requiredAction: '填历史人工最终提报名单中的真实博主名称和可访问主页链接;PoC 候选只能作为找回线索。'
- },
- 'manual-baseline': {
- priority: 5,
- title: '补历史人工补号量基线',
- targetFields: ['历史人工补号量基线'],
- rowSelector: 'history-data-template.csv 每个 Brief 行。',
- requiredAction: '填写每个历史 Brief 当时人工补号量基线;缺该字段不能证明人工补号量下降。'
- },
- 'customer-decision': {
- priority: 6,
- title: '补历史客户最终选择',
- targetFields: ['客户选择', '拒绝原因'],
- rowSelector: 'history-data-template.csv 每个 Brief 行。',
- requiredAction: '填写客户最终选中/拒绝;拒绝样本必须有拒绝原因。'
- }
- };
- return { ...base, ...(specs[type] || specs.placeholder) };
- }
- if (section === 'review') {
- return {
- priority: type === 'customer-decision' ? 12 : 11,
- owner: '商务',
- section,
- gapId: 'manual-review-and-customer-effect',
- title: type === 'customer-decision' ? '补客户最终选中/拒绝记录' : '替换人工复核模板占位内容',
- targetFile: 'outputs/data-intake-pack-latest/manual-review-template.csv',
- targetFields: type === 'customer-decision'
- ? ['客户选择', '归因类型', '反馈原因', '本轮人工补号量']
- : ['整行', '博主名称', '主页链接', '人工复核标签'],
- rowSelector: type === 'customer-decision'
- ? 'manual-review-template.csv 至少一批真实候选行。'
- : '所有包含 example.com、示例、候选、待客户反馈的行。',
- seedSuggestion: candidateSeedSuggestion(seed),
- requiredAction: type === 'customer-decision'
- ? '补客户最终选中/拒绝、负样本归因、反馈原因和本轮人工补号量。'
- : '把 PoC 候选转成真实人工复核记录,补主页链接和人工复核标签。',
- missingProofRequirements: ['review:metrics overallPass=true', 'customer-effect:audit overallPass=true'],
- acceptanceCommand: 'npm run intake:readiness -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --output outputs\\intake-readiness-latest',
- acceptance: 'reviewMetricsReady=true、customerEffectReady=true;随后 customer-effect:audit overallPass=true。'
- };
- }
- return {
- priority: 20,
- owner: '商务/投放',
- section: 'video',
- gapId: 'video-real-candidate',
- title: '补视频资源真实材料',
- targetFile: 'outputs/video-intake-pack-latest/video-resource-template.csv',
- targetFields: ['资源角色', '是否真实资源', '视频链接', '封面链接', '字幕或ASR文本', '帧图链接', '内容摘要'],
- rowSelector: 'video-resource-template.csv 参考视频和候选视频行。',
- seedSuggestion: videoSeedSuggestion(seed),
- requiredAction: '替换视频模板占位内容,补真实参考视频和真实候选视频。',
- missingProofRequirements: ['acceptance.videoReady=true', 'video:resource-readiness readyForVideoAbPreflight=true'],
- acceptanceCommand: 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict',
- acceptance: 'readyForVideoAbPreflight=true、failureCount=0。'
- };
- }
- function videoSpec(issue, seed) {
- if (issue.type === 'missing-real-candidate' || issue.type === 'real-candidate') {
- return {
- priority: 21,
- owner: '商务/投放',
- title: '补真实候选视频资源',
- targetFields: ['资源角色', '博主名称', '主页链接', '视频链接', '封面链接', '字幕或ASR文本', '帧图链接', '内容摘要', '是否真实资源'],
- rowSelector: '新增或更新至少 1 行资源角色=候选视频、是否真实资源=是。',
- requiredAction: '为候选账号补真实视频 URL 和至少一种封面/ASR/帧图/正文证据;参考视频不能替代候选视频。'
- };
- }
- if (issue.type === 'missing-video-url' || issue.type === 'video-url') {
- return {
- priority: 22,
- owner: '商务/投放',
- title: '补真实视频 URL',
- targetFields: ['视频链接'],
- rowSelector: '所有标为真实资源的参考视频和候选视频行。',
- requiredAction: '填真实可访问视频 URL;不能只用封面、标题、接口 200 或 404 链接。'
- };
- }
- return {
- priority: 23,
- owner: '商务/投放',
- title: '补视频资源证据字段',
- targetFields: ['封面链接', '字幕或ASR文本', '帧图链接', '标题', '内容摘要', '风格调性标签', '画面人设场景信号'],
- rowSelector: 'video-resource-template.csv 真实视频资源行。',
- requiredAction: '补足封面、ASR/字幕、帧图、标题或正文证据,避免把空资源标成真实。'
- };
- }
- function gapSpec(row, nextActions, seed) {
- const id = row.id || '';
- const action = findNextActionForGap(nextActions, id);
- const common = {
- priority: 40,
- owner: ownerForGap(id),
- title: row.title || id,
- targetFile: 'outputs/proof-gap-closure-latest/proof-gap-closure-summary.json',
- targetFields: ['status', 'missingProofRequirements'],
- rowSelector: id,
- seedSuggestion: '',
- requiredAction: action?.next || row.next || '',
- acceptanceCommand: row.command || '',
- acceptance: row.required || ''
- };
- const specs = {
- 'historical-dataset': {
- priority: 31,
- section: 'history',
- targetFile: 'outputs/data-intake-pack-latest/history-data-template.csv',
- targetFields: ['客户原始Brief', '历史人工补号量基线', '参考账号或视频', '博主名称', '主页链接', '客户选择', '拒绝原因'],
- rowSelector: '至少 5 个真实历史 Brief 行。',
- seedSuggestion: historySeedSuggestion(seed),
- requiredAction: '补 5-10 个真实 Brief、人工名单、客户选择、拒绝原因和历史人工补号量基线。',
- acceptanceCommand: 'npm run history:from-csv -- --input <历史数据CSV> --output <history-dataset目录> && npm run history:audit -- --input <history-dataset目录> --output <历史审计输出目录> --strict',
- acceptance: 'historical-dataset-audit readyForCustomerEffectProof=true。'
- },
- 'video-real-candidate': {
- priority: 32,
- section: 'video',
- targetFile: 'outputs/video-intake-pack-latest/video-resource-template.csv',
- targetFields: ['资源角色', '博主名称', '主页链接', '视频链接', '封面链接', '字幕或ASR文本', '帧图链接', '是否真实资源'],
- rowSelector: '至少 1 行真实候选视频。',
- seedSuggestion: videoSeedSuggestion(seed),
- requiredAction: '补真实候选视频 URL、封面、ASR/字幕、帧图或正文证据。',
- acceptanceCommand: 'npm run video:resource-readiness -- --input outputs\\video-intake-pack-latest\\video-resource-template.csv --output outputs\\video-resource-readiness-latest --strict',
- acceptance: 'readyForVideoAbPreflight=true、failureCount=0、realCandidateRows>=1。'
- },
- 'manual-review-and-customer-effect': {
- priority: 33,
- section: 'review',
- targetFile: 'outputs/data-intake-pack-latest/manual-review-template.csv',
- targetFields: ['人工复核标签', '客户选择', '归因类型', '反馈原因', '本轮人工补号量'],
- rowSelector: '真实人工复核候选行。',
- seedSuggestion: candidateSeedSuggestion(seed),
- requiredAction: '补商务复核标签、负样本归因、客户选择和本轮人工补号量。',
- acceptanceCommand: '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',
- acceptance: 'review:metrics overallPass=true、customer-effect:audit overallPass=true。'
- },
- 'video-ab-live-proof': {
- priority: 60,
- section: 'video-ab',
- targetFile: 'outputs/video-intake-pack-latest/video-resource-template.csv',
- targetFields: ['proofContext.mode', 'proofContext.collectionMode', 'requiresVocSocialProvider', 'requiresVideoAnalysisProvider'],
- rowSelector: '真实视频 A/B 输出目录。',
- requiredAction: '真实视频资源和真实 provider 就绪后运行 acceptance:video-ab,不得使用 sample 或 unknown proofContext。',
- acceptanceCommand: 'npm run acceptance:video-ab',
- acceptance: 'proofContext.mode=live、collectionMode=live、generatedBy=acceptance:video-ab。'
- },
- 'live-provider-overnight-proof': {
- priority: 61,
- section: 'longrun',
- targetFile: '<overnight输出目录>/aggregate-summary.json',
- targetFields: ['manifest.liveEnabled', 'acceptance.overallPass', 'failureCount', 'failedGateCount'],
- rowSelector: '真实 live/provider overnight aggregate。',
- requiredAction: 'longrun:readiness ready=true 后再跑真实 provider overnight:quality。',
- acceptanceCommand: 'npm run longrun:readiness -- --mode full-matrix --intake-readiness outputs\\intake-readiness-latest\\intake-readiness-summary.json --proof-gap outputs\\proof-gap-request-latest\\proof-gap-request-summary.json --proof-gap-closure outputs\\proof-gap-closure-latest\\proof-gap-closure-summary.json --output outputs\\long-run-readiness-latest',
- acceptance: 'longrun ready=true;overnight aggregate liveEnabled=true、overallPass=true、failureCount=0。'
- }
- };
- return { ...common, ...(specs[id] || {}) };
- }
- function makeWorkOrder(item) {
- return {
- id: item.id,
- priority: Number(item.priority || 99),
- owner: item.owner || '待分配',
- source: item.source || '',
- gapId: item.gapId || '',
- section: item.section || '',
- issueType: item.issueType || '',
- title: item.title || item.id,
- targetFile: item.targetFile || '',
- targetFields: normalizeList(item.targetFields),
- rowSelector: item.rowSelector || '',
- currentEvidence: item.currentEvidence || '',
- missingProofRequirements: normalizeList(item.missingProofRequirements),
- seedSuggestion: item.seedSuggestion || '',
- requiredAction: item.requiredAction || '',
- acceptanceCommand: item.acceptanceCommand || '',
- acceptance: item.acceptance || '',
- requiresHumanRealMaterial: true,
- directCustomerProof: false,
- canCloseProofGap: false,
- proofLevel: 'not_business_proof',
- boundary: '本工单只指导补真实材料和复验,不证明客户效果。'
- };
- }
- function historySeedSuggestion(seed) {
- const row = firstRow(seed.historyDraftRows);
- if (!row) return '';
- return `可参考本地 history 草稿 ${row.briefId || ''}:${truncate(row.customerBrief || row.projectName || '', 90)};仍缺 ${normalizeList(row.missingForProof).join(';')}`;
- }
- function referenceSeedSuggestion(seed) {
- const refs = normalizeRows(seed.referenceSeedRows).filter(row => row.finalLink && !/\/404\b/.test(row.finalLink));
- if (!refs.length) return historySeedSuggestion(seed);
- return refs.slice(0, 3).map(row => `${row.authorName || row.referenceType || '参考'} ${row.finalLink}`).join(';');
- }
- function candidateSeedSuggestion(seed) {
- const rows = normalizeRows(seed.candidateSeedRows).slice(0, 5);
- if (!rows.length) return '';
- return rows.map(row => `${row.creatorName || '候选'}(${row.xhsId || '缺 xhsId'},${row.suggestedFormat || '形式待定'})`).join(';');
- }
- function videoSeedSuggestion(seed) {
- const rows = normalizeRows(seed.videoWorklistRows);
- const candidates = rows.filter(row => row.role === '候选视频');
- const picked = (candidates.length ? candidates : rows).slice(0, 4);
- if (!picked.length) return candidateSeedSuggestion(seed);
- return picked.map(row => `${row.role || '视频'}:${row.creatorName || '缺博主'},${row.resourceStatus || '状态待复核'},${row.videoLink || '缺视频 URL'}`).join(';');
- }
- function findNextActionForGap(nextActions, gapId) {
- const actions = Array.isArray(nextActions.actions) ? nextActions.actions : [];
- return actions.find(action => String(action.id || '').includes(gapId) || String(action.gapId || '') === gapId) || null;
- }
- function ownerForGap(id) {
- if (id === 'historical-dataset' || id === 'manual-review-and-customer-effect') return '商务';
- if (id === 'video-real-candidate') return '商务/投放';
- return '技术/AI';
- }
- function dedupeWorkOrders(rows) {
- const seen = new Map();
- for (const row of rows.filter(Boolean)) {
- const key = `${row.source}:${row.id}`;
- if (!seen.has(key)) {
- seen.set(key, row);
- continue;
- }
- const existing = seen.get(key);
- existing.currentEvidence = joinUnique([existing.currentEvidence, row.currentEvidence]);
- existing.missingProofRequirements = Array.from(new Set([...existing.missingProofRequirements, ...row.missingProofRequirements]));
- }
- return [...seen.values()];
- }
- function groupByOwner(rows) {
- const map = new Map();
- for (const row of rows) {
- if (!map.has(row.owner)) map.set(row.owner, []);
- map.get(row.owner).push(row);
- }
- return [...map.entries()].map(([owner, ownerRows]) => ({
- owner,
- workOrderCount: ownerRows.length,
- topPriority: Math.min(...ownerRows.map(row => row.priority)),
- targetFiles: Array.from(new Set(ownerRows.map(row => row.targetFile).filter(Boolean))),
- gapIds: Array.from(new Set(ownerRows.map(row => row.gapId).filter(Boolean))),
- missingProofRequirementCount: ownerRows.reduce((total, row) => total + row.missingProofRequirements.length, 0)
- }));
- }
- function writeOwnerArtifacts(outputDir, rows) {
- const byOwnerDir = path.join(outputDir, 'by-owner');
- fs.mkdirSync(byOwnerDir, { recursive: true });
- return groupByOwner(rows).map(group => {
- const ownerRows = rows.filter(row => row.owner === group.owner);
- const slug = ownerSlug(group.owner);
- const markdownPath = path.join(byOwnerDir, `${slug}.md`);
- const csvPath = path.join(byOwnerDir, `${slug}.csv`);
- fs.writeFileSync(markdownPath, withBom(renderOwnerReport(group.owner, ownerRows)), 'utf8');
- fs.writeFileSync(csvPath, withBom(renderCsv(ownerRows)), 'utf8');
- return {
- owner: group.owner,
- workOrderCount: ownerRows.length,
- topPriority: group.topPriority,
- markdown: path.join('outputs', 'real-proof-closure-work-order-latest', 'by-owner', `${slug}.md`).replace(/\\/g, '/'),
- csv: path.join('outputs', 'real-proof-closure-work-order-latest', 'by-owner', `${slug}.csv`).replace(/\\/g, '/'),
- targetFiles: group.targetFiles,
- gapIds: group.gapIds
- };
- });
- }
- function renderReport(summary) {
- const lines = [
- '# 真实证明闭环工单',
- '',
- `- 生成时间:${summary.generatedAt}`,
- `- passed:${summary.passed}`,
- `- complete:${summary.complete}`,
- `- proofLevel:${summary.proofLevel}`,
- `- workOrderCount:${summary.workOrderCount}`,
- `- proofGapOpenCount:${summary.sourceState.proofGapOpenCount}`,
- `- intakeFailureCount:${summary.sourceState.intakeFailureCount}`,
- `- videoFailureCount:${summary.sourceState.videoFailureCount}`,
- `- realCandidateRows:${summary.sourceState.realCandidateRows}`,
- `- canCloseProofGap:${summary.canCloseProofGap}`,
- '',
- '## 负责人汇总',
- '',
- '| 负责人 | 工单数 | 最高优先级 | 目标文件 | 缺口 |',
- '| --- | ---: | ---: | --- | --- |',
- ...summary.ownerGroups.map(group => tableRow([
- group.owner,
- group.workOrderCount,
- group.topPriority,
- group.targetFiles.join(';'),
- group.gapIds.join(';')
- ])),
- '',
- '## 负责人附件',
- '',
- '| 负责人 | 工单数 | Markdown | CSV |',
- '| --- | ---: | --- | --- |',
- ...(summary.ownerArtifacts || []).map(item => tableRow([item.owner, item.workOrderCount, item.markdown, item.csv])),
- '',
- '## 工单明细',
- '',
- '| 优先级 | 负责人 | 缺口 | 标题 | 目标文件 | 字段 | 行范围 | 当前证据 | 缺失证明要求 | 种子线索 | 补齐动作 | 复验命令 | 验收 |',
- '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
- ...summary.workOrders.map(row => tableRow([
- row.priority,
- row.owner,
- row.gapId,
- row.title,
- row.targetFile,
- row.targetFields.join(';'),
- row.rowSelector,
- row.currentEvidence,
- row.missingProofRequirements.join(';'),
- row.seedSuggestion,
- row.requiredAction,
- row.acceptanceCommand,
- row.acceptance
- ])),
- '',
- '## 源文件',
- '',
- ...Object.entries(summary.sourceFiles).map(([key, value]) => `- ${key}:${value || '缺失'}`),
- '',
- '## 边界',
- '',
- ...summary.guardrails.map(item => `- ${item}`)
- ];
- return lines.join('\n');
- }
- function renderOwnerReport(owner, rows) {
- return [
- `# 真实证明闭环工单 - ${owner}`,
- '',
- `- 工单数:${rows.length}`,
- '- 边界:本附件只指导补真实材料和复验,不证明客户效果。',
- '',
- '| 优先级 | 缺口 | 标题 | 目标文件 | 字段 | 当前证据 | 缺失证明要求 | 补齐动作 | 复验命令 | 验收 |',
- '| ---: | --- | --- | --- | --- | --- | --- | --- | --- | --- |',
- ...rows.map(row => tableRow([
- row.priority,
- row.gapId,
- row.title,
- row.targetFile,
- row.targetFields.join(';'),
- row.currentEvidence,
- row.missingProofRequirements.join(';'),
- row.requiredAction,
- row.acceptanceCommand,
- row.acceptance
- ]))
- ].join('\n');
- }
- function renderCsv(rows) {
- const header = [
- 'id',
- '优先级',
- '负责人',
- '来源',
- '缺口ID',
- '区域',
- '问题类型',
- '标题',
- '目标文件',
- '目标字段',
- '行范围',
- '当前证据',
- '缺失证明要求',
- '种子线索',
- '补齐动作',
- '复验命令',
- '验收',
- '需要人工真实材料',
- '直接客户证明',
- '可关闭proof-gap',
- '证明等级',
- '边界'
- ];
- return [
- header.join(','),
- ...rows.map(row => [
- row.id,
- row.priority,
- row.owner,
- row.source,
- row.gapId,
- row.section,
- row.issueType,
- row.title,
- row.targetFile,
- row.targetFields.join(';'),
- row.rowSelector,
- row.currentEvidence,
- row.missingProofRequirements.join(';'),
- row.seedSuggestion,
- row.requiredAction,
- row.acceptanceCommand,
- row.acceptance,
- row.requiresHumanRealMaterial,
- row.directCustomerProof,
- row.canCloseProofGap,
- row.proofLevel,
- row.boundary
- ].map(csvCell).join(','))
- ].join('\n');
- }
- function latestSummary(outputsDir, fileName, fallbackDir) {
- const candidates = [];
- const fallbackPath = path.join(outputsDir, fallbackDir, fileName);
- if (fs.existsSync(fallbackPath)) candidates.push(fallbackPath);
- for (const dir of listDirs(outputsDir)) {
- const file = path.join(dir, fileName);
- if (fs.existsSync(file)) candidates.push(file);
- }
- const picked = candidates.sort((a, b) => mtimeMs(b) - mtimeMs(a))[0] || fallbackPath;
- return { path: picked, json: readJson(picked) };
- }
- function listDirs(dir) {
- if (!fs.existsSync(dir)) return [];
- return fs.readdirSync(dir, { withFileTypes: true })
- .filter(item => item.isDirectory())
- .map(item => path.join(dir, item.name));
- }
- function readJson(file) {
- try {
- return JSON.parse(fs.readFileSync(file, 'utf8'));
- } catch {
- return null;
- }
- }
- function firstRow(value) {
- const rows = normalizeRows(value);
- return rows[0] || null;
- }
- function normalizeRows(value) {
- return Array.isArray(value) ? value : [];
- }
- function normalizeList(value) {
- if (Array.isArray(value)) return value.map(item => String(item || '').trim()).filter(Boolean);
- if (!value) return [];
- return [String(value).trim()].filter(Boolean);
- }
- function countRows(rows, fallback = 0) {
- if (Array.isArray(rows)) return rows.length;
- return Number(fallback || 0);
- }
- function joinUnique(values) {
- return Array.from(new Set(values.filter(Boolean))).join(';');
- }
- function truncate(value, maxLength) {
- const text = String(value || '');
- return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
- }
- function tableRow(cells) {
- return `| ${cells.map(escapeCell).join(' | ')} |`;
- }
- function escapeCell(value) {
- return String(value ?? '').replace(/\r?\n/g, '<br>').replace(/\|/g, '\\|');
- }
- function csvCell(value) {
- const text = String(value ?? '').replace(/\r?\n/g, ' ');
- if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
- return text;
- }
- function withBom(text) {
- return `\uFEFF${text}`;
- }
- function ownerSlug(owner) {
- if (owner === '商务') return 'business';
- if (owner === '商务/投放') return 'business-media';
- if (owner === '技术/AI') return 'tech-ai';
- return String(owner || 'unknown').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'unknown';
- }
- function rel(root, file) {
- return path.relative(root, file).replace(/\\/g, '/');
- }
- function mtimeMs(file) {
- try {
- return fs.statSync(file).mtimeMs;
- } catch {
- return 0;
- }
- }
- function parseArgs(argv) {
- const result = {};
- for (let index = 0; index < argv.length; index += 1) {
- const arg = argv[index];
- if (!arg.startsWith('--')) continue;
- const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
- const next = argv[index + 1];
- if (!next || next.startsWith('--')) result[key] = true;
- else {
- result[key] = next;
- index += 1;
- }
- }
- return result;
- }
- if (require.main === module) main();
- module.exports = {
- buildRealProofClosureWorkOrder
- };
|