144 lines
5.4 KiB
JavaScript
144 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* function-manifest 校验器 (零依赖)
|
|
*
|
|
* 校验:
|
|
* 1. JSON 合法
|
|
* 2. 必填字段齐全 (ZY-FN/ZY-SVR 编号格式)
|
|
* 3. 引用一致性: function.host_server 必须存在于 servers
|
|
* function.modules 必须出现在 modules_index.id (如有 modules_index)
|
|
* active_routes.*.active_fn 必须存在于 functions
|
|
* 4. 中文名/动机字段非空 (created_for / display_name_zh)
|
|
*
|
|
* 用法:
|
|
* node scripts/manifest/validate.js
|
|
* node scripts/manifest/validate.js --strict (失败退出 1)
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
const MANIFEST_PATH = path.join(REPO_ROOT, '.github/brain/architecture/function-manifest.json');
|
|
const SCHEMA_PATH = path.join(REPO_ROOT, '.github/brain/architecture/function-manifest.schema.json');
|
|
|
|
const RE_SVR = /^ZY-SVR-[A-Z0-9]+$/;
|
|
const RE_FN = /^ZY-FN-[A-Z0-9]+$/;
|
|
const RE_M = /^M-[A-Z0-9-]+$/;
|
|
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
function err(msg) { errors.push(msg); }
|
|
function warn(msg) { warnings.push(msg); }
|
|
|
|
let manifest;
|
|
try {
|
|
manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
} catch (e) {
|
|
console.error('❌ manifest 解析失败:', e.message);
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
} catch (e) {
|
|
err('schema 解析失败: ' + e.message);
|
|
}
|
|
|
|
const servers = Array.isArray(manifest.servers) ? manifest.servers : [];
|
|
const functions = Array.isArray(manifest.functions) ? manifest.functions : [];
|
|
const modulesIndex = Array.isArray(manifest.modules_index) ? manifest.modules_index : [];
|
|
const activeRoutes = manifest.active_routes && typeof manifest.active_routes === 'object' ? manifest.active_routes : {};
|
|
|
|
// 1. servers
|
|
const serverIds = new Set();
|
|
for (const s of servers) {
|
|
if (!s.id || !RE_SVR.test(s.id)) err(`server id 不合法: ${JSON.stringify(s.id)}`);
|
|
if (serverIds.has(s.id)) err(`server id 重复: ${s.id}`);
|
|
serverIds.add(s.id);
|
|
if (!s.display_name_zh) err(`server ${s.id} 缺少 display_name_zh`);
|
|
if (!s.status) err(`server ${s.id} 缺少 status`);
|
|
if (!s.template_version) warn(`server ${s.id} 缺少 template_version`);
|
|
}
|
|
|
|
// 2. functions
|
|
const fnIds = new Set();
|
|
const moduleIds = new Set(modulesIndex.map((m) => m.id));
|
|
for (const fn of functions) {
|
|
if (!fn.id || !RE_FN.test(fn.id)) err(`function id 不合法: ${JSON.stringify(fn.id)}`);
|
|
if (fnIds.has(fn.id)) err(`function id 重复: ${fn.id}`);
|
|
fnIds.add(fn.id);
|
|
if (!fn.display_name_zh) err(`function ${fn.id} 缺少 display_name_zh (人类记忆主键, 必填)`);
|
|
if (!fn.created_for) err(`function ${fn.id} 缺少 created_for (开发动机, 必填)`);
|
|
if (!fn.host_server) err(`function ${fn.id} 缺少 host_server`);
|
|
else if (!serverIds.has(fn.host_server)) err(`function ${fn.id} host_server "${fn.host_server}" 未登记`);
|
|
if (!fn.domain) warn(`function ${fn.id} 缺少 domain`);
|
|
if (!fn.status) err(`function ${fn.id} 缺少 status`);
|
|
if (!Array.isArray(fn.modules)) err(`function ${fn.id} modules 必须是数组`);
|
|
else {
|
|
for (const mid of fn.modules) {
|
|
if (!RE_M.test(mid)) err(`function ${fn.id} 模块编号不合法: ${mid}`);
|
|
if (modulesIndex.length && !moduleIds.has(mid)) {
|
|
warn(`function ${fn.id} 引用模块 ${mid} 未在 modules_index 登记`);
|
|
}
|
|
}
|
|
}
|
|
if (!Array.isArray(fn.aliases) || fn.aliases.length === 0) {
|
|
warn(`function ${fn.id} 缺少 aliases (自然语言匹配会变弱)`);
|
|
}
|
|
}
|
|
|
|
// 3. active_routes
|
|
for (const [domain, route] of Object.entries(activeRoutes)) {
|
|
if (!route || typeof route !== 'object') {
|
|
err(`active_routes[${domain}] 不是对象`);
|
|
continue;
|
|
}
|
|
if (!route.active_fn) err(`active_routes[${domain}] 缺少 active_fn`);
|
|
else if (!fnIds.has(route.active_fn)) err(`active_routes[${domain}].active_fn "${route.active_fn}" 未登记`);
|
|
if (route.host_server && !serverIds.has(route.host_server)) {
|
|
err(`active_routes[${domain}].host_server "${route.host_server}" 未登记`);
|
|
}
|
|
// host_server 一致性
|
|
const fn = functions.find((f) => f.id === route.active_fn);
|
|
if (fn && route.host_server && fn.host_server !== route.host_server) {
|
|
err(`active_routes[${domain}] host_server 与 function ${fn.id} 不一致 (route=${route.host_server}, fn=${fn.host_server})`);
|
|
}
|
|
}
|
|
|
|
// 4. registered_functions vs host_server 反查
|
|
for (const s of servers) {
|
|
const expected = functions.filter((f) => f.host_server === s.id).map((f) => f.id).sort();
|
|
const declared = (s.registered_functions || []).slice().sort();
|
|
if (JSON.stringify(expected) !== JSON.stringify(declared)) {
|
|
warn(`server ${s.id} registered_functions 与 functions[].host_server 反查不一致 (declared=${declared.join(',')||'-'}, actual=${expected.join(',')||'-'})`);
|
|
}
|
|
}
|
|
|
|
const strict = process.argv.includes('--strict');
|
|
const summary = {
|
|
manifest_path: MANIFEST_PATH,
|
|
servers: servers.length,
|
|
functions: functions.length,
|
|
modules_index: modulesIndex.length,
|
|
errors: errors.length,
|
|
warnings: warnings.length
|
|
};
|
|
|
|
if (errors.length) {
|
|
console.error('❌ manifest 校验失败:');
|
|
errors.forEach((e) => console.error(' ✗ ' + e));
|
|
}
|
|
if (warnings.length) {
|
|
console.warn('⚠ 警告:');
|
|
warnings.forEach((w) => console.warn(' ! ' + w));
|
|
}
|
|
console.log(JSON.stringify(summary, null, 2));
|
|
|
|
if (errors.length) {
|
|
process.exit(1);
|
|
}
|
|
if (strict && warnings.length) process.exit(1);
|
|
process.exit(0);
|