zizi f59991b9f3 feat(game-runtime): T1b-α Gate0 七件落地(core-protocol-v0-rc)
- 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>
2026-06-12 11:10:12 +00:00

133 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* scripts/validate-manifest.mjs — 插件 manifest.json 零依赖校验器core-protocol-v0
* ownerT1b-α 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任一不合规=1CI/收口可据此 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.jsonsrc/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);