optimization-pipeline.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { spawnSync } = require('child_process');
  5. const ROOT = path.resolve(__dirname, '..');
  6. function main() {
  7. const args = parseArgs(process.argv.slice(2));
  8. const outputRoot = path.resolve(args.output || process.env.TIHAO_PIPELINE_OUTPUT || path.join(ROOT, 'outputs', `optimization-pipeline-${Date.now()}`));
  9. const dryRun = Boolean(args.dryRun);
  10. const strict = Boolean(args.strict);
  11. fs.mkdirSync(outputRoot, { recursive: true });
  12. const context = buildContext(args, outputRoot);
  13. const steps = buildSteps(context);
  14. const results = [];
  15. for (const step of steps) {
  16. if (dryRun) {
  17. results.push({ id: step.id, status: 'planned', command: step.command.join(' '), output: step.output || '' });
  18. continue;
  19. }
  20. const result = runStep(step);
  21. results.push(result);
  22. if (strict && result.status !== 'pass') break;
  23. }
  24. const closureResult = results.find(item => item.id === 'proof-gap-closure');
  25. const closureSummary = closureResult?.status === 'pass' ? readJson(closureResult.output) : null;
  26. const proofGapClosureClaimable = isClaimableProofGapClosure(closureSummary);
  27. const readyForClaim = results.every(item => item.status === 'pass') &&
  28. Boolean(context.inputs.reviewCsv) &&
  29. proofGapClosureClaimable;
  30. const summary = {
  31. generatedAt: new Date().toISOString(),
  32. outputRoot,
  33. dryRun,
  34. strict,
  35. inputs: context.inputs,
  36. counts: countStatuses(results),
  37. readyForClaim,
  38. proofGapClosure: closureSummary ? {
  39. complete: Boolean(closureSummary.complete),
  40. openCount: Number(closureSummary.openCount || 0),
  41. closedCount: Number(closureSummary.closedCount || 0),
  42. businessGapCount: Number(closureSummary.businessGapCount || 0),
  43. claimable: proofGapClosureClaimable
  44. } : null,
  45. results,
  46. nextActions: buildNextActions(results, context, { dryRun })
  47. };
  48. const jsonPath = path.join(outputRoot, 'optimization-pipeline-summary.json');
  49. const reportPath = path.join(outputRoot, 'optimization-pipeline-report.md');
  50. fs.writeFileSync(jsonPath, JSON.stringify(summary, null, 2), 'utf8');
  51. fs.writeFileSync(reportPath, withBom(renderReport(summary)), 'utf8');
  52. console.log(JSON.stringify({
  53. outputRoot,
  54. json: jsonPath,
  55. report: reportPath,
  56. counts: summary.counts,
  57. readyForClaim: summary.readyForClaim,
  58. nextActionCount: summary.nextActions.length
  59. }, null, 2));
  60. if (strict && !summary.readyForClaim) process.exitCode = 2;
  61. }
  62. function buildContext(args, outputRoot) {
  63. const dataPack = resolveOptional(args.dataPack || process.env.TIHAO_DATA_PACK);
  64. const videoPack = resolveOptional(args.videoPack || process.env.TIHAO_VIDEO_PACK);
  65. const historyCsv = resolveOptional(args.historyCsv || process.env.TIHAO_HISTORY_CSV || (dataPack ? path.join(dataPack, 'history-data-template.csv') : ''));
  66. const historyDir = resolveOptional(args.historyDir || process.env.TIHAO_HISTORY_DATASET);
  67. const preflight = resolveOptional(args.preflight || process.env.TIHAO_PREFLIGHT_SUMMARY);
  68. const reviewCsv = resolveOptional(args.reviewCsv || process.env.TIHAO_REVIEW_CSV || (dataPack ? path.join(dataPack, 'manual-review-template.csv') : ''));
  69. const videoCsv = resolveOptional(args.videoCsv || process.env.TIHAO_VIDEO_CSV || (videoPack ? path.join(videoPack, 'video-resource-template.csv') : ''));
  70. const currentManualSupplementCount = args.currentManualSupplementCount || process.env.TIHAO_CURRENT_MANUAL_SUPPLEMENT_COUNT || '';
  71. const historyMinBriefs = args.historyMinBriefs || process.env.TIHAO_HISTORY_MIN_BRIEFS || '';
  72. const generatedHistoryDir = path.join(outputRoot, 'history-dataset');
  73. const intakeOutput = path.join(outputRoot, 'intake-readiness');
  74. const historyAuditOutput = path.join(outputRoot, 'history-audit');
  75. const videoReadinessOutput = path.join(outputRoot, 'video-resource-readiness');
  76. const reviewOutput = path.join(outputRoot, 'review-metrics');
  77. const customerEffectOutput = path.join(outputRoot, 'customer-effect-audit');
  78. const closureOutput = path.join(outputRoot, 'proof-gap-closure');
  79. const readinessOutput = path.join(outputRoot, 'long-run-readiness');
  80. const evidenceOutput = path.join(outputRoot, 'evidence-index');
  81. return {
  82. outputRoot,
  83. historyDatasetDir: historyCsv ? generatedHistoryDir : historyDir,
  84. historyAuditJson: path.join(historyAuditOutput, 'historical-dataset-audit.json'),
  85. intakeReadinessJson: path.join(intakeOutput, 'intake-readiness-summary.json'),
  86. videoReadinessJson: path.join(videoReadinessOutput, 'video-resource-readiness-summary.json'),
  87. inputs: {
  88. dataPack,
  89. videoPack,
  90. historyCsv,
  91. historyDir,
  92. videoCsv,
  93. preflight,
  94. reviewCsv,
  95. currentManualSupplementCount,
  96. historyMinBriefs
  97. },
  98. paths: {
  99. generatedHistoryDir,
  100. intakeOutput,
  101. historyAuditOutput,
  102. videoReadinessOutput,
  103. reviewOutput,
  104. customerEffectOutput,
  105. closureOutput,
  106. readinessOutput,
  107. evidenceOutput
  108. }
  109. };
  110. }
  111. function buildSteps(context) {
  112. const steps = [];
  113. if (context.inputs.dataPack || context.inputs.videoPack || (context.inputs.historyCsv && context.inputs.reviewCsv && context.inputs.videoCsv)) {
  114. const intakeArgs = ['npm.cmd', 'run', 'intake:readiness', '--', '--output', context.paths.intakeOutput];
  115. if (context.inputs.dataPack) intakeArgs.push('--data-pack', context.inputs.dataPack);
  116. else {
  117. intakeArgs.push('--history-csv', context.inputs.historyCsv);
  118. intakeArgs.push('--review-csv', context.inputs.reviewCsv);
  119. }
  120. if (context.inputs.videoPack) intakeArgs.push('--video-pack', context.inputs.videoPack);
  121. else intakeArgs.push('--video-csv', context.inputs.videoCsv);
  122. if (context.inputs.historyMinBriefs) intakeArgs.push('--min-briefs', context.inputs.historyMinBriefs);
  123. steps.push(step('intake-readiness', intakeArgs, context.intakeReadinessJson));
  124. }
  125. if (context.inputs.historyCsv) {
  126. steps.push(step('history-from-csv', ['npm.cmd', 'run', 'history:from-csv', '--', '--input', context.inputs.historyCsv, '--output', context.paths.generatedHistoryDir], context.paths.generatedHistoryDir));
  127. }
  128. if (context.historyDatasetDir) {
  129. const historyAuditArgs = ['npm.cmd', 'run', 'history:audit', '--', '--input', context.historyDatasetDir, '--output', context.paths.historyAuditOutput, '--strict'];
  130. if (context.inputs.historyMinBriefs) historyAuditArgs.push('--min-briefs', context.inputs.historyMinBriefs);
  131. steps.push(step('history-audit', historyAuditArgs, context.historyAuditJson));
  132. }
  133. if (context.inputs.videoCsv) {
  134. steps.push(step('video-resource-readiness', ['npm.cmd', 'run', 'video:resource-readiness', '--', '--input', context.inputs.videoCsv, '--output', context.paths.videoReadinessOutput, '--strict'], context.videoReadinessJson));
  135. }
  136. const readinessArgs = ['npm.cmd', 'run', 'longrun:readiness', '--', '--mode', context.inputs.reviewCsv ? 'customer-effect' : 'full-matrix', '--output', context.paths.readinessOutput];
  137. if (context.inputs.preflight) readinessArgs.push('--preflight', context.inputs.preflight);
  138. if (context.historyDatasetDir) readinessArgs.push('--history', context.historyAuditJson);
  139. if (context.inputs.dataPack || context.inputs.videoPack || context.inputs.videoCsv) readinessArgs.push('--intake-readiness', context.intakeReadinessJson);
  140. if (context.inputs.videoCsv) readinessArgs.push('--video-readiness', context.videoReadinessJson);
  141. steps.push(step('longrun-readiness', readinessArgs, path.join(context.paths.readinessOutput, 'long-run-readiness-summary.json')));
  142. if (context.inputs.reviewCsv) {
  143. steps.push(step('review-metrics', ['npm.cmd', 'run', 'review:metrics', '--', '--input', context.inputs.reviewCsv, '--output', context.paths.reviewOutput, '--strict'], path.join(context.paths.reviewOutput, 'review-metrics-summary.json')));
  144. const effectArgs = ['npm.cmd', 'run', 'customer-effect:audit', '--', '--review-csv', context.inputs.reviewCsv, '--history-audit', context.historyAuditJson, '--output', context.paths.customerEffectOutput, '--strict'];
  145. if (context.inputs.currentManualSupplementCount) effectArgs.push('--current-manual-supplement-count', context.inputs.currentManualSupplementCount);
  146. steps.push(step('customer-effect-audit', effectArgs, path.join(context.paths.customerEffectOutput, 'customer-effect-summary.json')));
  147. }
  148. const closureArgs = ['npm.cmd', 'run', 'proof-gap:closure', '--', '--output', context.paths.closureOutput];
  149. if (context.historyDatasetDir) closureArgs.push('--history-audit', context.historyAuditJson);
  150. if (context.inputs.dataPack || context.inputs.videoPack || context.inputs.videoCsv) closureArgs.push('--intake-readiness', context.intakeReadinessJson);
  151. if (context.inputs.videoCsv) closureArgs.push('--video-readiness', context.videoReadinessJson);
  152. if (context.inputs.reviewCsv) closureArgs.push('--customer-effect', path.join(context.paths.customerEffectOutput, 'customer-effect-summary.json'));
  153. steps.push(step('proof-gap-closure', closureArgs, path.join(context.paths.closureOutput, 'proof-gap-closure-summary.json')));
  154. steps.push(step('evidence-index', ['npm.cmd', 'run', 'evidence:index', '--', '--output', context.paths.evidenceOutput], path.join(context.paths.evidenceOutput, 'evidence-index-summary.json')));
  155. return steps;
  156. }
  157. function step(id, command, output) {
  158. return { id, command, output };
  159. }
  160. function runStep(step) {
  161. const [command, ...args] = step.command;
  162. const result = process.platform === 'win32'
  163. ? spawnSync('cmd.exe', ['/d', '/s', '/c', step.command.map(cmdQuote).join(' ')], { cwd: ROOT, encoding: 'utf8' })
  164. : spawnSync(command, args, { cwd: ROOT, encoding: 'utf8' });
  165. return {
  166. id: step.id,
  167. command: step.command.join(' '),
  168. output: step.output,
  169. status: result.status === 0 ? 'pass' : 'fail',
  170. exitCode: result.status,
  171. stdout: safeTail(result.stdout),
  172. stderr: safeTail(result.stderr)
  173. };
  174. }
  175. function cmdQuote(value) {
  176. const text = String(value || '');
  177. if (!/[()\s&|<>^"]/u.test(text)) return text;
  178. return `"${text.replace(/"/g, '\\"')}"`;
  179. }
  180. function renderReport(summary) {
  181. const lines = [
  182. '# 提号优化 Pipeline 报告',
  183. '',
  184. `- 生成时间:${summary.generatedAt}`,
  185. `- 输出目录:${summary.outputRoot}`,
  186. `- dryRun:${summary.dryRun ? '是' : '否'}`,
  187. `- strict:${summary.strict ? '是' : '否'}`,
  188. `- 是否可宣称业务效果:${summary.readyForClaim ? '是' : '否'}`,
  189. `- proofGapClosure.complete:${summary.proofGapClosure?.complete ? 'true' : 'false'}`,
  190. `- proofGapClosure.openCount:${summary.proofGapClosure?.openCount ?? 'unknown'}`,
  191. '',
  192. '## 输入',
  193. '',
  194. ...Object.entries(summary.inputs).map(([key, value]) => `- ${key}: ${value || '未提供'}`),
  195. '',
  196. '## 步骤',
  197. '',
  198. '| 步骤 | 状态 | 输出 | 命令 |',
  199. '| --- | --- | --- | --- |',
  200. ...summary.results.map(item => `| ${escapeCell(item.id)} | ${item.status} | ${escapeCell(item.output || '')} | ${escapeCell(item.command)} |`),
  201. '',
  202. '## 下一步动作',
  203. '',
  204. summary.nextActions.length
  205. ? '| 优先级 | 负责人 | 动作 | 触发步骤 | 执行命令 | 预期产物 | 通过标准 |\n| --- | --- | --- | --- | --- | --- | --- |\n' + summary.nextActions.map(item => `| ${item.priority} | ${escapeCell(item.owner)} | ${escapeCell(item.title)} | ${escapeCell(item.step)} | \`${escapeCell(item.command)}\` | ${escapeCell(item.expectedArtifact)} | ${escapeCell(item.acceptance)} |`).join('\n')
  206. : '- 暂无后续动作。',
  207. '',
  208. '## 边界',
  209. '',
  210. '- pipeline 只编排已有脚本,不自动写入 token,也不默认启动真实 live 长跑。',
  211. '- 没有客户选择、历史人工补号基线或本轮人工补号量时,不能宣称客户效果完成。',
  212. '- `readyForClaim=false` 时,只能说明链路入口或局部验收完成。'
  213. ];
  214. return lines.join('\n');
  215. }
  216. function buildNextActions(results, context, { dryRun }) {
  217. if (dryRun) {
  218. return [{
  219. priority: 1,
  220. owner: '技术/AI',
  221. title: '确认输入后执行真实 pipeline',
  222. step: 'dry-run',
  223. command: rerunCommandWithoutDryRun(context),
  224. expectedArtifact: path.join(context.outputRoot, 'optimization-pipeline-summary.json'),
  225. acceptance: 'dryRun=false,且关键步骤不再是 planned。'
  226. }];
  227. }
  228. const actions = [];
  229. for (const result of results) {
  230. if (result.status === 'pass') continue;
  231. actions.push(actionForFailedStep(result.id, context, result));
  232. if (context.inputs.currentManualSupplementCount && result.id === 'customer-effect-audit') continue;
  233. }
  234. if (!context.inputs.currentManualSupplementCount && context.inputs.reviewCsv) {
  235. actions.push({
  236. priority: 2,
  237. owner: '商务',
  238. title: '补本轮人工补号量',
  239. step: 'customer-effect-audit',
  240. command: '补齐 --current-manual-supplement-count <本轮人工补号量> 后重跑 optimization:pipeline',
  241. expectedArtifact: '<客户效果输出目录>/customer-effect-summary.json',
  242. acceptance: 'customer-effect:audit 输出 manualSupplementReductionRate,且 strict 审计通过。'
  243. });
  244. }
  245. if (!actions.length && !results.every(item => item.status === 'pass')) {
  246. actions.push({
  247. priority: 99,
  248. owner: '技术/AI',
  249. title: '检查 pipeline 未通过步骤',
  250. step: 'pipeline',
  251. command: '打开 optimization-pipeline-report.md,按首个 fail 步骤处理。',
  252. expectedArtifact: 'optimization-pipeline-summary.json',
  253. acceptance: '所有 pipeline 步骤 status=pass。'
  254. });
  255. }
  256. const closureResult = results.find(item => item.id === 'proof-gap-closure');
  257. const closureSummary = closureResult?.status === 'pass' ? readJson(closureResult.output) : null;
  258. if (!actions.length && closureSummary && !isClaimableProofGapClosure(closureSummary)) {
  259. actions.push({
  260. priority: 2,
  261. owner: '技术/AI',
  262. title: '关闭真实证明缺口',
  263. step: 'proof-gap-closure',
  264. command: '按 proof-gap-closure-report.md 补齐 open 项后重跑 optimization:pipeline。',
  265. expectedArtifact: closureResult.output || '<输出目录>/proof-gap-closure/proof-gap-closure-summary.json',
  266. acceptance: 'proof-gap:closure complete=true、openCount=0、closedCount>=5,且 businessGapCount>=5。'
  267. });
  268. }
  269. if (!actions.length && results.every(item => item.status === 'pass') && !context.inputs.reviewCsv) {
  270. actions.push({
  271. priority: 3,
  272. owner: '商务',
  273. title: '补人工复核 CSV 后验证客户效果',
  274. step: 'review-metrics',
  275. command: 'npm run optimization:pipeline -- --data-pack outputs\\data-intake-pack-latest --video-pack outputs\\video-intake-pack-latest --current-manual-supplement-count <本轮人工补号量> --output <输出目录> --strict',
  276. expectedArtifact: '<输出目录>/customer-effect-audit/customer-effect-summary.json',
  277. acceptance: 'review:metrics 和 customer-effect:audit 均通过。'
  278. });
  279. }
  280. return dedupeActions(actions).sort((a, b) => a.priority - b.priority);
  281. }
  282. function actionForFailedStep(stepId, context, result) {
  283. const base = {
  284. priority: 10,
  285. owner: '技术/AI',
  286. title: `修复 ${stepId}`,
  287. step: stepId,
  288. command: result.command,
  289. expectedArtifact: result.output || '',
  290. acceptance: `${stepId} status=pass。`
  291. };
  292. const map = {
  293. 'intake-readiness': {
  294. priority: 1,
  295. owner: '商务/投放',
  296. title: '修复真实材料收集包',
  297. command: '按 intake-readiness-report.md 的问题修正 data-pack/video-pack 后重跑 optimization:pipeline。',
  298. acceptance: 'intake-readiness overallReady=true 且 failureCount=0。'
  299. },
  300. 'history-from-csv': {
  301. priority: 2,
  302. owner: '商务',
  303. title: '修复历史数据 CSV',
  304. command: '检查 history-data-template.csv 表头、编码和必填字段后重跑 history:from-csv。',
  305. acceptance: 'history-dataset JSON 成功生成。'
  306. },
  307. 'history-audit': {
  308. priority: 3,
  309. owner: '商务',
  310. title: '补齐历史数据审计字段',
  311. command: '补齐真实 Brief、参考链接、人工名单、客户选择、拒绝原因和历史人工补号量基线后重跑 history:audit --strict。',
  312. acceptance: 'historical-dataset-audit readyForCustomerEffectProof=true。'
  313. },
  314. 'video-resource-readiness': {
  315. priority: 4,
  316. owner: '商务/投放',
  317. title: '补齐真实视频资源',
  318. command: '补齐真实参考视频、真实候选视频、视频 URL、封面、ASR、帧图或正文证据后重跑 video:resource-readiness --strict。',
  319. acceptance: 'video-resource-readiness readyForVideoAbPreflight=true。'
  320. },
  321. 'longrun-readiness': {
  322. priority: 5,
  323. owner: '技术/AI',
  324. title: '修复长跑前门禁',
  325. command: '根据 long-run-readiness-report.md 补 live preflight、history audit、intake readiness、video readiness 或 proof gap 后重跑 longrun:readiness。',
  326. acceptance: 'longrun:readiness ready=true 且 fail=0。'
  327. },
  328. 'review-metrics': {
  329. priority: 6,
  330. owner: '商务',
  331. title: '补齐人工复核标注',
  332. command: '补齐人工复核标签、客户选择和负样本归因后重跑 review:metrics --strict。',
  333. acceptance: 'review:metrics overallPass=true,负样本归因覆盖率=100%。'
  334. },
  335. 'customer-effect-audit': {
  336. priority: 7,
  337. owner: '商务',
  338. title: '补齐客户效果证明',
  339. command: '补齐客户最终选择、历史人工补号量基线和本轮人工补号量后重跑 customer-effect:audit --strict。',
  340. acceptance: 'customer-effect:audit overallPass=true,客户选中率和人工补号减少率达标。'
  341. },
  342. 'evidence-index': {
  343. priority: 20,
  344. owner: '技术/AI',
  345. title: '刷新证据台账',
  346. command: 'npm run evidence:index -- --output outputs\\evidence-index-latest',
  347. acceptance: 'evidence-index-summary.json 成功生成,且不把 smoke 当业务证明。'
  348. }
  349. };
  350. return { ...base, ...(map[stepId] || {}) };
  351. }
  352. function rerunCommandWithoutDryRun(context) {
  353. const parts = ['npm run optimization:pipeline --'];
  354. if (context.inputs.dataPack) parts.push(`--data-pack ${context.inputs.dataPack}`);
  355. if (context.inputs.videoPack) parts.push(`--video-pack ${context.inputs.videoPack}`);
  356. if (!context.inputs.dataPack && context.inputs.historyCsv) parts.push(`--history-csv ${context.inputs.historyCsv}`);
  357. if (!context.inputs.dataPack && context.inputs.reviewCsv) parts.push(`--review-csv ${context.inputs.reviewCsv}`);
  358. if (context.inputs.historyMinBriefs) parts.push(`--history-min-briefs ${context.inputs.historyMinBriefs}`);
  359. if (context.inputs.currentManualSupplementCount) parts.push(`--current-manual-supplement-count ${context.inputs.currentManualSupplementCount}`);
  360. parts.push(`--output ${context.outputRoot}`);
  361. if (context.strict) parts.push('--strict');
  362. return parts.join(' ');
  363. }
  364. function dedupeActions(actions) {
  365. const seen = new Set();
  366. return actions.filter(action => {
  367. const key = `${action.owner}|${action.title}|${action.step}`;
  368. if (seen.has(key)) return false;
  369. seen.add(key);
  370. return true;
  371. });
  372. }
  373. function countStatuses(results) {
  374. return results.reduce((acc, item) => {
  375. acc[item.status] = (acc[item.status] || 0) + 1;
  376. return acc;
  377. }, {});
  378. }
  379. function parseArgs(argv) {
  380. const args = {};
  381. for (let index = 0; index < argv.length; index += 1) {
  382. const raw = argv[index];
  383. if (!raw.startsWith('--')) continue;
  384. const key = raw.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase());
  385. const next = argv[index + 1];
  386. if (!next || next.startsWith('--')) args[key] = true;
  387. else {
  388. args[key] = next;
  389. index += 1;
  390. }
  391. }
  392. return args;
  393. }
  394. function resolveOptional(value) {
  395. return value ? path.resolve(value) : '';
  396. }
  397. function safeTail(value) {
  398. return String(value || '').slice(-1200);
  399. }
  400. function readJson(file) {
  401. if (!file || !fs.existsSync(file)) return null;
  402. try {
  403. return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
  404. } catch {
  405. return null;
  406. }
  407. }
  408. function isClaimableProofGapClosure(summary) {
  409. return Boolean(
  410. summary?.complete === true &&
  411. Number(summary?.openCount || 0) === 0 &&
  412. Number(summary?.closedCount || 0) >= 5 &&
  413. Number(summary?.businessGapCount || 0) >= 5
  414. );
  415. }
  416. function escapeCell(value) {
  417. return String(value || '').replace(/\|/g, '/').replace(/\r?\n/g, ' ');
  418. }
  419. function withBom(text) {
  420. return `\uFEFF${text}`;
  421. }
  422. if (require.main === module) {
  423. try {
  424. main();
  425. } catch (error) {
  426. console.error(error && error.stack ? error.stack : String(error));
  427. process.exit(1);
  428. }
  429. }
  430. module.exports = {
  431. buildContext,
  432. buildSteps,
  433. isClaimableProofGapClosure
  434. };