- core 协议:Plugin 接口/注册器(重名拒绝/生命周期正序逆序/dispose幂等/错误隔离不连坐)+受控面 PluginContext(禁直透 littlejsengine)——前任agent核心设计17测绿收编(死于收尾的工具调用解析失败,续作字节级未碰) - _example 空插件样例五件全(克隆母本)+PLUGIN-TEMPLATE 六节规格+manifest schema+零依赖校验器(负例自检逮6违规) - scripts:esbuild锁参build/增量法size/test.sh遍历;browser-evidence harness纯计算口径冻结(FNV-1a/直方图/几何,CDP四函数=集成段占位) - 22/22 测全绿(RESULT.txt 留档);ESM JS+JSDoc+手写d.ts 零工具链纪律全程 - rc 状态:待 Gate0.1 受控面补丁(input/audio+per-plugin派生RNG+useContext守卫)后冻结 core-protocol-v0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
133 lines
5.5 KiB
JavaScript
133 lines
5.5 KiB
JavaScript
/**
|
||
* scripts/validate-manifest.mjs — 插件 manifest.json 零依赖校验器(core-protocol-v0)
|
||
* owner:T1b-α lane-core | 运行:node scripts/validate-manifest.mjs [<manifest 路径>...]
|
||
* 缺省:自动扫描 src/plugins/<*>/manifest.json 全部校验。
|
||
*
|
||
* 【为什么不引 ajv】
|
||
* 本机零 npm 依赖纪律(执行版 §1)。draft-07 全量校验需 ajv —— 违纪。本脚本只对
|
||
* `schema/plugin-manifest.schema.json` 的**关键约束**做手写校验(required / additionalProperties:false /
|
||
* type / pattern / minimum-maximum / enum-uniqueItems),覆盖该 schema 实际用到的全部规则,
|
||
* 足够把「manifest 是否合契约」判明并留证。集成段如需全量 draft-07 校验可另接 ajv(属工具链,集成段跑)。
|
||
*
|
||
* 退出码:全部通过=0;任一不合规=1(CI/收口可据此 gate)。
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join, resolve } from 'node:path';
|
||
|
||
// 定位仓内路径(脚本在 game-runtime/scripts/ 下,根 = 上一级)。
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = resolve(__dirname, '..');
|
||
const SCHEMA_PATH = join(ROOT, 'schema', 'plugin-manifest.schema.json');
|
||
const PLUGINS_DIR = join(ROOT, 'src', 'plugins');
|
||
|
||
// 读 schema(仅取本校验器需要的字段;schema 是单一事实源,约束改动须同步本校验器)。
|
||
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
||
|
||
/**
|
||
* 针对一个对象按(子)schema 做关键约束校验,收集错误到 errs。
|
||
* @param {*} value 待校验值
|
||
* @param {object} sch (子)schema
|
||
* @param {string} path 字段路径(错误定位用)
|
||
* @param {string[]} errs 错误累加数组
|
||
*/
|
||
function validate(value, sch, path, errs) {
|
||
// type 校验(支持 object/array/string/integer)
|
||
if (sch.type === 'object') {
|
||
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
||
errs.push(`${path}: 应为 object`);
|
||
return;
|
||
}
|
||
// required
|
||
for (const key of sch.required || []) {
|
||
if (!(key in value)) errs.push(`${path}: 缺少必填字段 "${key}"`);
|
||
}
|
||
// additionalProperties:false → 不允许未声明字段
|
||
if (sch.additionalProperties === false) {
|
||
const allowed = new Set(Object.keys(sch.properties || {}));
|
||
for (const k of Object.keys(value)) {
|
||
if (!allowed.has(k)) errs.push(`${path}: 出现未声明字段 "${k}"(additionalProperties:false)`);
|
||
}
|
||
}
|
||
// 递归校验各属性
|
||
for (const [k, subSch] of Object.entries(sch.properties || {})) {
|
||
if (k in value) validate(value[k], subSch, path === '$' ? k : `${path}.${k}`, errs);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (sch.type === 'array') {
|
||
if (!Array.isArray(value)) { errs.push(`${path}: 应为 array`); return; }
|
||
if (sch.uniqueItems && new Set(value).size !== value.length) {
|
||
errs.push(`${path}: 数组元素须唯一(uniqueItems)`);
|
||
}
|
||
value.forEach((item, i) => validate(item, sch.items || {}, `${path}[${i}]`, errs));
|
||
return;
|
||
}
|
||
|
||
if (sch.type === 'string') {
|
||
if (typeof value !== 'string') { errs.push(`${path}: 应为 string`); return; }
|
||
if (sch.minLength != null && value.length < sch.minLength) errs.push(`${path}: 长度 < ${sch.minLength}`);
|
||
if (sch.maxLength != null && value.length > sch.maxLength) errs.push(`${path}: 长度 > ${sch.maxLength}`);
|
||
if (sch.pattern && !new RegExp(sch.pattern).test(value)) {
|
||
errs.push(`${path}: 不匹配 pattern /${sch.pattern}/(值="${value}")`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (sch.type === 'integer') {
|
||
if (typeof value !== 'number' || !Number.isInteger(value)) { errs.push(`${path}: 应为 integer`); return; }
|
||
if (sch.minimum != null && value < sch.minimum) errs.push(`${path}: < minimum ${sch.minimum}`);
|
||
if (sch.maximum != null && value > sch.maximum) errs.push(`${path}: > maximum ${sch.maximum}`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
/** 收集待校验的 manifest 路径:命令行给定优先,否则扫描 src/plugins/<*>/manifest.json。 */
|
||
function collectTargets() {
|
||
const argv = process.argv.slice(2);
|
||
if (argv.length > 0) return argv.map((p) => resolve(p));
|
||
const targets = [];
|
||
if (existsSync(PLUGINS_DIR)) {
|
||
for (const name of readdirSync(PLUGINS_DIR)) {
|
||
const mf = join(PLUGINS_DIR, name, 'manifest.json');
|
||
if (existsSync(mf)) targets.push(mf);
|
||
}
|
||
}
|
||
return targets;
|
||
}
|
||
|
||
// ── 主流程 ──────────────────────────────────────────────────────────────────
|
||
const targets = collectTargets();
|
||
if (targets.length === 0) {
|
||
console.error('[validate-manifest] 未找到任何 manifest.json(src/plugins/<*>/manifest.json)');
|
||
process.exit(1);
|
||
}
|
||
|
||
let failed = 0;
|
||
for (const file of targets) {
|
||
const errs = [];
|
||
let data;
|
||
try {
|
||
data = JSON.parse(readFileSync(file, 'utf8'));
|
||
} catch (e) {
|
||
console.error(`✘ ${file}: JSON 解析失败 —— ${e.message}`);
|
||
failed += 1;
|
||
continue;
|
||
}
|
||
validate(data, schema, '$', errs);
|
||
if (errs.length === 0) {
|
||
console.log(`✔ ${file} [${data.name}@${data.version}, gzBudget=${data.gzBudgetBytes}]`);
|
||
} else {
|
||
console.error(`✘ ${file}`);
|
||
for (const e of errs) console.error(` - ${e}`);
|
||
failed += 1;
|
||
}
|
||
}
|
||
|
||
console.log(`\n[validate-manifest] 共 ${targets.length} 件,通过 ${targets.length - failed},失败 ${failed}`);
|
||
process.exit(failed === 0 ? 0 : 1);
|