| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- #!/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();
|