validate-output-standard.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  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_ACCOUNT_SCOPED_CATEGORIES,
  8. OUTPUT_PERSISTENT_STORES,
  9. outputsRoot
  10. } = require('../mcp/src/core/output-paths');
  11. // Keep the documented subdirectories strict, while allowing the checked-in
  12. // project/release notes that live at docs/ root. Runtime artifacts still belong
  13. // under outputs/ and are rejected by the separate outputs checks below.
  14. const DOCS_ALLOWED = new Set([
  15. 'OUTPUT-STANDARD.md',
  16. 'DASHBOARD-DEV-LOG.md',
  17. 'RELEASE.md',
  18. 'AGENT-HOUSE-MIGRATION.md',
  19. 'DELIVERY.md',
  20. 'PROJECT-SUMMARY.md',
  21. 'fix-training-package-token-issue.md',
  22. 'training-package-deployment-guide.md',
  23. 'training-package-verification-report.md',
  24. 'voice-enabled-training-package.md',
  25. 'voice-integration-completion-report.md',
  26. 'voice-integration-test-report.md',
  27. 'voice-test-summary.md',
  28. 'specs',
  29. 'guides',
  30. 'research',
  31. 'generated',
  32. ]);
  33. const RUN_DIR_RE = /^\d{6}-[a-z0-9\u4e00-\u9fa5-]+$/;
  34. const DATE_DIR_RE = /^\d{4}-\d{2}-\d{2}$/;
  35. const ACCOUNT_DIR_RE = /^(?:[a-f0-9]{16}|legacy-[a-z0-9_-]+)$/i;
  36. const problems = [];
  37. function listEntries(dir) {
  38. if (!fs.existsSync(dir)) return [];
  39. return fs.readdirSync(dir, { withFileTypes: true });
  40. }
  41. function checkOutputs() {
  42. const root = outputsRoot();
  43. for (const entry of listEntries(root)) {
  44. if (!entry.isDirectory()) {
  45. problems.push(`outputs 根目录不允许直接放文件:outputs/${entry.name}`);
  46. continue;
  47. }
  48. if (!OUTPUT_CATEGORIES.includes(entry.name)) {
  49. problems.push(`outputs 下存在未注册类别:outputs/${entry.name}(允许:${OUTPUT_CATEGORIES.join(', ')})`);
  50. continue;
  51. }
  52. checkCategory(path.join(root, entry.name), entry.name);
  53. }
  54. }
  55. function checkCategory(dir, category) {
  56. const persistentStores = new Set(OUTPUT_PERSISTENT_STORES[category] || []);
  57. const accountScoped = OUTPUT_ACCOUNT_SCOPED_CATEGORIES.includes(category);
  58. for (const entry of listEntries(dir)) {
  59. if (!entry.isDirectory()) continue; // latest 模式文件
  60. if (persistentStores.has('*') || persistentStores.has(entry.name)) continue; // 注册的持续知识库
  61. if (accountScoped && ACCOUNT_DIR_RE.test(entry.name)) continue; // 账号隔离的 latest/persistent 数据
  62. if (!DATE_DIR_RE.test(entry.name)) {
  63. problems.push(`outputs/${category}/${entry.name} 不是 YYYY-MM-DD 日期目录`);
  64. continue;
  65. }
  66. const dateDir = path.join(dir, entry.name);
  67. for (const run of listEntries(dateDir)) {
  68. const rel = `outputs/${category}/${entry.name}/${run.name}`;
  69. if (!run.isDirectory()) {
  70. problems.push(`${rel} 应为 run 目录而不是文件`);
  71. continue;
  72. }
  73. if (!RUN_DIR_RE.test(run.name)) {
  74. problems.push(`${rel} 不符合 HHmmss-<slug> 命名`);
  75. }
  76. if (!fs.existsSync(path.join(dateDir, run.name, 'manifest.json'))) {
  77. problems.push(`${rel} 缺少 manifest.json`);
  78. }
  79. }
  80. }
  81. }
  82. function checkDocs() {
  83. const docsDir = path.join(PACKAGE_ROOT, 'docs');
  84. for (const entry of listEntries(docsDir)) {
  85. if (!DOCS_ALLOWED.has(entry.name)) {
  86. problems.push(`docs 第一层存在未注册条目:docs/${entry.name}(允许:${[...DOCS_ALLOWED].join(', ')})`);
  87. }
  88. }
  89. }
  90. checkOutputs();
  91. checkDocs();
  92. if (problems.length) {
  93. console.error('输出目录标准校验失败:');
  94. for (const problem of problems) console.error(` - ${problem}`);
  95. process.exit(1);
  96. }
  97. console.log('输出目录标准校验通过(outputs 与 docs 均符合 docs/OUTPUT-STANDARD.md)。');