validate-output-standard.js 3.0 KB

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