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

185 lines
8.5 KiB
JavaScript
Raw 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/size.mjs — 发行版字节预算「增量法」测量器core-protocol-v0
*
* 口径(执行版 §5与 channel-spike 同策略,写死不得擅改):
* - 打包:复用 build.mjs 的锁定参数bundle+minify+iife+es2019+sourcemap=false
* - gz**zlib.gzipSync(buf, { level: 9 })**node 内置;等价 `gzip -9`,但纯内置免外部进程);
* - **增量法**:基线壳 → +core → 逐插件叠加,每步实测「当前累计 bundle 的 raw/gz」
* 相邻两步差值 = 该新增件的「增量 raw/gz」树摇后的真实净增非孤立打包值
* - 输出 `SIZES.md`:分项 raw/gz 双列 + 命令注记 + 各插件 manifest 的 gzBudgetBytes 对照(超配额标 ⚠️)。
*
* ⚠️ 运行环境:真实打包需 esbuild → **仅集成段mini-desktop跑**;本机无 esbuild 会报指引。
* gz 量算zlib逻辑本机可独立自检`node scripts/size.mjs --selftest`(不需 esbuild
*
* 用法集成段node scripts/size.mjs → 生成 game-runtime/SIZES.md
* 本机自检 node scripts/size.mjs --selftest
*/
'use strict';
import { gzipSync } from 'node:zlib';
import { writeFileSync, readFileSync, readdirSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { LOCKED_BUILD_OPTIONS } from './build.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
const PLUGINS_DIR = join(ROOT, 'src', 'plugins');
const CORE_ENTRY = join(ROOT, 'src', 'core', 'plugin.js');
/**
* 量算一段 buffer 的 raw 与 gzlevel 9字节数。这是 SIZES 的最底层口径,纯内置可独立测。
* @param {Buffer|Uint8Array|string} content
* @returns {{ raw: number, gz: number }}
*/
export function measureBytes(content) {
const buf = Buffer.isBuffer(content) ? content : Buffer.from(content);
return { raw: buf.length, gz: gzipSync(buf, { level: 9 }).length };
}
/**
* 用锁定参数把一组入口打成「单 iife bundle」并返回其字节数增量法的一档
* 集成段用;需 esbuild。多入口时用一个聚合虚拟入口把它们 import 到一起,量整包累计大小。
* @param {string[]} entries 该档要叠加进来的入口文件(绝对路径)
* @param {object} esbuild 已 import 的 esbuild 模块
* @returns {Promise<{ raw:number, gz:number }>}
*/
async function bundleSize(entries, esbuild) {
if (entries.length === 0) return { raw: 0, gz: 0 };
// 聚合入口:把多个文件 import 进一个临时入口,模拟「整包累计」。
const tmp = mkdtempSync(join(tmpdir(), 'rt-size-'));
try {
const aggregate = join(tmp, 'aggregate.js');
const body = entries.map((e, i) => `import * as m${i} from ${JSON.stringify(e)};`).join('\n')
+ '\n// 防树摇整体消除:把各模块挂到全局副作用点。\n'
+ 'globalThis.__rt_keep = [' + entries.map((_, i) => `m${i}`).join(', ') + '];\n';
writeFileSync(aggregate, body);
const out = await esbuild.build({
...LOCKED_BUILD_OPTIONS,
entryPoints: [aggregate],
bundle: true,
write: false, // 不落盘,直接拿产物 buffer 量字节
globalName: '__RT_SIZE_PROBE',
});
const code = out.outputFiles[0].contents; // Uint8Array
return measureBytes(Buffer.from(code));
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
/** 扫描 src/plugins/<*>(排除 _example 可选)→ 返回 [{name, entry, budget}],按 name 排序稳定。 */
function collectPlugins() {
const list = [];
if (!existsSync(PLUGINS_DIR)) return list;
for (const name of readdirSync(PLUGINS_DIR).sort()) {
const dir = join(PLUGINS_DIR, name);
const mf = join(dir, 'manifest.json');
if (!existsSync(mf)) continue;
const manifest = JSON.parse(readFileSync(mf, 'utf8'));
const impl = join(dir, (manifest.exports && manifest.exports.impl) || 'impl.js');
list.push({ name: manifest.name, dir: name, entry: impl, budget: manifest.gzBudgetBytes });
}
return list;
}
/** 生成 SIZES.md 文本。 */
function renderSizesMd(rows, baseline) {
const lines = [];
lines.push('# SIZES.md —— 发行版字节预算实测增量法core-protocol-v0');
lines.push('');
lines.push('> 由 `scripts/size.mjs` 在集成段mini-desktop生成。**勿手改**,改源后重跑覆盖。');
lines.push('> 口径esbuild `bundle+minify+format=iife+target=es2019+sourcemap=false`');
lines.push('> gz = `zlib.gzipSync(buf,{level:9})`(等价 `gzip -9`**增量法** = 基线壳→+core→逐插件叠加差值为各件净增。');
lines.push('');
lines.push('| 叠加项 | 累计 raw | 累计 gz | 增量 raw | 增量 gz | gz 预算 | 判定 |');
lines.push('|---|--:|--:|--:|--:|--:|:--|');
lines.push(`| 基线壳 | ${baseline.raw} | ${baseline.gz} | — | — | — | 基准 |`);
let totalGz = 0;
for (const r of rows) {
const verdict = r.budget == null ? '—'
: r.deltaGz <= r.budget ? `✔ ≤${r.budget}` : `⚠️ 超 ${r.deltaGz - r.budget}B`;
if (r.budget != null) totalGz += r.deltaGz;
lines.push(`| ${r.label} | ${r.cumRaw} | ${r.cumGz} | ${r.deltaRaw} | ${r.deltaGz} | ${r.budget ?? '—'} | ${verdict} |`);
}
lines.push('');
lines.push(`合计插件 gz 增量 ≈ **${totalGz}B**(自律线 60K超 60K 须创始人显式放行,执行版 §5`);
lines.push('');
return lines.join('\n') + '\n';
}
/** --selftest仅验 zlib gz 量算口径(不需 esbuild本机可跑留证。 */
function selftest() {
const cases = ['', 'a', 'hello world', 'x'.repeat(1000)];
console.log('[size --selftest] zlib gzipSync(level:9) 量算自检:');
for (const s of cases) {
const m = measureBytes(s);
// 基本不变式raw=字节长度gz>0gzip 头即非零gz 对高度可压缩串应远小于 raw。
if (m.raw !== Buffer.from(s).length) throw new Error('raw 口径错');
if (m.gz <= 0) throw new Error('gz 应 >0');
console.log(` len=${String(m.raw).padStart(4)} raw=${m.raw} gz=${m.gz}`);
}
// 可压缩性断言1000 个相同字符 gz 必远小于 raw。
const big = measureBytes('x'.repeat(1000));
if (!(big.gz < big.raw / 5)) throw new Error('高度可压缩串 gz 未显著小于 raw口径可疑');
console.log('[size --selftest] OKgz 量算口径正确(高压缩串 gz << raw。');
}
// ── 主流程 ──────────────────────────────────────────────────────────────────
async function main() {
if (process.argv.includes('--selftest')) {
selftest();
return;
}
let esbuild;
try {
esbuild = await import('esbuild');
} catch {
console.error(
'[size] 未找到 esbuild —— 增量法实测仅在集成段mini-desktop跑。\n' +
' 请在集成段执行npm i -D esbuild再重跑生成 SIZES.md。\n' +
' 本机仅可跑 gz 量算自检node scripts/size.mjs --selftest'
);
process.exit(1);
}
// 基线壳本波无独立「空壳」入口以「core 之前」为 0 基线(增量法起点)。
// 若后续有引擎壳入口,可在此把 baseline 换成壳的实测值(口径不变)。
const baseline = { raw: 0, gz: 0 };
// 第一档:+core
const coreSize = await bundleSize([CORE_ENTRY], esbuild);
const rows = [];
rows.push({
label: '+core', cumRaw: coreSize.raw, cumGz: coreSize.gz,
deltaRaw: coreSize.raw - baseline.raw, deltaGz: coreSize.gz - baseline.gz, budget: null,
});
// 逐插件叠加(累计 = core + 已叠插件 + 本插件),差值 = 本插件增量。
const plugins = collectPlugins();
let prev = coreSize;
const cumulativeEntries = [CORE_ENTRY];
for (const p of plugins) {
cumulativeEntries.push(p.entry);
const cur = await bundleSize(cumulativeEntries, esbuild);
rows.push({
label: `+${p.name}`, cumRaw: cur.raw, cumGz: cur.gz,
deltaRaw: cur.raw - prev.raw, deltaGz: cur.gz - prev.gz, budget: p.budget,
});
prev = cur;
}
const md = renderSizesMd(rows, baseline);
const outPath = join(ROOT, 'SIZES.md');
writeFileSync(outPath, md);
console.log(`[size] 已写 ${outPath}${plugins.length} 插件,增量法)`);
}
main().catch((e) => {
console.error('[size] 失败:' + (e && e.message ? e.message : e));
process.exit(1);
});