diff --git a/.claude/hooks/check-redlines.js b/.claude/hooks/check-redlines.js
deleted file mode 100644
index 1135584..0000000
--- a/.claude/hooks/check-redlines.js
+++ /dev/null
@@ -1,213 +0,0 @@
-#!/usr/bin/env node
-// 机械红线校验(只覆盖“可机器判定、低误报”的硬红线;判断类规范不在此列)。
-//
-// 运行模式:
-// (默认,PreToolUse 钩子)读 stdin 工具调用 JSON,命中即 deny 阻止 Write/Edit。
-// --staged (pre-commit·严格)扫暂存文件“整文件内容”,命中即非零退出、阻断提交。
-// --staged-diff (pre-commit·增量)只查本次“新增行”,老代码存量违规不阻塞——适合已有
-// 开发量的老项目回迁:新代码从此强制合规、旧债日后碰到再还。
-// --scan-all (报告)扫全仓库可编辑正式代码的 ES5 存量违规,只打印、退出 0、不阻断。
-//
-// 覆盖:A. 可编辑范围(路径级,禁改平台/vendor/00_Surface/02_Input/server 平台,app.js 例外)
-// B. 严格 ES5(运行时正式 .js 禁箭头/模板串/class/let/const;测试脚本放行现代语法)
-// 本脚本自身是 Node 工具,不受被检规则约束。
-
-var fs = require('fs');
-var cp = require('child_process');
-
-function norm(p) { return String(p || '').replace(/\\/g, '/'); }
-// 归一为带前导斜杠,使绝对路径与仓库相对路径统一可匹配。
-function m(p) { return '/' + norm(p).replace(/^\/+/, ''); }
-
-function forbiddenReason(p) {
- p = m(p);
- if (/\/js\/vendor\//.test(p)) return '第三方/引擎 js/vendor/ 禁止修改';
- if (/\/js\/00_Surface\//.test(p)) return '平台代码 js/00_Surface/ 禁止修改';
- if (/\/02_SubGame_Input\.js$/.test(p)) return '受限契约文件 02_SubGame_Input.js 完全不碰';
- if (/\/server\/packet\.js$/.test(p)) return '服务端收包入口 server/packet.js 禁止修改';
- if (/\/server\/applist\.js$/.test(p)) return '服务端应用注册表 server/applist.js 禁止修改';
- if (/\/server\/class\//.test(p)) return '平台三层路由基类 server/class/ 禁止修改';
- if (/\/server\/youle\//.test(p) && !/\/server\/youle\/app\.js$/.test(p))
- return 'youle 平台代码 server/youle/ 禁止修改(仅 app.js 为接入注册点例外)';
- return null;
-}
-
-function isRuntimeJs(p) {
- p = m(p);
- if (!/\.js$/.test(p)) return false;
- if (/\.min\.js$/.test(p)) return false;
- if (/\/(node_modules|\.claude|docs|client\/generated)\//.test(p)) return false;
- if (/\/js\/vendor\//.test(p) || /\/js\/00_Surface\//.test(p)) return false;
- if (/(^|\/)tests?\//.test(p) || /\.(test|spec)\.js$/.test(p)) return false; // 测试可用现代语法
- return /\/client\/js\//.test(p) || /\/server\//.test(p);
-}
-
-// 去块注释(保留换行以维持行号),逐行去行注释与 '..'/".." 字符串(保留反引号以检测模板串)。
-function stripForScan(src) {
- src = src.replace(/\/\*[\s\S]*?\*\//g, function (mm) { return mm.replace(/[^\n]/g, ' '); });
- return src.split('\n').map(function (line) {
- var s = line.replace(/\/\/.*$/, '');
- s = s.replace(/'(?:\\.|[^'\\])*'/g, "''");
- s = s.replace(/"(?:\\.|[^"\\])*"/g, '""');
- return s;
- });
-}
-
-var ES5_RULES = [
- { re: /=>/, msg: '箭头函数(=>)——请用 function' },
- { re: /`/, msg: '模板字符串(反引号)——请用字符串拼接' },
- { re: /\bclass\s+[A-Za-z_$]/, msg: 'class 声明——请用 Object.create 继承' },
- { re: /\b(?:let|const)\s+[A-Za-z_$[{]/, msg: 'let/const 声明——请用 var' }
-];
-
-// 扫描已 strip 的行数组;onlyLines 为 Set 时只查这些行号(1 基),null=全查。
-function scanStripped(lines, onlyLines) {
- var out = [];
- for (var i = 0; i < lines.length; i++) {
- if (onlyLines && !onlyLines.has(i + 1)) continue;
- for (var r = 0; r < ES5_RULES.length; r++) {
- if (ES5_RULES[r].re.test(lines[i])) {
- out.push({ line: i + 1, msg: ES5_RULES[r].msg, text: lines[i].trim().slice(0, 80) });
- }
- }
- }
- return out;
-}
-
-// 对「路径 + 完整内容」做检查(PreToolUse 用)。
-function checkOne(p, content) {
- var problems = [];
- var fr = forbiddenReason(p);
- if (fr) problems.push('【可编辑范围】' + fr);
- if (content != null && isRuntimeJs(p)) {
- scanStripped(stripForScan(content), null).forEach(function (v) {
- problems.push('【严格 ES5】第 ' + v.line + ' 行 ' + v.msg + ': ' + v.text);
- });
- }
- return problems;
-}
-
-// ---------- git 辅助(--staged / --staged-diff / --scan-all 用) ----------
-
-function gitStaged() {
- try {
- return cp.execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' })
- .split('\n').map(function (s) { return s.trim(); }).filter(Boolean);
- } catch (e) { return []; }
-}
-function stagedContent(name) {
- try { return cp.execSync('git show ":' + name + '"', { encoding: 'utf8' }); } catch (e) { return null; }
-}
-// 本次暂存 diff 里“新增行”的新文件行号集合。
-function addedLines(name) {
- var set = {}, has = function (n) { return set[n] === true; };
- var out;
- try { out = cp.execSync('git diff --cached -U0 -- "' + name + '"', { encoding: 'utf8' }); }
- catch (e) { return { has: function () { return false; } }; }
- var cur = 0;
- out.split('\n').forEach(function (l) {
- var mh = l.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
- if (mh) { cur = parseInt(mh[1], 10); return; }
- if (l.charAt(0) === '+' && l.substr(0, 3) !== '+++') { set[cur] = true; cur++; return; }
- // '-' 删除行不推进新文件行号;'\' 无换行标记忽略
- });
- return { has: has };
-}
-
-function checkStagedFile(name, diffMode) {
- var probs = [];
- var fr = forbiddenReason(name);
- if (fr) probs.push('【可编辑范围】' + fr);
- if (isRuntimeJs(name)) {
- var content = stagedContent(name);
- if (content != null) {
- var lines = stripForScan(content);
- var only = diffMode ? addedLines(name) : null;
- for (var i = 0; i < lines.length; i++) {
- if (only && !only.has(i + 1)) continue;
- for (var r = 0; r < ES5_RULES.length; r++) {
- if (ES5_RULES[r].re.test(lines[i])) {
- probs.push('【严格 ES5】第 ' + (i + 1) + ' 行 ' + ES5_RULES[r].msg + ': ' + lines[i].trim().slice(0, 80));
- }
- }
- }
- }
- }
- return probs;
-}
-
-function runStaged(diffMode) {
- var all = [];
- gitStaged().forEach(function (name) {
- var probs = checkStagedFile(norm(name), diffMode);
- if (probs.length) all.push(' ' + name + '\n - ' + probs.join('\n - '));
- });
- if (all.length) {
- process.stderr.write('\n✗ 机械红线校验未通过' + (diffMode ? '(仅查本次新增行)' : '') +
- ',提交被阻止:\n' + all.join('\n') +
- '\n\n修正后重新提交(这些是文档记载对应过真实事故的硬红线;不要用 --no-verify 绕过)。\n\n');
- process.exit(1);
- }
- process.exit(0);
-}
-
-// 存量违规报告:只看“可编辑的正式运行时 js”的 ES5 违规;平台文件(本就禁改)不计为存量债。
-function scanAll() {
- var files;
- try { files = cp.execSync('git ls-files', { encoding: 'utf8' }).split('\n').map(function (s) { return s.trim(); }).filter(Boolean); }
- catch (e) { console.log('(非 git 仓库,跳过存量扫描)'); process.exit(0); }
- var scanned = 0, bad = 0, total = 0, rows = [];
- files.forEach(function (name) {
- var p = norm(name);
- if (!isRuntimeJs(p) || forbiddenReason(p)) return;
- var content = null;
- try { content = fs.readFileSync(p, 'utf8'); } catch (e) { return; }
- scanned++;
- var v = scanStripped(stripForScan(content), null);
- if (v.length) { bad++; total += v.length; rows.push(' ' + name + ' (' + v.length + ' 处:第 ' + v.map(function (x) { return x.line; }).join('/') + ' 行)'); }
- });
- console.log('存量 ES5 违规扫描(可编辑正式代码,' + scanned + ' 文件):');
- if (bad) {
- console.log(' 发现 ' + bad + ' 个文件、' + total + ' 处违规(不阻塞,日后碰到逐步还):');
- console.log(rows.join('\n'));
- } else {
- console.log(' ✓ 可编辑正式代码零 ES5 存量违规。');
- }
- process.exit(0);
-}
-
-function runPreToolUse() {
- var chunks = [];
- process.stdin.on('data', function (c) { chunks.push(c); });
- process.stdin.on('end', function () {
- var probs = [];
- try {
- var input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
- var ti = input.tool_input || {};
- var p = norm(ti.file_path || '');
- if (!p) process.exit(0);
- var content = null;
- if (typeof ti.content === 'string') content = ti.content;
- else if (typeof ti.new_string === 'string') content = ti.new_string;
- else if (Array.isArray(ti.edits))
- content = ti.edits.map(function (e) { return e && e.new_string; }).filter(function (s) { return typeof s === 'string'; }).join('\n');
- probs = checkOne(p, content);
- } catch (e) { process.exit(0); }
- if (probs.length) {
- process.stdout.write(JSON.stringify({
- hookSpecificOutput: {
- hookEventName: 'PreToolUse',
- permissionDecision: 'deny',
- permissionDecisionReason: '机械红线校验拦截(改正后重试):\n- ' + probs.join('\n- ')
- }
- }));
- }
- process.exit(0);
- });
-}
-
-var argv = process.argv;
-if (argv.indexOf('--staged-diff') !== -1) runStaged(true);
-else if (argv.indexOf('--staged') !== -1) runStaged(false);
-else if (argv.indexOf('--scan-all') !== -1) scanAll();
-else runPreToolUse();
diff --git a/.claude/hooks/remind-docs.js b/.claude/hooks/remind-docs.js
deleted file mode 100644
index 6d12ed5..0000000
--- a/.claude/hooks/remind-docs.js
+++ /dev/null
@@ -1,105 +0,0 @@
-#!/usr/bin/env node
-// PreToolUse 钩子:编辑 client/** 或 server/** 时,把「必须严格遵守的完整开发规范」注入上下文。
-//
-// 策略(对应用户要求:三套文档全部规范时刻遵守、新会话也一样):
-// - 每个文档目录,每会话「首次」编辑相关区域时全量注入其所有 .md 全文;
-// - 同会话之后再改同区域,只给简短「已注入、继续遵守」提醒,避免重复刷屏;
-// - 会话级去重用 .claude/.spec-injected/ 下的哨兵文件;SessionStart 钩子会在
-// 每个新会话开始时清空该目录 → 新会话必然重新注入。
-// - 改 docs/ 本身不打扰。
-//
-// 本脚本是「工具代码」,只在 Node(Claude Code 宿主)里跑,不受项目 ES5 红线约束。
-
-var fs = require('fs');
-var path = require('path');
-
-var HOOK_DIR = __dirname; // .../.claude/hooks
-var PROJECT_DIR = path.resolve(HOOK_DIR, '..', '..'); // 项目根
-var SENTINEL_DIR = path.join(PROJECT_DIR, '.claude', '.spec-injected');
-
-function slug(relDir) { return relDir.replace(/[^A-Za-z0-9]+/g, '_'); }
-
-function alreadyInjected(relDir) {
- try { return fs.existsSync(path.join(SENTINEL_DIR, slug(relDir))); } catch (e) { return false; }
-}
-
-function markInjected(relDir) {
- try {
- fs.mkdirSync(SENTINEL_DIR, { recursive: true });
- fs.writeFileSync(path.join(SENTINEL_DIR, slug(relDir)), '1');
- } catch (e) {}
-}
-
-// 读取某目录下的编号正文 .md(按文件名排序),拼成一段带分隔标题的全文。
-// 排除 README.md:它已被 CLAUDE.md 用 @import 常驻每次会话上下文,注入里再带就是重复。
-function readDirDocs(relDir) {
- var abs = path.join(PROJECT_DIR, relDir);
- var files;
- try { files = fs.readdirSync(abs); } catch (e) { return ''; }
- var out = [];
- files.filter(function (f) { return /\.md$/i.test(f) && !/^README\.md$/i.test(f); })
- .sort()
- .forEach(function (f) {
- try {
- out.push('===== ' + relDir + '/' + f + ' =====\n' +
- fs.readFileSync(path.join(abs, f), 'utf8'));
- } catch (e) {}
- });
- return out.join('\n\n');
-}
-
-var chunks = [];
-process.stdin.on('data', function (c) { chunks.push(c); });
-process.stdin.on('end', function () {
- var context = '';
- try {
- var input = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
- var p = String((input.tool_input && input.tool_input.file_path) || '').replace(/\\/g, '/');
-
- var dirs = null, area = '';
- if (/\/docs\//.test(p)) {
- dirs = null; // 改文档本身不提醒
- } else if (/\/server\//.test(p)) {
- area = '服务端';
- dirs = ['docs/server/development-guide', 'docs/games/engineering'];
- } else if (/\/client\/js\//.test(p) || /\/codes\/shared\//.test(p)) {
- area = '客户端';
- dirs = ['docs/client/development-guide', 'docs/games/engineering'];
- }
-
- if (dirs) {
- var pending = [];
- for (var i = 0; i < dirs.length; i++) {
- if (!alreadyInjected(dirs[i])) { pending.push(dirs[i]); }
- }
-
- if (pending.length) {
- var full = [];
- pending.forEach(function (d) {
- var txt = readDirDocs(d);
- if (txt) { full.push(txt); }
- markInjected(d);
- });
- context =
- '【必须严格遵守 · ' + area + '开发规范(本会话首次注入编号正文全文)】\n' +
- '你正在修改' + area + '代码。下面是本次及后续所有改动都必须时刻遵守的完整规范正文' +
- '(' + dirs.join('、') + ';各目录 README 的红线速查/一页纸总则已随 CLAUDE.md 常驻上下文,此处不再重复)。' +
- '逐条落实,勿凭记忆越线;dev-guide 与 engineering 冲突时以 dev-guide 硬红线为准。\n\n' +
- full.join('\n\n');
- } else {
- context =
- '【必须严格遵守 · ' + area + '开发规范】本会话已注入完整规范(' + dirs.join('、') +
- ')。继续严格遵守;细节记不清就回看已注入全文或对应编号文档,不要绕过任何红线与规范。';
- }
- }
- } catch (e) {
- context = '';
- }
-
- if (context) {
- process.stdout.write(JSON.stringify({
- hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: context }
- }));
- }
- process.exit(0);
-});
diff --git a/.claude/settings.json b/.claude/settings.json
index 0b3da80..e69de29 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,43 +0,0 @@
-{
- "hooks": {
- "SessionStart": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "rm -rf \"$CLAUDE_PROJECT_DIR/.claude/.spec-injected\""
- }
- ]
- }
- ],
- "PostCompact": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "rm -rf \"$CLAUDE_PROJECT_DIR/.claude/.spec-injected\""
- }
- ]
- }
- ],
- "PreToolUse": [
- {
- "matcher": "Edit|Write|MultiEdit",
- "hooks": [
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-redlines.js\"",
- "timeout": 10,
- "statusMessage": "校验机械红线…"
- },
- {
- "type": "command",
- "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/remind-docs.js\"",
- "timeout": 15,
- "statusMessage": "注入/校验开发规范…"
- }
- ]
- }
- ]
- }
-}
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
deleted file mode 100644
index 8a45653..0000000
--- a/.githooks/pre-commit
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-# 机械红线校验:对本次暂存做「可编辑范围 + 严格 ES5」硬检查,命中即阻断提交。
-# 工具无关——Claude Code、别的编辑器、人手改,提交都过这道闸。
-# 模式(本地 git 配置 redlines.stagedmode):
-# 未设 / 非 diff → 严格:整文件扫描(模板等干净仓库用)
-# diff → 增量:只查本次新增行(已有开发量的老项目回迁用)
-# 启用(每个克隆一次性):git config core.hooksPath .githooks (或跑 setup-redlines.sh)
-MODE="$(git config --get redlines.stagedmode 2>/dev/null)"
-if [ "$MODE" = "diff" ]; then FLAG="--staged-diff"; else FLAG="--staged"; fi
-exec node "$(git rev-parse --show-toplevel)/.claude/hooks/check-redlines.js" "$FLAG"
diff --git a/CLAUDE.md b/CLAUDE.md
index 55ea7b0..af237a4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,12 +1,12 @@
# CLAUDE.md
-本文件为 Claude Code(claude.ai/code)在本仓库中工作时提供指导。**它是入口与强制指引**:三套文档的 README(含各自「红线速查 / 一页纸总则」)已通过 `@import` 常驻上下文(见下「文档」一节),**红线以它们为权威、始终在上下文里,本文件不再另抄一份**;规范的完整细节在 `docs/` 编号文档里,改动前先读对应文档。
+本文件为 Claude Code(claude.ai/code)在本仓库中工作时提供指导。**它是入口与强制指引**:三套文档的「红线速查 / 一页纸总则」单页已通过 `@import` 常驻上下文(见下「文档」一节),**红线以它们为权威、始终在上下文里,本文件不再另抄一份**;规范的完整细节在 `docs/` 编号文档里,改动前先读对应文档。
## 这是一个什么仓库
一个友乐/gameabc 房卡类小游戏平台:浏览器端由私有的 `gameabc.min.js` 2D Canvas 引擎驱动,服务端是一个 Node.js 游戏服务器平台(`youle` 应用),双方通过 WebSocket/HTTP 收发 JSON 包通信。项目根目录**没有 `package.json`、没有 npm 构建/lint/测试流水线**——这是纯静态 JS:客户端靠 `
-
+
+
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+