validate-output-standard.js 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const {
  5. PACKAGE_ROOT,
  6. OUTPUT_CATEGORIES,
  7. OUTPUT_PERSISTENT_STORES,
  8. outputsRoot
  9. } = require('../mcp/src/core/output-paths');
  10. const DOCS_ALLOWED = new Set(['OUTPUT-STANDARD.md', 'DASHBOARD-DEV-LOG.md', 'RELEASE.md', 'specs', 'guides', 'generated']);
  11. const RUN_DIR_RE = /^\d{6}-[a-z0-9\u4e00-\u9fa5-]+$/;
  12. const DATE_DIR_RE = /^\d{4}-\d{2}-\d{2}$/;
  13. const problems = [];
  14. function listEntries(dir) {
  15. if (!fs.existsSync(dir)) return [];
  16. return fs.readdirSync(dir, { withFileTypes: true });
  17. }
  18. function checkOutputs() {
  19. const root = outputsRoot();
  20. for (const entry of listEntries(root)) {
  21. if (!entry.isDirectory()) {
  22. problems.push(`outputs 根目录不允许直接放文件:outputs/${entry.name}`);
  23. continue;
  24. }
  25. if (!OUTPUT_CATEGORIES.includes(entry.name)) {
  26. problems.push(`outputs 下存在未注册类别:outputs/${entry.name}(允许:${OUTPUT_CATEGORIES.join(', ')})`);
  27. continue;
  28. }
  29. checkCategory(path.join(root, entry.name), entry.name);
  30. }
  31. }
  32. function checkCategory(dir, category) {
  33. const persistentStores = new Set(OUTPUT_PERSISTENT_STORES[category] || []);
  34. for (const entry of listEntries(dir)) {
  35. if (!entry.isDirectory()) continue; // latest 模式文件
  36. if (persistentStores.has('*') || persistentStores.has(entry.name)) continue; // 注册的持续知识库
  37. if (!DATE_DIR_RE.test(entry.name)) {
  38. problems.push(`outputs/${category}/${entry.name} 不是 YYYY-MM-DD 日期目录`);
  39. continue;
  40. }
  41. const dateDir = path.join(dir, entry.name);
  42. for (const run of listEntries(dateDir)) {
  43. const rel = `outputs/${category}/${entry.name}/${run.name}`;
  44. if (!run.isDirectory()) {
  45. problems.push(`${rel} 应为 run 目录而不是文件`);
  46. continue;
  47. }
  48. if (!RUN_DIR_RE.test(run.name)) {
  49. problems.push(`${rel} 不符合 HHmmss-<slug> 命名`);
  50. }
  51. if (!fs.existsSync(path.join(dateDir, run.name, 'manifest.json'))) {
  52. problems.push(`${rel} 缺少 manifest.json`);
  53. }
  54. }
  55. }
  56. }
  57. function checkDocs() {
  58. const docsDir = path.join(PACKAGE_ROOT, 'docs');
  59. for (const entry of listEntries(docsDir)) {
  60. if (!DOCS_ALLOWED.has(entry.name)) {
  61. problems.push(`docs 第一层存在未注册条目:docs/${entry.name}(允许:${[...DOCS_ALLOWED].join(', ')})`);
  62. }
  63. }
  64. }
  65. checkOutputs();
  66. checkDocs();
  67. if (problems.length) {
  68. console.error('输出目录标准校验失败:');
  69. for (const problem of problems) console.error(` - ${problem}`);
  70. process.exit(1);
  71. }
  72. console.log('输出目录标准校验通过(outputs 与 docs 均符合 docs/OUTPUT-STANDARD.md)。');