#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const root = path.resolve(__dirname, '..'); const currentSpecFiles = [ 'docs/tihao-experience-optimization-plan.md', 'docs/software-client-table-format.md', 'docs/ai-optimization-task-software-table.md', 'docs/ai-optimization-job-dedup-software-format.md', 'docs/ai-optimization-proof-gap-software-table.md', 'docs/business-proof-action-order.md', 'skills/tihao/references/output-format.md' ]; const softwareTableHeader = [ 'brief编号', '策略', '排名', '平台', '博主名称', '综合分', 'brief匹配分', '参考风格分', '主页证据分', '视觉质感分', '调性一致分', '证据加分', '证据风险扣分', '推荐理由', '风险提示', '主页链接', '人工复核标签' ]; const requiredPhrases = [ { file: 'docs/tihao-experience-optimization-plan.md', phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称'] }, { file: 'docs/software-client-table-format.md', phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称'] }, { file: 'docs/ai-optimization-task-software-table.md', phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称'] }, { file: 'docs/ai-optimization-job-dedup-software-format.md', phrases: [ '固定表头', '全局博主唯一', '软件端重复键为 0', 'sample/smoke/provider fallback 不算业务证明', '客户效果不可宣称' ] }, { file: 'docs/ai-optimization-proof-gap-software-table.md', phrases: [ '软件端格式补证表', '同一张历史数据表相关的缺口只保留一行', '同一张视频资源表相关的缺口只保留一行', 'sample、smoke、接口 200', '真实视频 A/B', '真实 live/provider 长跑', '商务复核和客户效果' ] }, { file: 'docs/business-proof-action-order.md', phrases: [ '15 步', 'npm run intake:readiness', 'npm run longrun:readiness', 'npm run customer-effect:audit', 'npm run acceptance:video-ab', '软件端重复键为 0', '客户效果不可宣称', 'sample、smoke', 'proof-gap:closure' ] }, { file: 'skills/tihao/references/output-format.md', phrases: ['全局博主唯一', '平台 + 主页链接', '平台 + 博主名称'] } ]; const forbiddenCurrentSpecPhrases = [ 'brief编号 + 平台 + 规范化主页链接', 'brief编号 + 平台 + 规范化博主名称', 'brief编号 + 平台 + 主页链接', 'brief编号 + 平台 + 博主名称' ]; function main() { const hits = []; for (const spec of requiredPhrases) { const text = read(spec.file); if (spec.file === 'docs/tihao-experience-optimization-plan.md') { for (const phrase of ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称', '不得宣称客户效果完成']) { if (!text.includes(phrase)) { hits.push({ file: spec.file, type: 'missing-required-phrase', phrase }); } } continue; } for (const phrase of spec.phrases) { if (!text.includes(phrase)) { hits.push({ file: spec.file, type: 'missing-required-phrase', phrase }); } } } for (const file of currentSpecFiles) { const text = read(file); for (const phrase of forbiddenCurrentSpecPhrases) { if (text.includes(phrase)) { hits.push({ file, type: 'legacy-dedupe-phrase', phrase }); } } } checkDedupJobTable(hits); checkProofGapSoftwareTable(hits); checkLatestProofGapSoftwareForm(hits); checkBusinessProofActionOrder(hits); const packageJson = JSON.parse(read('package.json')); const acceptance = read('scripts/acceptance.js'); if (!packageJson.scripts['plan:consistency:smoke']) { hits.push({ file: 'package.json', type: 'missing-script', phrase: 'plan:consistency:smoke' }); } if (!acceptance.includes("plan:consistency:smoke")) { hits.push({ file: 'scripts/acceptance.js', type: 'missing-acceptance-gate', phrase: 'plan:consistency:smoke' }); } const summary = { ok: hits.length === 0, scannedFiles: currentSpecFiles.length, hits }; console.log(JSON.stringify(summary, null, 2)); if (hits.length) process.exitCode = 1; } function checkProofGapSoftwareTable(hits) { const file = 'docs/ai-optimization-proof-gap-software-table.md'; const text = read(file); const lines = text.split(/\r?\n/); const headerLine = lines.find((line) => { if (!line.startsWith('| brief编号 |')) return false; const cells = splitMarkdownRow(line); return JSON.stringify(cells) === JSON.stringify(softwareTableHeader); }); if (!headerLine) { hits.push({ file, type: 'missing-proof-gap-software-header', phrase: 'brief编号' }); } const rows = lines .map(splitMarkdownRow) .filter((cells) => /^GAP-\d{3}$/.test(cells[0] || '')); const ids = rows.map((cells) => cells[0]); const uniqueIds = new Set(ids); if (rows.length !== 5) { hits.push({ file, type: 'unexpected-proof-gap-software-row-count', phrase: `count=${rows.length}` }); } if (uniqueIds.size !== ids.length) { hits.push({ file, type: 'duplicate-proof-gap-software-id', phrase: ids.join(',') }); } const requiredRows = [ ['GAP-001', '真实历史 Brief 数据集', '商务数据'], ['GAP-002', '真实视频资源表', '商务/投放'], ['GAP-003', '真实视频证据 A/B', '技术/AI'], ['GAP-004', '真实 live/provider 长跑证明', '技术/AI'], ['GAP-005', '商务复核和客户效果', '商务数据'] ]; for (const [id, title, owner] of requiredRows) { const row = rows.find((cells) => cells[0] === id); if (!row) { hits.push({ file, type: 'missing-proof-gap-software-row', phrase: id }); continue; } if (row[3] !== owner || row[4] !== title) { hits.push({ file, type: 'proof-gap-software-row-mismatch', phrase: `${id}:${row[3]}/${row[4]}` }); } } checkProofGapSoftwareMatchesNextActions(hits, rows); } function checkProofGapSoftwareMatchesNextActions(hits, proofGapRows) { const file = 'outputs/business-proof-next-actions-latest/business-proof-next-actions-summary.json'; const fullPath = path.join(root, file); if (!fs.existsSync(fullPath)) { hits.push({ file, type: 'missing-next-actions-latest', phrase: file }); return; } const summary = JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, '')); const actions = Array.isArray(summary.actions) ? summary.actions : []; const closure = readLatestProofGapClosure(); const expectedBusinessGapCount = Number(closure?.businessGapCount || proofGapRows.length); if (proofGapRows.length !== expectedBusinessGapCount) { hits.push({ file, type: 'proof-gap-software-business-row-count-mismatch', phrase: `${expectedBusinessGapCount}/${proofGapRows.length}` }); } const openBusinessGaps = currentBusinessProofGapRows(closure, 'open'); const closedBusinessGaps = currentBusinessProofGapRows(closure, 'closed'); const expectedActionCount = openBusinessGaps ? openBusinessGaps.length : proofGapRows.length; if (actions.length !== expectedActionCount) { hits.push({ file, type: 'proof-gap-software-next-actions-count-mismatch', phrase: `${expectedActionCount}/${actions.length}` }); } const actionIds = new Set(actions.map((action) => action.id)); if (openBusinessGaps) { for (const row of openBusinessGaps) { for (const actionId of actionIdsForProofGap(row.id)) { if (!actionIds.has(actionId)) { hits.push({ file, type: 'missing-next-action-for-open-proof-gap', phrase: `${row.id}:${actionId}` }); } } } for (const row of closedBusinessGaps) { for (const actionId of actionIdsForProofGap(row.id)) { if (actionIds.has(actionId)) { hits.push({ file, type: 'unexpected-next-action-for-closed-proof-gap', phrase: `${row.id}:${actionId}` }); } } } } else { const requiredActionTitles = [ '填真实历史 Brief', '填真实视频资源', '人工复核标注', '真实视频 A/B 验收', '真实 live/provider 长跑证明' ]; const actionTitles = new Set(actions.map((action) => action.title)); for (const title of requiredActionTitles) { if (!actionTitles.has(title)) { hits.push({ file, type: 'missing-next-action-for-proof-gap-software-table', phrase: title }); } } } const forbiddenActionTitles = [ '真实材料预审', '历史数据导入', '历史数据审计', '视频资源就绪审计', '长跑前门禁', '运行策略矩阵', '视频 A/B 验收', '商务复核指标', '客户效果审计' ]; for (const action of actions) { if (forbiddenActionTitles.includes(action.title)) { hits.push({ file, type: 'duplicate-technical-next-action', phrase: action.title }); } } } function readLatestProofGapClosure() { const file = 'outputs/proof-gap-closure-latest/proof-gap-closure-summary.json'; const fullPath = path.join(root, file); if (!fs.existsSync(fullPath)) return null; try { return JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, '')); } catch { return null; } } function currentBusinessProofGapRows(closure, status) { if (!closure || !Array.isArray(closure.rows)) return null; return closure.rows.filter((row) => row && row.id !== 'intake-readiness' && row.status === status); } function actionIdsForProofGap(gapId) { const map = { 'historical-dataset': ['step-03'], 'video-real-candidate': ['step-04'], 'manual-review-and-customer-effect': ['step-12'], 'video-ab-live-proof': ['gap-video-ab-live-proof'], 'live-provider-overnight-proof': ['gap-live-provider-overnight-proof'] }; return map[gapId] || [`gap-${gapId}`]; } function checkLatestProofGapSoftwareForm(hits) { const file = 'outputs/tihao-proof-gap-software-form-latest/tihao-proof-gap-software-form-summary.json'; const fullPath = path.join(root, file); if (!fs.existsSync(fullPath)) { hits.push({ file, type: 'missing-latest-proof-gap-software-form', phrase: file }); return; } const summary = JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, '')); if (summary.passed !== true) { hits.push({ file, type: 'latest-proof-gap-software-form-not-passed', phrase: 'passed=false' }); } if (summary.headerMatches !== true || summary.rowWidthOk !== true) { hits.push({ file, type: 'latest-proof-gap-software-form-header-mismatch', phrase: 'header/width' }); } if (summary.rowCount !== 5) { hits.push({ file, type: 'latest-proof-gap-software-form-row-count', phrase: `count=${summary.rowCount}` }); } if (summary.duplicateIdCount !== 0) { hits.push({ file, type: 'latest-proof-gap-software-form-duplicate-id', phrase: `duplicate=${summary.duplicateIdCount}` }); } } function checkBusinessProofActionOrder(hits) { const file = 'docs/business-proof-action-order.md'; const text = read(file); const lines = text.split(/\r?\n/); const stepRows = lines .map(splitMarkdownRow) .filter((cells) => /^\d+$/.test(cells[0] || '')); if (stepRows.length !== 15) { hits.push({ file, type: 'unexpected-proof-action-step-count', phrase: `count=${stepRows.length}` }); } const expectedCommands = [ 'data:intake-template', 'video:intake-template', 'intake:readiness', 'history:from-csv', 'history:audit', 'video:resource-readiness', 'longrun:readiness', 'optimization:pipeline', 'acceptance:video-ab', 'review:metrics', 'customer-effect:audit', 'evidence:index', 'proof-gap:closure', 'handoff:summary', 'round:deposition' ]; for (const command of expectedCommands) { if (!text.includes(command)) { hits.push({ file, type: 'missing-proof-action-command', phrase: command }); } } const boundaryPhrases = [ 'overallReady=true', 'failureCount=0', 'openCount=0', 'complete=true', 'ready_not_proven', 'blocked_by_external_data' ]; for (const phrase of boundaryPhrases) { if (!text.includes(phrase)) { hits.push({ file, type: 'missing-proof-action-boundary', phrase }); } } } function read(file) { return fs.readFileSync(path.join(root, file), 'utf8').replace(/^\uFEFF/, ''); } function checkDedupJobTable(hits) { const file = 'docs/ai-optimization-job-dedup-software-format.md'; const text = read(file); const lines = text.split(/\r?\n/); const headerLine = lines.find((line) => { if (!line.startsWith('| brief编号 |')) return false; const cells = splitMarkdownRow(line); return JSON.stringify(cells) === JSON.stringify(softwareTableHeader); }); if (!headerLine) { hits.push({ file, type: 'missing-software-header', phrase: 'brief编号' }); } else { const actualHeader = splitMarkdownRow(headerLine); if (JSON.stringify(actualHeader) !== JSON.stringify(softwareTableHeader)) { hits.push({ file, type: 'software-header-mismatch', phrase: actualHeader.join(',') }); } } const ids = []; for (const line of lines) { const cells = splitMarkdownRow(line); const id = cells[0]; if (/^OPT-\d{3}$/.test(id)) ids.push(id); } const uniqueIds = new Set(ids); if (ids.length !== 9) { hits.push({ file, type: 'unexpected-task-count', phrase: `count=${ids.length}` }); } if (uniqueIds.size !== ids.length) { hits.push({ file, type: 'duplicate-task-id', phrase: ids.join(',') }); } const requiredRows = [ 'OPT-001', 'OPT-002', 'OPT-003', 'OPT-004', 'OPT-005', 'OPT-006', 'OPT-007', 'OPT-008', 'OPT-009' ]; for (const id of requiredRows) { if (!uniqueIds.has(id)) hits.push({ file, type: 'missing-task-row', phrase: id }); } } function splitMarkdownRow(line) { if (!line || !line.trim().startsWith('|')) return []; return line .trim() .replace(/^\|/, '') .replace(/\|$/, '') .split('|') .map((cell) => cell.trim()); } if (require.main === module) main();