plan-consistency-smoke.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const root = path.resolve(__dirname, '..');
  5. const currentSpecFiles = [
  6. 'docs/tihao-experience-optimization-plan.md',
  7. 'docs/software-client-table-format.md',
  8. 'docs/ai-optimization-task-software-table.md',
  9. 'docs/ai-optimization-job-dedup-software-format.md',
  10. 'docs/ai-optimization-proof-gap-software-table.md',
  11. 'docs/business-proof-action-order.md',
  12. 'skills/tihao/references/output-format.md'
  13. ];
  14. const softwareTableHeader = [
  15. 'brief编号',
  16. '策略',
  17. '排名',
  18. '平台',
  19. '博主名称',
  20. '综合分',
  21. 'brief匹配分',
  22. '参考风格分',
  23. '主页证据分',
  24. '视觉质感分',
  25. '调性一致分',
  26. '证据加分',
  27. '证据风险扣分',
  28. '推荐理由',
  29. '风险提示',
  30. '主页链接',
  31. '人工复核标签'
  32. ];
  33. const requiredPhrases = [
  34. {
  35. file: 'docs/tihao-experience-optimization-plan.md',
  36. phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称']
  37. },
  38. {
  39. file: 'docs/software-client-table-format.md',
  40. phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称']
  41. },
  42. {
  43. file: 'docs/ai-optimization-task-software-table.md',
  44. phrases: ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称']
  45. },
  46. {
  47. file: 'docs/ai-optimization-job-dedup-software-format.md',
  48. phrases: [
  49. '固定表头',
  50. '全局博主唯一',
  51. '软件端重复键为 0',
  52. 'sample/smoke/provider fallback 不算业务证明',
  53. '客户效果不可宣称'
  54. ]
  55. },
  56. {
  57. file: 'docs/ai-optimization-proof-gap-software-table.md',
  58. phrases: [
  59. '软件端格式补证表',
  60. '同一张历史数据表相关的缺口只保留一行',
  61. '同一张视频资源表相关的缺口只保留一行',
  62. 'sample、smoke、接口 200',
  63. '真实视频 A/B',
  64. '真实 live/provider 长跑',
  65. '商务复核和客户效果'
  66. ]
  67. },
  68. {
  69. file: 'docs/business-proof-action-order.md',
  70. phrases: [
  71. '15 步',
  72. 'npm run intake:readiness',
  73. 'npm run longrun:readiness',
  74. 'npm run customer-effect:audit',
  75. 'npm run acceptance:video-ab',
  76. '软件端重复键为 0',
  77. '客户效果不可宣称',
  78. 'sample、smoke',
  79. 'proof-gap:closure'
  80. ]
  81. },
  82. {
  83. file: 'skills/tihao/references/output-format.md',
  84. phrases: ['全局博主唯一', '平台 + 主页链接', '平台 + 博主名称']
  85. }
  86. ];
  87. const forbiddenCurrentSpecPhrases = [
  88. 'brief编号 + 平台 + 规范化主页链接',
  89. 'brief编号 + 平台 + 规范化博主名称',
  90. 'brief编号 + 平台 + 主页链接',
  91. 'brief编号 + 平台 + 博主名称'
  92. ];
  93. function main() {
  94. const hits = [];
  95. for (const spec of requiredPhrases) {
  96. const text = read(spec.file);
  97. if (spec.file === 'docs/tihao-experience-optimization-plan.md') {
  98. for (const phrase of ['全局博主唯一', '平台 + 规范化主页链接', '平台 + 规范化博主名称', '不得宣称客户效果完成']) {
  99. if (!text.includes(phrase)) {
  100. hits.push({ file: spec.file, type: 'missing-required-phrase', phrase });
  101. }
  102. }
  103. continue;
  104. }
  105. for (const phrase of spec.phrases) {
  106. if (!text.includes(phrase)) {
  107. hits.push({ file: spec.file, type: 'missing-required-phrase', phrase });
  108. }
  109. }
  110. }
  111. for (const file of currentSpecFiles) {
  112. const text = read(file);
  113. for (const phrase of forbiddenCurrentSpecPhrases) {
  114. if (text.includes(phrase)) {
  115. hits.push({ file, type: 'legacy-dedupe-phrase', phrase });
  116. }
  117. }
  118. }
  119. checkDedupJobTable(hits);
  120. checkProofGapSoftwareTable(hits);
  121. checkLatestProofGapSoftwareForm(hits);
  122. checkBusinessProofActionOrder(hits);
  123. const packageJson = JSON.parse(read('package.json'));
  124. const acceptance = read('scripts/acceptance.js');
  125. if (!packageJson.scripts['plan:consistency:smoke']) {
  126. hits.push({ file: 'package.json', type: 'missing-script', phrase: 'plan:consistency:smoke' });
  127. }
  128. if (!acceptance.includes("plan:consistency:smoke")) {
  129. hits.push({ file: 'scripts/acceptance.js', type: 'missing-acceptance-gate', phrase: 'plan:consistency:smoke' });
  130. }
  131. const summary = {
  132. ok: hits.length === 0,
  133. scannedFiles: currentSpecFiles.length,
  134. hits
  135. };
  136. console.log(JSON.stringify(summary, null, 2));
  137. if (hits.length) process.exitCode = 1;
  138. }
  139. function checkProofGapSoftwareTable(hits) {
  140. const file = 'docs/ai-optimization-proof-gap-software-table.md';
  141. const text = read(file);
  142. const lines = text.split(/\r?\n/);
  143. const headerLine = lines.find((line) => {
  144. if (!line.startsWith('| brief编号 |')) return false;
  145. const cells = splitMarkdownRow(line);
  146. return JSON.stringify(cells) === JSON.stringify(softwareTableHeader);
  147. });
  148. if (!headerLine) {
  149. hits.push({ file, type: 'missing-proof-gap-software-header', phrase: 'brief编号' });
  150. }
  151. const rows = lines
  152. .map(splitMarkdownRow)
  153. .filter((cells) => /^GAP-\d{3}$/.test(cells[0] || ''));
  154. const ids = rows.map((cells) => cells[0]);
  155. const uniqueIds = new Set(ids);
  156. if (rows.length !== 5) {
  157. hits.push({ file, type: 'unexpected-proof-gap-software-row-count', phrase: `count=${rows.length}` });
  158. }
  159. if (uniqueIds.size !== ids.length) {
  160. hits.push({ file, type: 'duplicate-proof-gap-software-id', phrase: ids.join(',') });
  161. }
  162. const requiredRows = [
  163. ['GAP-001', '真实历史 Brief 数据集', '商务数据'],
  164. ['GAP-002', '真实视频资源表', '商务/投放'],
  165. ['GAP-003', '真实视频证据 A/B', '技术/AI'],
  166. ['GAP-004', '真实 live/provider 长跑证明', '技术/AI'],
  167. ['GAP-005', '商务复核和客户效果', '商务数据']
  168. ];
  169. for (const [id, title, owner] of requiredRows) {
  170. const row = rows.find((cells) => cells[0] === id);
  171. if (!row) {
  172. hits.push({ file, type: 'missing-proof-gap-software-row', phrase: id });
  173. continue;
  174. }
  175. if (row[3] !== owner || row[4] !== title) {
  176. hits.push({ file, type: 'proof-gap-software-row-mismatch', phrase: `${id}:${row[3]}/${row[4]}` });
  177. }
  178. }
  179. checkProofGapSoftwareMatchesNextActions(hits, rows);
  180. }
  181. function checkProofGapSoftwareMatchesNextActions(hits, proofGapRows) {
  182. const file = 'outputs/business-proof-next-actions-latest/business-proof-next-actions-summary.json';
  183. const fullPath = path.join(root, file);
  184. if (!fs.existsSync(fullPath)) {
  185. hits.push({ file, type: 'missing-next-actions-latest', phrase: file });
  186. return;
  187. }
  188. const summary = JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, ''));
  189. const actions = Array.isArray(summary.actions) ? summary.actions : [];
  190. const closure = readLatestProofGapClosure();
  191. const expectedBusinessGapCount = Number(closure?.businessGapCount || proofGapRows.length);
  192. if (proofGapRows.length !== expectedBusinessGapCount) {
  193. hits.push({ file, type: 'proof-gap-software-business-row-count-mismatch', phrase: `${expectedBusinessGapCount}/${proofGapRows.length}` });
  194. }
  195. const openBusinessGaps = currentBusinessProofGapRows(closure, 'open');
  196. const closedBusinessGaps = currentBusinessProofGapRows(closure, 'closed');
  197. const expectedActionCount = openBusinessGaps ? openBusinessGaps.length : proofGapRows.length;
  198. if (actions.length !== expectedActionCount) {
  199. hits.push({ file, type: 'proof-gap-software-next-actions-count-mismatch', phrase: `${expectedActionCount}/${actions.length}` });
  200. }
  201. const actionIds = new Set(actions.map((action) => action.id));
  202. if (openBusinessGaps) {
  203. for (const row of openBusinessGaps) {
  204. for (const actionId of actionIdsForProofGap(row.id)) {
  205. if (!actionIds.has(actionId)) {
  206. hits.push({ file, type: 'missing-next-action-for-open-proof-gap', phrase: `${row.id}:${actionId}` });
  207. }
  208. }
  209. }
  210. for (const row of closedBusinessGaps) {
  211. for (const actionId of actionIdsForProofGap(row.id)) {
  212. if (actionIds.has(actionId)) {
  213. hits.push({ file, type: 'unexpected-next-action-for-closed-proof-gap', phrase: `${row.id}:${actionId}` });
  214. }
  215. }
  216. }
  217. } else {
  218. const requiredActionTitles = [
  219. '填真实历史 Brief',
  220. '填真实视频资源',
  221. '人工复核标注',
  222. '真实视频 A/B 验收',
  223. '真实 live/provider 长跑证明'
  224. ];
  225. const actionTitles = new Set(actions.map((action) => action.title));
  226. for (const title of requiredActionTitles) {
  227. if (!actionTitles.has(title)) {
  228. hits.push({ file, type: 'missing-next-action-for-proof-gap-software-table', phrase: title });
  229. }
  230. }
  231. }
  232. const forbiddenActionTitles = [
  233. '真实材料预审',
  234. '历史数据导入',
  235. '历史数据审计',
  236. '视频资源就绪审计',
  237. '长跑前门禁',
  238. '运行策略矩阵',
  239. '视频 A/B 验收',
  240. '商务复核指标',
  241. '客户效果审计'
  242. ];
  243. for (const action of actions) {
  244. if (forbiddenActionTitles.includes(action.title)) {
  245. hits.push({ file, type: 'duplicate-technical-next-action', phrase: action.title });
  246. }
  247. }
  248. }
  249. function readLatestProofGapClosure() {
  250. const file = 'outputs/proof-gap-closure-latest/proof-gap-closure-summary.json';
  251. const fullPath = path.join(root, file);
  252. if (!fs.existsSync(fullPath)) return null;
  253. try {
  254. return JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, ''));
  255. } catch {
  256. return null;
  257. }
  258. }
  259. function currentBusinessProofGapRows(closure, status) {
  260. if (!closure || !Array.isArray(closure.rows)) return null;
  261. return closure.rows.filter((row) => row && row.id !== 'intake-readiness' && row.status === status);
  262. }
  263. function actionIdsForProofGap(gapId) {
  264. const map = {
  265. 'historical-dataset': ['step-03'],
  266. 'video-real-candidate': ['step-04'],
  267. 'manual-review-and-customer-effect': ['step-12'],
  268. 'video-ab-live-proof': ['gap-video-ab-live-proof'],
  269. 'live-provider-overnight-proof': ['gap-live-provider-overnight-proof']
  270. };
  271. return map[gapId] || [`gap-${gapId}`];
  272. }
  273. function checkLatestProofGapSoftwareForm(hits) {
  274. const file = 'outputs/tihao-proof-gap-software-form-latest/tihao-proof-gap-software-form-summary.json';
  275. const fullPath = path.join(root, file);
  276. if (!fs.existsSync(fullPath)) {
  277. hits.push({ file, type: 'missing-latest-proof-gap-software-form', phrase: file });
  278. return;
  279. }
  280. const summary = JSON.parse(fs.readFileSync(fullPath, 'utf8').replace(/^\uFEFF/, ''));
  281. if (summary.passed !== true) {
  282. hits.push({ file, type: 'latest-proof-gap-software-form-not-passed', phrase: 'passed=false' });
  283. }
  284. if (summary.headerMatches !== true || summary.rowWidthOk !== true) {
  285. hits.push({ file, type: 'latest-proof-gap-software-form-header-mismatch', phrase: 'header/width' });
  286. }
  287. if (summary.rowCount !== 5) {
  288. hits.push({ file, type: 'latest-proof-gap-software-form-row-count', phrase: `count=${summary.rowCount}` });
  289. }
  290. if (summary.duplicateIdCount !== 0) {
  291. hits.push({ file, type: 'latest-proof-gap-software-form-duplicate-id', phrase: `duplicate=${summary.duplicateIdCount}` });
  292. }
  293. }
  294. function checkBusinessProofActionOrder(hits) {
  295. const file = 'docs/business-proof-action-order.md';
  296. const text = read(file);
  297. const lines = text.split(/\r?\n/);
  298. const stepRows = lines
  299. .map(splitMarkdownRow)
  300. .filter((cells) => /^\d+$/.test(cells[0] || ''));
  301. if (stepRows.length !== 15) {
  302. hits.push({ file, type: 'unexpected-proof-action-step-count', phrase: `count=${stepRows.length}` });
  303. }
  304. const expectedCommands = [
  305. 'data:intake-template',
  306. 'video:intake-template',
  307. 'intake:readiness',
  308. 'history:from-csv',
  309. 'history:audit',
  310. 'video:resource-readiness',
  311. 'longrun:readiness',
  312. 'optimization:pipeline',
  313. 'acceptance:video-ab',
  314. 'review:metrics',
  315. 'customer-effect:audit',
  316. 'evidence:index',
  317. 'proof-gap:closure',
  318. 'handoff:summary',
  319. 'round:deposition'
  320. ];
  321. for (const command of expectedCommands) {
  322. if (!text.includes(command)) {
  323. hits.push({ file, type: 'missing-proof-action-command', phrase: command });
  324. }
  325. }
  326. const boundaryPhrases = [
  327. 'overallReady=true',
  328. 'failureCount=0',
  329. 'openCount=0',
  330. 'complete=true',
  331. 'ready_not_proven',
  332. 'blocked_by_external_data'
  333. ];
  334. for (const phrase of boundaryPhrases) {
  335. if (!text.includes(phrase)) {
  336. hits.push({ file, type: 'missing-proof-action-boundary', phrase });
  337. }
  338. }
  339. }
  340. function read(file) {
  341. return fs.readFileSync(path.join(root, file), 'utf8').replace(/^\uFEFF/, '');
  342. }
  343. function checkDedupJobTable(hits) {
  344. const file = 'docs/ai-optimization-job-dedup-software-format.md';
  345. const text = read(file);
  346. const lines = text.split(/\r?\n/);
  347. const headerLine = lines.find((line) => {
  348. if (!line.startsWith('| brief编号 |')) return false;
  349. const cells = splitMarkdownRow(line);
  350. return JSON.stringify(cells) === JSON.stringify(softwareTableHeader);
  351. });
  352. if (!headerLine) {
  353. hits.push({ file, type: 'missing-software-header', phrase: 'brief编号' });
  354. } else {
  355. const actualHeader = splitMarkdownRow(headerLine);
  356. if (JSON.stringify(actualHeader) !== JSON.stringify(softwareTableHeader)) {
  357. hits.push({
  358. file,
  359. type: 'software-header-mismatch',
  360. phrase: actualHeader.join(',')
  361. });
  362. }
  363. }
  364. const ids = [];
  365. for (const line of lines) {
  366. const cells = splitMarkdownRow(line);
  367. const id = cells[0];
  368. if (/^OPT-\d{3}$/.test(id)) ids.push(id);
  369. }
  370. const uniqueIds = new Set(ids);
  371. if (ids.length !== 9) {
  372. hits.push({ file, type: 'unexpected-task-count', phrase: `count=${ids.length}` });
  373. }
  374. if (uniqueIds.size !== ids.length) {
  375. hits.push({ file, type: 'duplicate-task-id', phrase: ids.join(',') });
  376. }
  377. const requiredRows = [
  378. 'OPT-001',
  379. 'OPT-002',
  380. 'OPT-003',
  381. 'OPT-004',
  382. 'OPT-005',
  383. 'OPT-006',
  384. 'OPT-007',
  385. 'OPT-008',
  386. 'OPT-009'
  387. ];
  388. for (const id of requiredRows) {
  389. if (!uniqueIds.has(id)) hits.push({ file, type: 'missing-task-row', phrase: id });
  390. }
  391. }
  392. function splitMarkdownRow(line) {
  393. if (!line || !line.trim().startsWith('|')) return [];
  394. return line
  395. .trim()
  396. .replace(/^\|/, '')
  397. .replace(/\|$/, '')
  398. .split('|')
  399. .map((cell) => cell.trim());
  400. }
  401. if (require.main === module) main();