business-execution-index.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const ROOT = path.resolve(__dirname, '..');
  5. const DEFAULT_OUTPUT = path.join(ROOT, 'outputs', 'business-execution-index-latest');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputsDir = path.resolve(args.outputs || path.join(ROOT, 'outputs'));
  9. const outputDir = path.resolve(args.output || DEFAULT_OUTPUT);
  10. const summary = buildBusinessExecutionIndex({ root: ROOT, outputsDir });
  11. fs.mkdirSync(outputDir, { recursive: true });
  12. const jsonPath = path.join(outputDir, 'business-execution-index-summary.json');
  13. const reportPath = path.join(outputDir, 'business-execution-index.md');
  14. const csvPath = path.join(outputDir, 'business-execution-index.csv');
  15. fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
  16. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  17. fs.writeFileSync(csvPath, withBom(renderCsv(summary.rows)), 'utf8');
  18. console.log(JSON.stringify({
  19. outputDir,
  20. json: jsonPath,
  21. report: reportPath,
  22. csv: csvPath,
  23. passed: summary.passed,
  24. rowCount: summary.rowCount,
  25. proofOpenCount: summary.proofOpenCount
  26. }, null, 2));
  27. if (args.strict && !summary.passed) process.exitCode = 1;
  28. }
  29. function buildBusinessExecutionIndex({ root, outputsDir }) {
  30. const nextActionsDir = pickDir(outputsDir, 'business-proof-next-actions-latest', /^business-proof-next-actions-/);
  31. const latestFormDir = pickDir(outputsDir, 'latest-form-index-latest', /^latest-form-index-/);
  32. const proofFormDir = pickDir(outputsDir, 'tihao-proof-gap-software-form-latest', /^tihao-proof-gap-software-form-/);
  33. const proofClosureDir = pickDir(outputsDir, 'proof-gap-closure-latest', /^proof-gap-closure-/);
  34. const evidenceDir = pickDir(outputsDir, 'evidence-index-latest', /^evidence-index-/);
  35. const fieldChecklistDir = pickDir(outputsDir, 'intake-field-checklist-latest', /^intake-field-checklist-/);
  36. const longRunReadinessDir = pickDir(outputsDir, 'long-run-readiness-latest', /^long-run-readiness-/);
  37. const optimizationCompletionDir = pickDir(outputsDir, 'optimization-completion-latest', /^optimization-completion-/);
  38. const proofGapOperatorPackDir = pickDir(outputsDir, 'proof-gap-operator-pack-latest', /^proof-gap-operator-pack-/);
  39. const nextActions = readJson(path.join(nextActionsDir, 'business-proof-next-actions-summary.json'));
  40. const latestForm = readJson(path.join(latestFormDir, 'latest-form-index-summary.json'));
  41. const proofForm = readJson(path.join(proofFormDir, 'tihao-proof-gap-software-form-summary.json'));
  42. const closure = readJson(path.join(proofClosureDir, 'proof-gap-closure-summary.json'));
  43. const evidence = readJson(path.join(evidenceDir, 'evidence-index-summary.json'));
  44. const fieldChecklist = readJson(path.join(fieldChecklistDir, 'intake-field-checklist-summary.json'));
  45. const longRunReadiness = readJson(path.join(longRunReadinessDir, 'long-run-readiness-summary.json'));
  46. const optimizationCompletion = readJson(path.join(optimizationCompletionDir, 'optimization-completion-summary.json'));
  47. const proofGapOperatorPack = readJson(path.join(proofGapOperatorPackDir, 'proof-gap-operator-pack-summary.json'));
  48. const operatorPackOwnerArtifacts = normalizeProofGapOperatorOwnerArtifacts(root, proofGapOperatorPackDir, proofGapOperatorPack);
  49. const rows = [
  50. ...buildOwnerActionRows(root, nextActionsDir, nextActions),
  51. ...buildRepairRows(root, nextActionsDir, nextActions),
  52. ...buildProofGapOperatorOwnerRows(operatorPackOwnerArtifacts),
  53. ...buildFieldChecklistOwnerRows(root, fieldChecklistDir, fieldChecklist),
  54. ...buildFieldChecklistRecheckRows(root, fieldChecklistDir, fieldChecklist),
  55. ...buildPrimaryFileRows(root, latestFormDir, latestForm, proofForm, fieldChecklistDir, fieldChecklist, longRunReadinessDir, longRunReadiness, optimizationCompletionDir, optimizationCompletion)
  56. ].map(row => ({
  57. ...row,
  58. boundary: normalizeProofBoundary(row.boundary)
  59. }));
  60. const proofOpenCount = Number(nextActions?.sourceState?.proofGapOpenCount ?? closure?.openCount ?? latestForm?.nextActions?.proofOpenCount ?? 0);
  61. const passed = rows.length > 0 && rows.every(row => row.owner && row.title && row.acceptance && row.boundary);
  62. return {
  63. generatedAt: new Date().toISOString(),
  64. root,
  65. outputsDir,
  66. passed,
  67. rowCount: rows.length,
  68. actionRowCount: rows.filter(row => row.type === '负责人行动').length,
  69. repairRowCount: rows.filter(row => row.type === '修复清单').length,
  70. operatorPackOwnerArtifactRowCount: rows.filter(row => row.type === '操作包负责人附件').length,
  71. operatorPackOwnerArtifactCount: operatorPackOwnerArtifacts.length,
  72. operatorPackOwnerArtifacts,
  73. fieldOwnerRowCount: rows.filter(row => row.type === '字段补齐负责人').length,
  74. recheckCommandRowCount: rows.filter(row => row.type === '补齐后复验命令').length,
  75. fileRowCount: rows.filter(row => row.type === '关键文件').length,
  76. proofOpenCount,
  77. closureComplete: Boolean(closure?.complete),
  78. status: proofOpenCount === 0 && closure?.complete ? 'ready_for_claim_audit' : 'actions_open',
  79. evidenceCounts: evidence?.counts || {},
  80. proofLevel: 'smoke_or_local',
  81. directCustomerProof: false,
  82. files: {
  83. report: 'outputs/business-execution-index-latest/business-execution-index.md',
  84. csv: 'outputs/business-execution-index-latest/business-execution-index.csv',
  85. summary: 'outputs/business-execution-index-latest/business-execution-index-summary.json'
  86. },
  87. sourceFiles: {
  88. nextActions: rel(root, path.join(nextActionsDir, 'business-proof-next-actions-summary.json')),
  89. latestFormIndex: rel(root, path.join(latestFormDir, 'latest-form-index-summary.json')),
  90. proofGapSoftwareForm: rel(root, path.join(proofFormDir, 'tihao-proof-gap-software-form-summary.json')),
  91. proofGapClosure: rel(root, path.join(proofClosureDir, 'proof-gap-closure-summary.json')),
  92. intakeFieldChecklist: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist-summary.json')),
  93. longRunReadiness: rel(root, path.join(longRunReadinessDir, 'long-run-readiness-summary.json')),
  94. optimizationCompletion: rel(root, path.join(optimizationCompletionDir, 'optimization-completion-summary.json')),
  95. proofGapOperatorPack: rel(root, path.join(proofGapOperatorPackDir, 'proof-gap-operator-pack-summary.json')),
  96. evidenceIndex: rel(root, path.join(evidenceDir, 'evidence-index-summary.json'))
  97. },
  98. guardrails: [
  99. '本表只用于商务补证执行分派,不证明客户效果或命中率提升。',
  100. 'sample、smoke、模板、provider fallback、接口 200 都不能作为业务证明。',
  101. '只有 proof-gap-closure openCount=0 且 customer-effect/video/live 真实审计通过后,才能进入效果宣称。',
  102. 'optimization-completion readyForClaim=false 时,本表不证明客户效果,不得宣称提号率提升、客户效果达标或人工补号量下降。',
  103. '本表不得包含 sessionToken、Authorization、模型 token 或 npm token。'
  104. ],
  105. rows
  106. };
  107. }
  108. function buildOwnerActionRows(root, nextActionsDir, nextActions) {
  109. const actions = Array.isArray(nextActions?.actions) ? nextActions.actions : [];
  110. return actions.map(action => {
  111. const ownerArtifact = findOwnerArtifact(nextActions, action.owner);
  112. return {
  113. owner: action.owner || '未分配',
  114. type: '负责人行动',
  115. title: action.title || '',
  116. priority: Number(action.priority || action.order || 0),
  117. status: action.status || 'open',
  118. mainFile: ownerArtifact?.markdown ? rel(root, path.join(nextActionsDir, ownerArtifact.markdown)) : rel(root, path.join(nextActionsDir, 'business-proof-next-actions-report.md')),
  119. csv: ownerArtifact?.csv ? rel(root, path.join(nextActionsDir, ownerArtifact.csv)) : rel(root, path.join(nextActionsDir, 'business-proof-next-actions.csv')),
  120. repairFile: '',
  121. actionCount: 1,
  122. command: action.command || '',
  123. expectedArtifact: action.expectedArtifact || '',
  124. acceptance: appendMissingProofRequirements(action.acceptance || '', action.missingProofRequirements),
  125. boundary: '补证任务,不证明业务效果'
  126. };
  127. });
  128. }
  129. function buildRepairRows(root, nextActionsDir, nextActions) {
  130. const ownerArtifacts = Array.isArray(nextActions?.ownerArtifacts) ? nextActions.ownerArtifacts : [];
  131. return ownerArtifacts.flatMap(owner => {
  132. const repairs = Array.isArray(owner.repairArtifacts) ? owner.repairArtifacts : [];
  133. const ownerMarkdown = owner.markdown ? rel(root, path.join(nextActionsDir, owner.markdown)) : '';
  134. const ownerCsv = owner.csv ? rel(root, path.join(nextActionsDir, owner.csv)) : '';
  135. return repairs.map(repair => ({
  136. owner: owner.owner || repair.owner || '未分配',
  137. type: '修复清单',
  138. title: repair.title || '修复清单',
  139. priority: Number(repair.topPriority || 0),
  140. status: Number(repair.actionCount || 0) > 0 ? 'actions_open' : 'empty',
  141. mainFile: ownerMarkdown,
  142. csv: ownerCsv,
  143. repairFile: repair.csv || '',
  144. actionCount: Number(repair.actionCount || 0),
  145. command: '按修复清单补齐字段后重新运行 round:refresh',
  146. expectedArtifact: repair.csv || '',
  147. acceptance: Number(repair.actionCount || 0) > 0 ? `修复动作数=${Number(repair.actionCount || 0)},补齐后对应 readiness failureCount=0` : '无修复动作',
  148. boundary: '修复清单是补证执行材料,不证明客户效果'
  149. }));
  150. });
  151. }
  152. function buildProofGapOperatorOwnerRows(ownerArtifacts) {
  153. return ownerArtifacts.map(item => ({
  154. owner: item.owner || '未分配',
  155. type: '操作包负责人附件',
  156. title: 'proof-gap 操作包负责人附件',
  157. priority: Number(item.topPriority || 0),
  158. status: Number(item.actionCount || 0) > 0 ? 'actions_open' : 'empty',
  159. mainFile: item.markdown || '',
  160. csv: item.csv || '',
  161. repairFile: item.csv || '',
  162. actionCount: Number(item.actionCount || 0),
  163. command: '打开负责人附件,按 gapId 补齐当前证据和下一步动作后重新运行 proof-gap:operator-pack 与 round:refresh',
  164. expectedArtifact: [item.markdown, item.csv].filter(Boolean).join(';'),
  165. acceptance: [
  166. `actionCount=${Number(item.actionCount || 0)}`,
  167. `evidenceRowCount=${Number(item.evidenceRowCount || 0)}`,
  168. `nextActionCount=${Number(item.nextActionCount || 0)}`,
  169. `gapIds=${(item.gapIds || []).join(';') || 'none'}`
  170. ].join(';'),
  171. boundary: '负责人附件只用于补证派工,不证明客户效果'
  172. }));
  173. }
  174. function buildFieldChecklistOwnerRows(root, fieldChecklistDir, fieldChecklist) {
  175. const groups = Array.isArray(fieldChecklist?.ownerGroups) ? fieldChecklist.ownerGroups : [];
  176. return groups.map((group, index) => ({
  177. owner: group.owner || '未分配',
  178. type: '字段补齐负责人',
  179. title: `${sectionLabel(group.section)}字段补齐`,
  180. priority: index + 1,
  181. status: Number(group.missingRequiredCount || 0) > 0 || Number(group.placeholderCount || 0) > 0 ? 'actions_open' : 'ready',
  182. mainFile: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.md')),
  183. csv: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.csv')),
  184. repairFile: Array.isArray(group.files) ? group.files.join(';') : '',
  185. actionCount: Number(group.itemCount || 0),
  186. command: group.nextAction || '按字段核对表补齐真实材料',
  187. expectedArtifact: Array.isArray(group.files) ? group.files.join(';') : rel(root, path.join(fieldChecklistDir, 'intake-field-checklist-summary.json')),
  188. acceptance: [
  189. `字段项=${Number(group.itemCount || 0)}`,
  190. `缺必填=${Number(group.missingRequiredCount || 0)}`,
  191. `占位=${Number(group.placeholderCount || 0)}`,
  192. group.acceptance || ''
  193. ].filter(Boolean).join(';'),
  194. boundary: group.boundary || '补齐材料,不证明客户效果'
  195. }));
  196. }
  197. function buildFieldChecklistRecheckRows(root, fieldChecklistDir, fieldChecklist) {
  198. const commands = Array.isArray(fieldChecklist?.recheckCommands) ? fieldChecklist.recheckCommands : [];
  199. return commands.map((item, index) => ({
  200. owner: item.owner || '技术/AI',
  201. type: '补齐后复验命令',
  202. title: item.stage || `复验命令 ${index + 1}`,
  203. priority: index + 1,
  204. status: 'pending_after_fill',
  205. mainFile: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.md')),
  206. csv: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.csv')),
  207. repairFile: '',
  208. actionCount: 1,
  209. command: item.command || '',
  210. expectedArtifact: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist-summary.json')),
  211. acceptance: item.acceptance || '复验命令通过',
  212. boundary: item.boundary || '复验通过不证明客户效果'
  213. }));
  214. }
  215. function buildPrimaryFileRows(root, latestFormDir, latestForm, proofForm, fieldChecklistDir, fieldChecklist, longRunReadinessDir, longRunReadiness, optimizationCompletionDir, optimizationCompletion) {
  216. const rows = [];
  217. if (latestForm?.primaryForm?.csv || proofForm) {
  218. rows.push({
  219. owner: '商务',
  220. type: '关键文件',
  221. title: '最新提号补证表单',
  222. priority: 0,
  223. status: proofForm?.passed ? 'pass' : 'not_ready',
  224. mainFile: latestForm?.primaryForm?.csv || 'outputs/tihao-proof-gap-software-form-latest/tihao-proof-gap-software-form.csv',
  225. csv: latestForm?.primaryForm?.csv || '',
  226. repairFile: '',
  227. actionCount: Number(proofForm?.rowCount || 0),
  228. command: '打开 CSV 分派补证任务',
  229. expectedArtifact: latestForm?.primaryForm?.summary || '',
  230. acceptance: `rowCount=${Number(proofForm?.rowCount || 0)},duplicateId=${Number(proofForm?.duplicateIdCount || 0)}`,
  231. boundary: '补证表单不证明客户效果'
  232. });
  233. }
  234. if (latestForm?.clientList?.csv) {
  235. rows.push({
  236. owner: '商务',
  237. type: '关键文件',
  238. title: '最新软件端候选名单',
  239. priority: 0,
  240. status: latestForm.clientList.exists === false ? 'missing' : 'ready',
  241. mainFile: latestForm.clientList.csv,
  242. csv: latestForm.clientList.csv,
  243. repairFile: '',
  244. actionCount: Number(latestForm.clientList.rowCount || 0),
  245. command: '用于商务复核候选博主',
  246. expectedArtifact: latestForm.clientList.summary || '',
  247. acceptance: `候选行数=${Number(latestForm.clientList.rowCount || 0)},仍需人工复核和客户反馈`,
  248. boundary: '候选名单不证明命中率提升'
  249. });
  250. }
  251. if (fieldChecklist?.passed) {
  252. rows.push({
  253. owner: '商务',
  254. type: '关键文件',
  255. title: '真实材料字段级补齐核对表',
  256. priority: 0,
  257. status: Number(fieldChecklist.itemCount || 0) > 0 ? 'actions_open' : 'empty',
  258. mainFile: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.md')),
  259. csv: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.csv')),
  260. repairFile: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist.csv')),
  261. actionCount: Number(fieldChecklist.itemCount || 0),
  262. command: '按字段级核对表逐格补齐真实材料后重新运行 round:refresh',
  263. expectedArtifact: rel(root, path.join(fieldChecklistDir, 'intake-field-checklist-summary.json')),
  264. acceptance: `itemCount=${Number(fieldChecklist.itemCount || 0)},placeholder=${Number(fieldChecklist.placeholderCount || 0)},missingRequired=${Number(fieldChecklist.missingRequiredCount || 0)}`,
  265. boundary: '字段级核对表只指导补齐材料,不证明客户效果'
  266. });
  267. }
  268. if (longRunReadiness || fs.existsSync(path.join(longRunReadinessDir, 'long-run-readiness-report.md'))) {
  269. const failCount = Number(longRunReadiness?.counts?.fail ?? longRunReadiness?.failCount ?? 0);
  270. rows.push({
  271. owner: '技术/AI',
  272. type: '关键文件',
  273. title: '长跑前置门禁报告',
  274. priority: 0,
  275. status: longRunReadiness?.ready ? 'ready' : 'blocked',
  276. mainFile: rel(root, path.join(longRunReadinessDir, 'long-run-readiness-report.md')),
  277. csv: '',
  278. repairFile: '',
  279. actionCount: failCount,
  280. command: '补齐真实材料后运行 longrun:readiness,再判断是否允许启动 full-matrix/live 长跑',
  281. expectedArtifact: rel(root, path.join(longRunReadinessDir, 'long-run-readiness-summary.json')),
  282. acceptance: `ready=${Boolean(longRunReadiness?.ready)},failCount=${failCount};ready=true 且 failCount=0 后才允许进入真实 live/provider 长跑启动判断。`,
  283. boundary: '长跑前置门禁只判断启动条件,不证明客户效果;ready=false 时不能启动或宣称 full-matrix/live 长跑完成。'
  284. });
  285. }
  286. if (optimizationCompletion || fs.existsSync(path.join(optimizationCompletionDir, 'optimization-completion-report.md'))) {
  287. const blockingReasonCount = Number(optimizationCompletion?.blockingReasonCount ?? (Array.isArray(optimizationCompletion?.blockingReasons)
  288. ? optimizationCompletion.blockingReasons.length
  289. : 0));
  290. const missingProofRequirementCount = Number(optimizationCompletion?.missingProofRequirementCount ?? uniqueMissingProofRequirementCount(optimizationCompletion?.blockingReasons));
  291. const proofGapOpenCount = Number(optimizationCompletion?.proofGapOpenCount || 0);
  292. const videoAbPreflightReadyForVideoAb = Boolean(optimizationCompletion?.videoAbPreflightReadyForVideoAb);
  293. const videoAbPreflightFailureCount = Number.isFinite(Number(optimizationCompletion?.videoAbPreflightFailureCount))
  294. ? Number(optimizationCompletion.videoAbPreflightFailureCount)
  295. : null;
  296. rows.push({
  297. owner: '技术/AI',
  298. type: '关键文件',
  299. title: '优化完成度审计报告',
  300. priority: 0,
  301. status: optimizationCompletion?.readyForClaim ? 'ready_for_claim' : 'blocked',
  302. mainFile: rel(root, path.join(optimizationCompletionDir, 'optimization-completion-report.md')),
  303. csv: '',
  304. repairFile: '',
  305. actionCount: blockingReasonCount,
  306. command: '补齐真实材料和证明缺口后运行 optimization:completion,再判断是否允许声明长期目标完成',
  307. expectedArtifact: rel(root, path.join(optimizationCompletionDir, 'optimization-completion-summary.json')),
  308. acceptance: `complete=${Boolean(optimizationCompletion?.complete)},readyForClaim=${Boolean(optimizationCompletion?.readyForClaim)},proofGapOpenCount=${proofGapOpenCount},blockingReasons=${blockingReasonCount},missingProofRequirementCount=${missingProofRequirementCount},videoAbPreflightReadyForVideoAb=${videoAbPreflightReadyForVideoAb},videoAbPreflightFailureCount=${videoAbPreflightFailureCount ?? 'unknown'}`,
  309. boundary: '优化完成度审计只判断能否进入完成声明,不证明客户效果;readyForClaim=false 时不得宣称提号率提升、客户效果达标或人工补号量下降。'
  310. });
  311. }
  312. if (latestForm?.handoff?.report) {
  313. rows.push({
  314. owner: '技术/AI',
  315. type: '关键文件',
  316. title: '下一轮交接摘要',
  317. priority: 0,
  318. status: latestForm.handoff.complete ? 'complete_reported' : 'incomplete_reported',
  319. mainFile: latestForm.handoff.report,
  320. csv: '',
  321. repairFile: '',
  322. actionCount: 1,
  323. command: '下一轮 AI 先读 handoff 和本执行总表',
  324. expectedArtifact: latestForm.handoff.summary || '',
  325. acceptance: `handoff.complete=${Boolean(latestForm.handoff.complete)}`,
  326. boundary: '交接摘要不证明客户效果'
  327. });
  328. } else if (fs.existsSync(path.join(latestFormDir, 'latest-form-index.md'))) {
  329. rows.push({
  330. owner: '技术/AI',
  331. type: '关键文件',
  332. title: '最新表单索引',
  333. priority: 0,
  334. status: latestForm?.passed ? 'pass' : 'not_ready',
  335. mainFile: rel(root, path.join(latestFormDir, 'latest-form-index.md')),
  336. csv: '',
  337. repairFile: '',
  338. actionCount: 1,
  339. command: '定位最新补证表单、候选名单和负责人文件',
  340. expectedArtifact: rel(root, path.join(latestFormDir, 'latest-form-index-summary.json')),
  341. acceptance: `latestFormIndex.passed=${Boolean(latestForm?.passed)}`,
  342. boundary: '索引不证明客户效果'
  343. });
  344. }
  345. return rows;
  346. }
  347. function renderReport(summary) {
  348. const lines = [
  349. '# 商务补证执行总表',
  350. '',
  351. `- 生成时间:${summary.generatedAt}`,
  352. `- 是否通过:${summary.passed ? '是' : '否'}`,
  353. `- 当前状态:${summary.status}`,
  354. `- 未关闭证明缺口:${summary.proofOpenCount}`,
  355. `- 行数:${summary.rowCount}`,
  356. '',
  357. '## 执行总表',
  358. '',
  359. '| 负责人 | 类型 | 标题 | 优先级 | 状态 | 主文件 | 修复清单 | 动作数 | 验收标准 | 边界 |',
  360. '| --- | --- | --- | ---: | --- | --- | --- | ---: | --- | --- |',
  361. ...summary.rows.map(row => tableRow([
  362. row.owner,
  363. row.type,
  364. row.title,
  365. row.priority,
  366. row.status,
  367. row.mainFile || row.csv || '',
  368. row.repairFile || '',
  369. row.actionCount,
  370. row.acceptance,
  371. row.boundary
  372. ])),
  373. '',
  374. '## 来源',
  375. '',
  376. ...Object.entries(summary.sourceFiles).map(([key, value]) => `- ${key}:${value || '缺失'}`),
  377. '',
  378. '## 防误用边界',
  379. '',
  380. ...summary.guardrails.map(item => `- ${item}`)
  381. ];
  382. return lines.join('\n');
  383. }
  384. function renderCsv(rows) {
  385. const headers = ['负责人', '类型', '标题', '优先级', '状态', '主文件', '负责人CSV', '修复清单', '动作数', '命令/动作', '预期产物', '验收标准', '边界'];
  386. return [
  387. headers.join(','),
  388. ...rows.map(row => [
  389. row.owner,
  390. row.type,
  391. row.title,
  392. row.priority,
  393. row.status,
  394. row.mainFile,
  395. row.csv,
  396. row.repairFile,
  397. row.actionCount,
  398. row.command,
  399. row.expectedArtifact,
  400. row.acceptance,
  401. row.boundary
  402. ].map(csvCell).join(','))
  403. ].join('\n');
  404. }
  405. function appendMissingProofRequirements(acceptance, missingProofRequirements) {
  406. const items = Array.isArray(missingProofRequirements) ? missingProofRequirements.filter(Boolean) : [];
  407. if (!items.length) return acceptance;
  408. return `${acceptance}${acceptance ? ';' : ''}缺失证明:${items.join(';')}`;
  409. }
  410. function normalizeProofBoundary(boundary) {
  411. const text = String(boundary || '').trim();
  412. const fallback = '不证明客户效果';
  413. if (!text) return fallback;
  414. if (text.includes('不证明') || text.includes('不能证明')) return text;
  415. if (text.includes('不得宣称') || text.includes('不得宣布') || text.includes('只证明')) {
  416. return `${text};${fallback}`;
  417. }
  418. return `${text};${fallback}`;
  419. }
  420. function uniqueMissingProofRequirementCount(rows) {
  421. if (!Array.isArray(rows)) return 0;
  422. const values = new Set();
  423. for (const row of rows) {
  424. const requirements = Array.isArray(row.missingProofRequirements) ? row.missingProofRequirements : [];
  425. for (const requirement of requirements) {
  426. if (requirement) values.add(String(requirement));
  427. }
  428. }
  429. return values.size;
  430. }
  431. function sectionLabel(section) {
  432. const value = String(section || '');
  433. if (value === 'history') return '历史 Brief';
  434. if (value === 'video') return '视频资源';
  435. if (value === 'review') return '商务复核';
  436. return value || '真实材料';
  437. }
  438. function findOwnerArtifact(summary, owner) {
  439. return (summary?.ownerArtifacts || []).find(item => item.owner === owner);
  440. }
  441. function normalizeProofGapOperatorOwnerArtifacts(root, dir, summary) {
  442. const artifacts = Array.isArray(summary?.ownerArtifacts) ? summary.ownerArtifacts : [];
  443. return artifacts.map(item => ({
  444. owner: item.owner || '',
  445. actionCount: Number(item.actionCount || 0),
  446. topPriority: Number(item.topPriority || 0),
  447. markdown: artifactRef(root, dir, item.markdown || ''),
  448. csv: artifactRef(root, dir, item.csv || ''),
  449. gapIds: Array.isArray(item.gapIds) ? item.gapIds.filter(Boolean) : [],
  450. evidenceRowCount: Number(item.evidenceRowCount || 0),
  451. nextActionCount: Number(item.nextActionCount || 0)
  452. }));
  453. }
  454. function artifactRef(root, dir, file) {
  455. const value = String(file || '').replace(/\\/g, '/');
  456. if (!value) return '';
  457. if (path.isAbsolute(file)) return rel(root, file);
  458. if (value.startsWith('outputs/')) return value;
  459. return rel(root, path.join(dir, file));
  460. }
  461. function pickDir(outputsDir, latestName, pattern) {
  462. const latest = path.join(outputsDir, latestName);
  463. if (fs.existsSync(latest)) return latest;
  464. if (!fs.existsSync(outputsDir)) return latest;
  465. const dirs = fs.readdirSync(outputsDir, { withFileTypes: true })
  466. .filter(entry => entry.isDirectory() && pattern.test(entry.name))
  467. .map(entry => path.join(outputsDir, entry.name))
  468. .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
  469. return dirs[0] || latest;
  470. }
  471. function readJson(file) {
  472. if (!fs.existsSync(file)) return null;
  473. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  474. }
  475. function rel(root, file) {
  476. return file ? path.relative(root, file).replace(/\\/g, '/') : '';
  477. }
  478. function tableRow(cells) {
  479. return `| ${cells.map(escapeCell).join(' | ')} |`;
  480. }
  481. function escapeCell(value) {
  482. return String(value ?? '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  483. }
  484. function csvCell(value) {
  485. const text = String(value ?? '');
  486. return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
  487. }
  488. function parseArgs(argv) {
  489. const result = {};
  490. for (let index = 0; index < argv.length; index += 1) {
  491. const arg = argv[index];
  492. if (!arg.startsWith('--')) continue;
  493. const key = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  494. const next = argv[index + 1];
  495. if (!next || next.startsWith('--')) result[key] = true;
  496. else {
  497. result[key] = next;
  498. index += 1;
  499. }
  500. }
  501. return result;
  502. }
  503. function withBom(text) {
  504. return `\uFEFF${text}`;
  505. }
  506. if (require.main === module) main();
  507. module.exports = {
  508. buildBusinessExecutionIndex
  509. };