diff --git a/.planning/phases/09-minimum-browser-runnable/09-49-PLAN.md b/.planning/phases/09-minimum-browser-runnable/09-49-PLAN.md new file mode 100644 index 0000000..8894100 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-49-PLAN.md @@ -0,0 +1,291 @@ +--- +phase: 09-minimum-browser-runnable +plan: 49 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md + - .planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md +autonomous: false +requirements: [] +must_haves: + truths: + - "09-RUNNABILITY-AUDIT.md 用中文回答当前游戏能否运行,并给出最小阻断集与最小可运行路径。" + - "每条 P0 阻断与 P1 集成缺口都有 file:line 证据;预期中的类型错误单独归类,不计为新缺陷。" + - "审计覆盖入口与导入、旧代码与全局 core/main 残留、内容加载、渲染/UI、事件/战斗/寻路/存档、构建与工作区依赖、系统级集成缺口。" + - "只写审计文档与 SUMMARY;不改代码、配置、内容、STATE、ROADMAP;不提交;不运行游戏、浏览器、开发服务器或测试。" + artifacts: + - path: .planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md + provides: "静态可运行性审计:结论、P0/P1/预期中分类、最小可运行路径、证据快照" + - path: .planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md + provides: "基线、计数、tsc 证据摘要、覆盖与范围证明" + key_links: + - "index.html → src/bootstrap.ts → src/client/entry.ts → src/data/entry.ts 启动链" + - "package.json + pnpm-workspace.yaml + vite.config.ts + src/package.json 构建与工作区依赖" + - "git HEAD / 脏文件数 / 采集时间 / 可选 vue-tsc 输出构成同一时间点证据" +--- + + +对当前项目做一次只读的静态可运行性审计,找出距离“游戏真正能在浏览器运行”还缺什么、哪些现有因素可能阻止运行,并产出一份中文人读文档。 + +Purpose: 大多数子系统已确认可独立运行,剩余问题主要是实现与集成;本计划只做静态取证与归因,不把用户正在并行修改接口导致的预期类型错误当作新阻断。 +Output: `.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md` 与 `09-49-SUMMARY.md`。 + + + +@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md + + + +@AGENTS.md +@dev.md +@index.html +@src/bootstrap.ts +@src/client/entry.ts +@src/data/entry.ts +@package.json +@pnpm-workspace.yaml +@vite.config.ts + +- 只读参考:`src/package.json`、`script/dev.ts`、`src/content/**`、`packages-user/**/src/index.ts`、`packages/**/src/index.ts`。 +- 明确排除:`09-PARITY/` 与既有 `09-43` 至 `09-48` 的新旧引擎对照内容一律不读取、不修改、不写入审计;本计划不讨论 parity。 +- 用户正在并发编辑接口,工作树会变化;类型错误和文件内容只代表采集瞬间。 + + + + + + Task 0: 汇报审计范围与方式并等待放行 + + .planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md + .planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md + + + AGENTS.md + dev.md + 09-49-PLAN.md + + + 开始任何分析或写入之前,向用户汇报:① 要做什么——静态审计当前项目能否在浏览器运行、还缺什么、哪些因素会阻止运行;② 怎么做——只读源码与配置,按入口/导入、旧 core/main 残留、内容加载、渲染/UI、事件/战斗/寻路/存档、构建/工作区依赖、系统级集成逐类取证,可选运行一次 `pnpm check:type` 作为时间点证据;③ 影响范围——只新增 `09-RUNNABILITY-AUDIT.md` 与 `09-49-SUMMARY.md`,不改代码、配置、内容、STATE、ROADMAP,不提交,不运行游戏/浏览器/开发服务器/测试;④ 说明用户正在并发编辑接口,预期类型错误不会被当作新阻断;⑤ 修改方式:内联执行(Task 4 用 Write 创建两个文档)。汇报后停下等待用户明确回复「可以执行」四个字。只有用户原话「可以执行」(含 orchestrator 原样转发的同一串「可以执行」)被记录为结构化放行行 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`(`source` 只允许这两个固定值)才算放行;只出现「可以执行」子串、转述、概括、否定句里的提及或仅写「orchestrator 已放行」都不算。收到放行后,把该结构化放行行用 `Set-Content -LiteralPath "$env:TEMP/gsd-09-49-approval.txt" -Value $line -Encoding UTF8 -NoNewline` 写入 `%TEMP%/gsd-09-49-approval.txt`(文件只有这一行、无尾随换行、UTF-8),并立即执行 AGENTS.md 的账户/时间门禁:检查 `$env:USERNAME` 与本地时间,若账户名为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(用 `[datetime]::Today.AddHours(22)` 比较,精确到秒),立即停止并报告,不开始任何分析或写入。 + + + - 汇报包含目标、静态方法、影响范围、修改方式声明(`修改方式:内联执行(Task 4 用 Write 创建两个文档)`)、并发编辑与类型错误说明。 + - 未收到用户原话「可以执行」(或 orchestrator 原样转发的同一串「可以执行」)前,没有开始分析,也没有写入任何仓库文件。 + - 放行文件只有一行、无尾随换行,格式为 `APPROVED: 可以执行 | source: 用户原话 | time: yyyy-MM-dd HH:mm:ss` 或 `APPROVED: 可以执行 | source: orchestrator原样转发 | time: yyyy-MM-dd HH:mm:ss`,且该行来自用户原话或 orchestrator 原样转发。 + - 账户为 `LHY`/`lhy`/`13194` 且本地时间晚于 22:00 时已立即停止并报告。 + + + 汇报已发出且含 `修改方式:内联执行(Task 4 用 Write 创建两个文档)`;用户明确回复「可以执行」并已转成结构化放行行;账户/时间门禁已检查。 + $p=$env:TEMP+'/gsd-09-49-approval.txt'; if(-not(Test-Path -LiteralPath $p)){exit 1}; $a=Get-Content -LiteralPath $p -Raw -Encoding UTF8; if($a -notmatch '(?m)^APPROVED: 可以执行\s*\|\s*source: (?:用户原话|orchestrator原样转发)\s*\|\s*time: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\r?$'){Write-Output 'no-structured-approval';exit 1}; $u=$env:USERNAME; $cut=[datetime]::Today.AddHours(22); if($u -match '^(LHY|lhy|13194)$' -and (Get-Date) -gt $cut){Write-Output 'time-gate-stop';exit 1}; Write-Output 'approved' + 放行文件缺失、不含结构化 `APPROVED: 可以执行 | source: ... | time: ...` 行、`source` 不是 `用户原话` 或 `orchestrator原样转发`、或账户/时间门禁触发(22:00 后继续)。 + + 无代码产物;只完成结构化放行记录。 + 确认放行文件中的 `APPROVED: 可以执行` 结构化行来自用户原话或 orchestrator 原样转发、`source` 为固定值,并确认账户/时间门禁未触发。 + 用户回复「可以执行」(或 orchestrator 原样转发同一串「可以执行」)并写入结构化放行行,且账户/时间门禁通过后继续。 + 结构化放行状态已记录,账户/时间门禁已通过,后续任务可以开始。 + + + + Task 1: 采集时间点快照与静态清单 + + %TEMP%/gsd-09-49-baseline.txt + %TEMP%/gsd-09-49-inventory.md + %TEMP%/gsd-09-49-tsc.txt + %TEMP%/gsd-09-49-commands.log + + + AGENTS.md + dev.md + index.html + src/bootstrap.ts + src/client/entry.ts + src/data/entry.ts + src/package.json + package.json + pnpm-workspace.yaml + vite.config.ts + script/dev.ts + + + 只读采集,不启动游戏、浏览器、开发服务器或测试。先用 `git rev-parse HEAD`、`git status --porcelain`、`Get-Date` 把时间点快照写入 `%TEMP%/gsd-09-49-baseline.txt`,格式固定为 `HEAD: [sha]`、`脏文件数: [n]`、`采集时间: [本地时间]`,随后一行 `## git status --porcelain` 与逐行原始 porcelain 输出(供后续门禁按行、按精确路径比对)。建立 `%TEMP%/gsd-09-49-inventory.md`,至少包含:入口点(`index.html`、`src/bootstrap.ts`、`src/client/entry.ts`、`src/data/entry.ts`、`script/dev.ts`、`package.json` scripts);工作区与依赖(`package.json`、`pnpm-workspace.yaml`、`src/package.json`、`vite.config.ts` 的 aliases/base/plugins);静态导入图(从入口沿 import/export 逐层记录相对导入与 `@motajs/`、`@user/` 别名导入,不执行代码);旧代码与全局 `core`/`main` 残留(检索 `core.`、`main.`、`window.core`、`window.main`、`declare global`、`@motajs/legacy`、`packages/legacy`、`src/types/declaration`);TODO/FIXME/placeholder/`throw new Error` 未实现标记;内容文件清单(`src/content` 下 core/client/enemy/item/tile/map/animation/audio 等)。可选运行一次类型检查作为证据:`pnpm check:type` 或 `pnpm exec vue-tsc --noEmit --pretty false`,把命令、采集时间、`退出码: [n]`、错误总数与按文件分组的摘要写入 `%TEMP%/gsd-09-49-tsc.txt`;若依赖缺失或命令不可用,写「未运行」及原因,不安装依赖。`vue-tsc` 因并发编辑接口而报告预期类型错误、退出码非零是正常结果,只要证据文件已写入就不算本计划失败;只有证据文件缺失才算失败。所有 `%TEMP%` 证据文件统一用 PowerShell 5.1 `Set-Content -Encoding UTF8`(UTF-8 with BOM)写入;`commands.log` 用 `Add-Content -Encoding UTF8` 追加;本计划所有 verify 统一用 `Get-Content -Encoding UTF8` 读取并接受 BOM。命令日志 `%TEMP%/gsd-09-49-commands.log` 一行一条命令(可带 `PS>` 或 `>` 提示符前缀)。任一行出现 `|`、`(`、`)`、`{`、`}`、与号 或反引号即整行拒绝(`$(` 因含 `(` 同样拒绝);不得使用管道、子表达式、脚本块、命令替换或与号连接。用 `;` 分隔多条命令时必须逐段匹配白名单,任一段不匹配即整行拒绝。只允许白名单内的静态只读命令:git 只读子命令(rev-parse/rev-list/status/log/show/diff/grep/ls-files/cat-file/describe/check-ignore,以及仅 `--list`/`--show-current` 的 `branch`)、`rg`/`grep`/`findstr`、PowerShell 读取与格式命令(Get-Content/Get-ChildItem/Get-Item/Get-ItemProperty/Get-Date/Test-Path/Resolve-Path/Get-Location/Select-String/Measure-Object/Sort-Object/Where-Object/ForEach-Object/Select-Object/Group-Object/Compare-Object/ConvertFrom-Json/ConvertTo-Json/Write-Output/Write-Host/Join-Path/Split-Path/Get-FileHash),仅写 `%TEMP%` 的 Set-Content/Add-Content/Out-File/New-Item/Tee-Object,以及可选的 `vue-tsc --noEmit`、`pnpm check:type`。禁止 `node -e` 与任意 `node 脚本`;需要额外解析时改用 Read/Grep/PowerShell 静态读取。不得记录 npm/pnpm/yarn/bun 的 run/test/build/preview/--filter/exec(`pnpm exec vue-tsc --noEmit` 等价的 `pnpm check:type` 除外)、`vite`、`vitest`、`madge`、`tsx`、`playwright`、`puppeteer`、`node script/*` 等运行时/构建/测试命令。类型检查只作时间点证据,必须注明用户正在并发修改接口,错误数量与内容可能随时变化。不得写入仓库内任何文件。收尾门禁:每个任务结束(子代理执行完毕)时检查 `$env:USERNAME` 与 `Get-Date`;当账户为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(`[datetime]::Today.AddHours(22)`,精确到秒)时,立即停止后续任务并报告,不得继续写入或派发。 + + + - 基线文件含 `HEAD: [sha]`、`脏文件数`、`采集时间` 与完整的原始 porcelain 行。 + - 清单覆盖入口点、工作区/依赖、静态导入图、旧 core/main 残留、TODO/未实现标记、内容文件六类,且每条尽量带路径。 + - tsc 证据文件含命令、时间、`退出码`、结果摘要、并发修改注意事项;未运行时写明原因;退出码非零只要证据已写就不算失败。 + - 命令日志不含白名单之外的命令:任一行出现 `|`、`(`、`)`、`{`、`}`、与号 或反引号、任一段以 `node` 开头、或任一 `;` 段不匹配白名单(含 npm/pnpm/yarn/bun 的 run/test/build/preview/--filter、vite、vitest、madge、tsx、playwright、puppeteer、任何启动游戏/浏览器/开发服务器/测试/构建/预览的命令)均判失败。 + - 账户/时间门禁已执行:账户为 `LHY`/`lhy`/`13194` 且晚于 22:00 时已停止。 + - 本任务没有改动仓库内任何文件。 + + + $base=$env:TEMP+'/gsd-09-49-baseline.txt'; $inv=$env:TEMP+'/gsd-09-49-inventory.md'; $tsc=$env:TEMP+'/gsd-09-49-tsc.txt'; $log=$env:TEMP+'/gsd-09-49-commands.log'; foreach($p in @($base,$inv,$tsc,$log)){if(-not(Test-Path -LiteralPath $p)){Write-Output "missing:$p";exit 1}}; foreach($k in @('HEAD','脏文件数','采集时间')){if(-not(Select-String -LiteralPath $base -Pattern $k -SimpleMatch -Encoding UTF8)){Write-Output "base:$k";exit 1}}; foreach($k in @('入口','依赖','导入','core/main','TODO','内容')){if(-not(Select-String -LiteralPath $inv -Pattern $k -SimpleMatch -Encoding UTF8)){Write-Output "inv:$k";exit 1}}; if(-not(Select-String -LiteralPath $tsc -Pattern '命令' -SimpleMatch -Encoding UTF8)){Write-Output 'tsc-command';exit 1}; if(-not(Select-String -LiteralPath $tsc -Pattern '退出码|未运行' -Encoding UTF8)){Write-Output 'tsc-exitcode';exit 1}; if(-not(Select-String -LiteralPath $tsc -Pattern '并发|变化|注意' -Encoding UTF8)){Write-Output 'tsc-concurrency';exit 1}; $allow=@('^git\s+(?:rev-parse|rev-list|status|log|show|diff|grep|ls-files|cat-file|describe|check-ignore)\b','^git\s+branch\s+(?:--list|--show-current)\b','^rg\b','^grep\b','^findstr\b','^Get-Content\b','^Get-ChildItem\b','^Get-Item\b','^Get-ItemProperty\b','^Get-Date\b','^Test-Path\b','^Resolve-Path\b','^Get-Location\b','^Select-String\b','^Measure-Object\b','^Sort-Object\b','^Where-Object\b','^ForEach-Object\b','^Select-Object\b','^Group-Object\b','^Compare-Object\b','^ConvertFrom-Json\b','^ConvertTo-Json\b','^Write-Output\b','^Write-Host\b','^Set-Content\b','^Add-Content\b','^Out-File\b','^New-Item\b','^Tee-Object\b','^Join-Path\b','^Split-Path\b','^Get-FileHash\b','^(?:pnpm\s+exec\s+)?vue-tsc\s+--noEmit(?:\s|$)','^npx\s+vue-tsc\s+--noEmit(?:\s|$)','^pnpm\s+(?:run\s+)?check:type(?:\s|$)'); $off=@(); foreach($raw in @(Get-Content -LiteralPath $log -Encoding UTF8)){ $ln=$raw.Trim(); if($ln -eq '' -or $ln -match '^#'){continue}; if($ln.IndexOf([char]0x26) -ge 0 -or $ln.IndexOf([char]0x7C) -ge 0 -or $ln.IndexOf([char]0x28) -ge 0 -or $ln.IndexOf([char]0x29) -ge 0 -or $ln.IndexOf([char]0x7B) -ge 0 -or $ln.IndexOf([char]0x7D) -ge 0 -or $ln.IndexOf([char]0x60) -ge 0){$off+=$raw;continue}; $ln=$ln -replace '^(?:PS[^>]*>|\s*>)',''; $ln=$ln -replace '^\$\s+',''; $ln=$ln -replace '^\$[A-Za-z0-9_]+\s*=\s*',''; foreach($seg in @($ln -split ';')){ $st=$seg.Trim(); if($st -eq ''){continue}; if($st -match '^node\b'){$off+=$raw;break}; $st=$st -replace '^%\s*',''; $ok=$false; foreach($pat in $allow){if($st -match $pat){$ok=$true;break}}; if(-not $ok){$off+=$raw;break} } }; if($off.Count -ne 0){Write-Output 'command-not-allowlisted';$off|Select-Object -First 3;exit 1}; $u=$env:USERNAME; $cut=[datetime]::Today.AddHours(22); if($u -match '^(LHY|lhy|13194)$' -and (Get-Date) -gt $cut){Write-Output 'time-gate-stop';exit 1}; Write-Output 'ok' + 任一临时证据缺失、基线缺 HEAD/脏文件数/时间、清单缺类目、tsc 无命令或未记录退出码/未运行说明、无并发提示、命令日志任一行含 `|`、`(`、`)`、`{`、`}`、与号 或反引号、任一段以 `node` 开头、或任一 `;` 段不匹配白名单(含 npm/pnpm 运行命令),或账户/时间门禁触发。 + + 基线、清单、tsc 证据与命令日志已写入临时目录;仓库未改;账户/时间门禁已通过。 + + + + Task 2: 分析启动链与导入可解析性 + + %TEMP%/gsd-09-49-startup.md + + + index.html + src/bootstrap.ts + src/client/entry.ts + src/client/index.ts + src/data/entry.ts + src/data/index.ts + src/package.json + package.json + pnpm-workspace.yaml + vite.config.ts + script/dev.ts + packages-user/entry-client/src/index.ts + packages-user/entry-data/src/index.ts + packages-user/data-state/src/index.ts + packages-user/client-modules/src/index.ts + + + 从 `index.html` 的 `/src/bootstrap.ts` 开始,逐层核对 `bootstrap → createClientCore → ClientCore → createCoreStateConfig → CoreState` 的导入与初始化顺序;对每个 import 静态确认目标文件与导出存在,记录断链、缺失导出、别名未覆盖、workspace 包未声明或入口未指向 `src`。检查 `vite.config.ts` 的 aliases/base/plugins 与 `index.html` 的 canvas/script、`src/package.json` 的 workspace 依赖、`package.json` 的 scripts 是否与实际入口一致(例如 `check:circular` 指向的 `src/main.ts` 是否存在,只核对不执行)。评估循环引用与顶层副作用风险,但不得运行 madge、dev、build 或测试。把候选问题按 P0 阻断 / P1 集成缺口 / 预期中写入 `%TEMP%/gsd-09-49-startup.md`(按 Task 1 的统一策略用 `Set-Content -Encoding UTF8` 写入,UTF-8 with BOM),每条一行,格式为 `- **P0-n 标题**:原因。证据:\`path:line\`。`,并说明「为什么影响启动」。证据允许带扩展名的源码/配置文件,也允许 `.npmrc`、`.gitignore`、`.env` 等点文件或 `Dockerfile` 这类无扩展名配置,但必须能定位到具体行。无法静态确认的写「无法静态确认」,不要猜。收尾门禁:每个任务结束(子代理执行完毕)时检查 `$env:USERNAME` 与 `Get-Date`;当账户为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(`[datetime]::Today.AddHours(22)`,精确到秒)时,立即停止后续任务并报告,不得继续写入或派发。 + + + - 启动链每一跳都有对应文件与证据,能回答“游戏能否 bootstrap”。 + - 缺失模块、断链导入、别名/workspace/入口配置不一致均有候选结论。 + - 候选问题分为 P0、P1、预期中;每条 P0/P1 都有 file:line。 + - 没有执行 dev/build/test/browser,也没有修改仓库文件。 + - 账户/时间门禁已执行:账户为 `LHY`/`lhy`/`13194` 且晚于 22:00 时已停止。 + + + $p=$env:TEMP+'/gsd-09-49-startup.md'; if(-not(Test-Path -LiteralPath $p)){Write-Output 'analysis-missing';exit 1}; $txt=Get-Content -LiteralPath $p -Raw -Encoding UTF8; foreach($k in @('bootstrap','P0','P1','预期中','构建')){if($txt -notmatch [regex]::Escape($k)){Write-Output "missing:$k";exit 1}}; $lines=Get-Content -LiteralPath $p -Encoding UTF8; $ev='(?:[A-Za-z0-9_.-]+/)*(?:[A-Za-z0-9_.-]+\.(?:ts|tsx|js|mjs|cjs|json|jsonc|yaml|yml|css|scss|less|html|vue|md|frag)|\.[A-Za-z][A-Za-z0-9_.-]*|Dockerfile|Makefile):\d+'; $bad=@($lines | Where-Object { $_ -match '^- \*\*P[01]-\d+' -and $_ -notmatch $ev }); if($bad.Count -ne 0){$bad;exit 1}; $log=$env:TEMP+'/gsd-09-49-commands.log'; if(-not(Test-Path -LiteralPath $log)){Write-Output 'log-missing';exit 1}; $allow=@('^git\s+(?:rev-parse|rev-list|status|log|show|diff|grep|ls-files|cat-file|describe|check-ignore)\b','^git\s+branch\s+(?:--list|--show-current)\b','^rg\b','^grep\b','^findstr\b','^Get-Content\b','^Get-ChildItem\b','^Get-Item\b','^Get-ItemProperty\b','^Get-Date\b','^Test-Path\b','^Resolve-Path\b','^Get-Location\b','^Select-String\b','^Measure-Object\b','^Sort-Object\b','^Where-Object\b','^ForEach-Object\b','^Select-Object\b','^Group-Object\b','^Compare-Object\b','^ConvertFrom-Json\b','^ConvertTo-Json\b','^Write-Output\b','^Write-Host\b','^Set-Content\b','^Add-Content\b','^Out-File\b','^New-Item\b','^Tee-Object\b','^Join-Path\b','^Split-Path\b','^Get-FileHash\b','^(?:pnpm\s+exec\s+)?vue-tsc\s+--noEmit(?:\s|$)','^npx\s+vue-tsc\s+--noEmit(?:\s|$)','^pnpm\s+(?:run\s+)?check:type(?:\s|$)'); $off=@(); foreach($raw in @(Get-Content -LiteralPath $log -Encoding UTF8)){ $ln=$raw.Trim(); if($ln -eq '' -or $ln -match '^#'){continue}; if($ln.IndexOf([char]0x26) -ge 0 -or $ln.IndexOf([char]0x7C) -ge 0 -or $ln.IndexOf([char]0x28) -ge 0 -or $ln.IndexOf([char]0x29) -ge 0 -or $ln.IndexOf([char]0x7B) -ge 0 -or $ln.IndexOf([char]0x7D) -ge 0 -or $ln.IndexOf([char]0x60) -ge 0){$off+=$raw;continue}; $ln=$ln -replace '^(?:PS[^>]*>|\s*>)',''; $ln=$ln -replace '^\$\s+',''; $ln=$ln -replace '^\$[A-Za-z0-9_]+\s*=\s*',''; foreach($seg in @($ln -split ';')){ $st=$seg.Trim(); if($st -eq ''){continue}; if($st -match '^node\b'){$off+=$raw;break}; $st=$st -replace '^%\s*',''; $ok=$false; foreach($pat in $allow){if($st -match $pat){$ok=$true;break}}; if(-not $ok){$off+=$raw;break} } }; if($off.Count -ne 0){Write-Output 'command-not-allowlisted';$off|Select-Object -First 3;exit 1}; $u=$env:USERNAME; $cut=[datetime]::Today.AddHours(22); if($u -match '^(LHY|lhy|13194)$' -and (Get-Date) -gt $cut){Write-Output 'time-gate-stop';exit 1}; Write-Output 'ok' + 分析文件缺失、缺少启动链/P0/P1/预期中/构建关键词、任一 P0/P1 无 file:line、命令日志任一行含 `|`、`(`、`)`、`{`、`}`、与号 或反引号、任一段以 `node` 开头、或任一 `;` 段不匹配白名单,或账户/时间门禁触发。 + + 启动链、导入解析、入口/构建配置风险已静态归档并分级;账户/时间门禁已通过。 + + + + Task 3: 分析子系统集成与旧代码边界 + + %TEMP%/gsd-09-49-subsystems.md + + + src/client/initialize.ts + src/data/initialize.ts + src/client/plugin/list.ts + src/client/plugin/shop/index.ts + src/data/plugin/list.ts + src/data/event/registrations.ts + src/data/hook/index.ts + src/data/loader/index.ts + src/data/replay/index.ts + src/data/hero/index.ts + src/client/constant.ts + src/data/constant.ts + src/content/core.jsonc + src/content/client.jsonc + src/content/enemy.jsonc + src/content/item.jsonc + src/content/tile.jsonc + src/content/maps/.list.jsonc + packages-user/data-state/src/core.ts + packages-user/client-modules/src/client.ts + packages-user/client-base/src/save/index.ts + packages-user/data-system/src/combat/index.ts + packages-user/data-system/src/event/index.ts + packages-user/data-system/src/path/index.ts + packages-user/data-base/src/map/index.ts + packages-user/data-base/src/hero/index.ts + packages-user/data-base/src/enemy/index.ts + packages-user/data-base/src/flag/index.ts + + + 按八类做静态集成核对,不做任何运行验证:① 内容加载——`CORE_URL`/`CLIENT_CORE_URL` 指向的 `src/content/*.jsonc` 是否存在、列表文件是否覆盖、加载 hook 与 load starter 是否接上;② 渲染/UI——`index.html` 的 `#render-main` 与 `RENDER_CANVAS`、`LoadSceneUI`、`sceneController.open`、client 插件的 awake/initialize/afterLoad 是否形成可渲染链;③ 事件——`createEventRegistrations` 与事件执行/注册是否接通;④ 战斗——`initializeEnemy`、hook/weak handler 与数据系统是否接通;⑤ 寻路——`state.pathfinding.finder`、`DefaultPassPredicateImpl`、mover 是否接通;⑥ 存档——`state.save.init`、`addSaveableContent`、hotkey 全局存档是否接通;⑦ 旧代码边界——`core`/`main` 全局、`src/types/declaration`、legacy 目录引用是否仍被入口或运行时依赖;⑧ 系统级集成——`src/bootstrap.ts` 与 `createClientCore` 的初始化顺序、`src/client/initialize.ts` 与 `src/data/initialize.ts` 的调用时机、client/data 插件注册顺序、全局单例与 hook 注册先后、入口→client→data→packages-user 的跨层 wiring 是否存在缺环、重复初始化或先注册后覆盖。逐条写入 `%TEMP%/gsd-09-49-subsystems.md`(按 Task 1 的统一策略用 `Set-Content -Encoding UTF8` 写入,UTF-8 with BOM),按 P0 阻断 / P1 集成缺口 / 预期中分类;每条一行,格式为 `- **P0-n 标题**:原因。证据:\`path:line\`。`。证据允许带扩展名的源码/配置文件,也允许 `.npmrc`、`.gitignore`、`.env` 等点文件或 `Dockerfile` 这类无扩展名配置,但必须能定位到具体行。预期中的类型错误按文件或接口组归并,明确写出「不计为新缺陷」。不得把新旧引擎 parity 内容写进分析。收尾门禁:每个任务结束(子代理执行完毕)时检查 `$env:USERNAME` 与 `Get-Date`;当账户为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(`[datetime]::Today.AddHours(22)`,精确到秒)时,立即停止后续任务并报告,不得继续写入或派发。 + + + - 八类(含系统级集成)子系统均有明确结论:接通、缺口或无法静态确认。 + - 每条 P0/P1 都有 file:line 与影响说明,能区分“功能路径未接通”和“整局无法启动”。 + - 预期中的接口未适配/类型错误单独归类,并写明不计为新缺陷。 + - 没有运行游戏、dev、build、test 或浏览器自动化,也没有修改仓库文件。 + - 账户/时间门禁已执行:账户为 `LHY`/`lhy`/`13194` 且晚于 22:00 时已停止。 + + + $p=$env:TEMP+'/gsd-09-49-subsystems.md'; if(-not(Test-Path -LiteralPath $p)){Write-Output 'analysis-missing';exit 1}; $txt=Get-Content -LiteralPath $p -Raw -Encoding UTF8; foreach($k in @('内容加载','渲染','事件','战斗','寻路','存档','legacy','系统级集成','P0','P1','预期中')){if($txt -notmatch [regex]::Escape($k)){Write-Output "missing:$k";exit 1}}; $lines=Get-Content -LiteralPath $p -Encoding UTF8; $ev='(?:[A-Za-z0-9_.-]+/)*(?:[A-Za-z0-9_.-]+\.(?:ts|tsx|js|mjs|cjs|json|jsonc|yaml|yml|css|scss|less|html|vue|md|frag)|\.[A-Za-z][A-Za-z0-9_.-]*|Dockerfile|Makefile):\d+'; $bad=@($lines | Where-Object { $_ -match '^- \*\*P[01]-\d+' -and $_ -notmatch $ev }); if($bad.Count -ne 0){$bad;exit 1}; $log=$env:TEMP+'/gsd-09-49-commands.log'; if(-not(Test-Path -LiteralPath $log)){Write-Output 'log-missing';exit 1}; $allow=@('^git\s+(?:rev-parse|rev-list|status|log|show|diff|grep|ls-files|cat-file|describe|check-ignore)\b','^git\s+branch\s+(?:--list|--show-current)\b','^rg\b','^grep\b','^findstr\b','^Get-Content\b','^Get-ChildItem\b','^Get-Item\b','^Get-ItemProperty\b','^Get-Date\b','^Test-Path\b','^Resolve-Path\b','^Get-Location\b','^Select-String\b','^Measure-Object\b','^Sort-Object\b','^Where-Object\b','^ForEach-Object\b','^Select-Object\b','^Group-Object\b','^Compare-Object\b','^ConvertFrom-Json\b','^ConvertTo-Json\b','^Write-Output\b','^Write-Host\b','^Set-Content\b','^Add-Content\b','^Out-File\b','^New-Item\b','^Tee-Object\b','^Join-Path\b','^Split-Path\b','^Get-FileHash\b','^(?:pnpm\s+exec\s+)?vue-tsc\s+--noEmit(?:\s|$)','^npx\s+vue-tsc\s+--noEmit(?:\s|$)','^pnpm\s+(?:run\s+)?check:type(?:\s|$)'); $off=@(); foreach($raw in @(Get-Content -LiteralPath $log -Encoding UTF8)){ $ln=$raw.Trim(); if($ln -eq '' -or $ln -match '^#'){continue}; if($ln.IndexOf([char]0x26) -ge 0 -or $ln.IndexOf([char]0x7C) -ge 0 -or $ln.IndexOf([char]0x28) -ge 0 -or $ln.IndexOf([char]0x29) -ge 0 -or $ln.IndexOf([char]0x7B) -ge 0 -or $ln.IndexOf([char]0x7D) -ge 0 -or $ln.IndexOf([char]0x60) -ge 0){$off+=$raw;continue}; $ln=$ln -replace '^(?:PS[^>]*>|\s*>)',''; $ln=$ln -replace '^\$\s+',''; $ln=$ln -replace '^\$[A-Za-z0-9_]+\s*=\s*',''; foreach($seg in @($ln -split ';')){ $st=$seg.Trim(); if($st -eq ''){continue}; if($st -match '^node\b'){$off+=$raw;break}; $st=$st -replace '^%\s*',''; $ok=$false; foreach($pat in $allow){if($st -match $pat){$ok=$true;break}}; if(-not $ok){$off+=$raw;break} } }; if($off.Count -ne 0){Write-Output 'command-not-allowlisted';$off|Select-Object -First 3;exit 1}; if($txt -match 'parity|PARITY|新引擎独有|旧引擎独有'){Write-Output 'parity-content';exit 1}; $u=$env:USERNAME; $cut=[datetime]::Today.AddHours(22); if($u -match '^(LHY|lhy|13194)$' -and (Get-Date) -gt $cut){Write-Output 'time-gate-stop';exit 1}; Write-Output 'ok' + 分析文件缺失、八类或分级关键词缺失、任一 P0/P1 无 file:line、出现 parity 内容、命令日志任一行含 `|`、`(`、`)`、`{`、`}`、与号 或反引号、任一段以 `node` 开头、或任一 `;` 段不匹配白名单,或账户/时间门禁触发。 + + 八类集成路径(含系统级集成)与旧代码边界已静态归档;缺口均带证据并分级;账户/时间门禁已通过。 + + + + Task 4: 写入审计文档与 SUMMARY 并自检 + + .planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md + .planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md + + + %TEMP%/gsd-09-49-baseline.txt + %TEMP%/gsd-09-49-inventory.md + %TEMP%/gsd-09-49-tsc.txt + %TEMP%/gsd-09-49-startup.md + %TEMP%/gsd-09-49-subsystems.md + AGENTS.md + dev.md + + + 用 Write 创建 `09-RUNNABILITY-AUDIT.md`(不要用 shell heredoc),两个文档固定使用 CRLF 换行(dev.md 第 116 行要求),Write 内容中的换行一律写成 `\r\n`,不得只写 `\n`。文档使用中文、简洁、人读优先,固定包含这些二级标题:`## 结论`(第一段直接回答“现在能否运行”;不能则列出最小阻断集,并写 `P0 数量:N`、`P1 数量:M`)、`## P0 阻断`、`## P1 集成缺口`、`## 预期中(不计为新缺陷)`、`## 最小可运行路径`(编号步骤,给最短修复顺序)、`## 覆盖清单`(入口与导入、core/main 残留、内容加载、渲染/UI、事件、战斗、寻路、存档、构建、工作区依赖、系统级集成逐项写结论)、`## 证据与快照`(git HEAD、脏文件数、采集时间、tsc 命令与结果摘要、并发修改说明)、`## 范围与并发说明`(只写两个文档;未改代码/配置/内容/STATE/ROADMAP;未提交;用户并发编辑可能带来变化;若发现基线之外的路径漂移,写明是外部漂移、不归因于本执行器;两个目标文档的 `.bak`、`.tmp` 等后缀变体一律不算允许写入,视为范围外写入并失败)。每条 P0/P1 必须独占一行并以 `- **P0-n 标题**:` 或 `- **P1-n 标题**:` 开头,行内必须出现 `file:line` 证据与影响说明;证据允许带扩展名的源码/配置文件,也允许 `.npmrc`、`.gitignore`、`.env` 等点文件或 `Dockerfile` 这类无扩展名配置。没有 P0/P1 时写 `- 无。`。预期中的类型错误按接口或文件归并,不逐条罗列,不当作新缺陷。全文不得出现 parity、新旧引擎独有对照。写入后创建 `09-49-SUMMARY.md`,记录基线 HEAD/脏文件数/采集时间、P0/P1/预期中数量、tsc 命令与退出码摘要、八类覆盖、门禁结果、`执行器写入范围外:无`、`外部漂移数量:[n]` 或 `外部漂移:无`、范围证明、未提交声明与并发说明;若并发编辑导致基线之外的文件发生变化,只作为外部漂移记录,不当作本执行器的写入,也不因此硬失败,但若本执行器自己确实写了两个文档之外的仓库文件,必须如实写明范围外写入并视为失败。最后自检:标题齐全、每条 P0/P1 有证据、八类(含系统级集成)覆盖、无 parity、HEAD 未变、执行器只写两个文档;失败就修文档,不放宽门禁。收尾门禁:每个任务结束(子代理执行完毕)时检查 `$env:USERNAME` 与 `Get-Date`;当账户为 `LHY`、`lhy` 或 `13194` 且当前时间晚于今天 22:00(`[datetime]::Today.AddHours(22)`,精确到秒)时,立即停止后续任务并报告,不得继续写入或派发。 + + + - 审计文档存在,顶部结论直接回答能否运行,并给出最小阻断集。 + - P0/P1 每条都有 `file:line` 证据;预期中单独归类且写明不计为新缺陷。 + - 最小可运行路径是编号步骤;覆盖清单覆盖八类(含系统级集成);证据与快照含 HEAD、脏文件数、时间、tsc 摘要与并发说明。 + - 若账户为 `LHY`/`lhy`/`13194` 且时间晚于 22:00,已停止且未继续写入。 + - SUMMARY 含 P0/P1/预期中数量、覆盖、门禁、`执行器写入范围外:无`、`外部漂移` 记录、范围与未提交声明。 + - 仓库内执行器只新增这两个文档;代码、配置、内容、STATE、ROADMAP 未改;未提交;外部并发漂移如实记录且不归因于本执行器。 + + + $doc='.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md'; $sum='.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md'; if(-not(Test-Path -LiteralPath $doc) -or -not(Test-Path -LiteralPath $sum)){Write-Output 'doc-missing';exit 1}; foreach($pair in @(@($doc,'doc'),@($sum,'sum'))){ $raw=Get-Content -LiteralPath $pair[0] -Raw -Encoding UTF8; $lf=([regex]::Matches($raw, [string][char]0x0A)).Count; $crlf=([regex]::Matches($raw, ([string][char]0x0D + [string][char]0x0A))).Count; if($lf -ne $crlf){Write-Output ($pair[1]+'-not-crlf');exit 1} }; $txt=Get-Content -LiteralPath $doc -Raw -Encoding UTF8; foreach($h in @('## 结论','## P0 阻断','## P1 集成缺口','## 预期中','## 最小可运行路径','## 覆盖清单','## 证据与快照','## 范围与并发说明')){if($txt -notmatch [regex]::Escape($h)){Write-Output "missing:$h";exit 1}}; foreach($k in @('能否运行','P0 数量','P1 数量')){if($txt -notmatch [regex]::Escape($k)){Write-Output "missing:$k";exit 1}}; $lines=Get-Content -LiteralPath $doc -Encoding UTF8; $ev='(?:[A-Za-z0-9_.-]+/)*(?:[A-Za-z0-9_.-]+\.(?:ts|tsx|js|mjs|cjs|json|jsonc|yaml|yml|css|scss|less|html|vue|md|frag)|\.[A-Za-z][A-Za-z0-9_.-]*|Dockerfile|Makefile):\d+'; $bad=@($lines | Where-Object { $_ -match '^- \*\*P[01]-\d+' -and $_ -notmatch $ev }); if($bad.Count -ne 0){$bad;exit 1}; foreach($k in @('HEAD','脏文件数','采集时间','tsc','并发')){if($txt -notmatch [regex]::Escape($k)){Write-Output "snapshot:$k";exit 1}}; foreach($k in @('入口','core/main','内容加载','渲染','事件','战斗','寻路','存档','构建','工作区','系统级')){if($txt -notmatch [regex]::Escape($k)){Write-Output "cat:$k";exit 1}}; if($txt -match 'parity|PARITY|新引擎独有|旧引擎独有|新旧引擎对照'){Write-Output 'parity-content';exit 1}; $steps=@($lines | Where-Object { $_ -match '^\d+\. ' }); if($steps.Count -lt 2){Write-Output "steps=$($steps.Count)";exit 1}; $p0=@($lines | Where-Object { $_ -match '^- \*\*P0-\d+' }).Count; $p1=@($lines | Where-Object { $_ -match '^- \*\*P1-\d+' }).Count; $s=Get-Content -LiteralPath $sum -Raw -Encoding UTF8; foreach($k in @('P0 数量','P1 数量','未提交','并发','外部漂移','执行器写入范围外:无')){if($s -notmatch [regex]::Escape($k)){Write-Output "sum:$k";exit 1}}; if($s -notmatch ('P0 数量:'+$p0) -and $s -notmatch ('P0 数量:'+$p0)){Write-Output 'p0-count';exit 1}; if($s -notmatch ('P1 数量:'+$p1) -and $s -notmatch ('P1 数量:'+$p1)){Write-Output 'p1-count';exit 1}; $basePath=$env:TEMP+'/gsd-09-49-baseline.txt'; if(-not(Test-Path -LiteralPath $basePath)){Write-Output 'baseline-missing';exit 1}; $baseLines=@(Get-Content -LiteralPath $basePath -Encoding UTF8); $headRaw=git rev-parse HEAD; if($LASTEXITCODE -ne 0){Write-Output 'git-head-failed';exit 1}; $head=$headRaw.Trim(); if(-not ($baseLines -match [regex]::Escape($head))){Write-Output 'head-changed';exit 1}; $status=git status --porcelain; if($LASTEXITCODE -ne 0){Write-Output 'git-status-failed';exit 1}; $now=@($status); $baseStatus=@($baseLines | Where-Object { $_ -match '^(..|!!) ' }); $delta=@($now | Where-Object { $baseStatus -notcontains $_ }); $allowedDocs=@('.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md','.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md'); $extra=@(); $suffix=@(); foreach($entry in $delta){ if($entry.Length -lt 4){$extra+=$entry;continue}; $path=$entry.Substring(3).Trim(); if($path -match ' -> '){$path=($path -split ' -> ')[-1].Trim()}; if($path -match '^"(.*)"$'){$path=$Matches[1]}; if($allowedDocs -contains $path){continue}; $isSuffix=$false; foreach($d in $allowedDocs){ if($path.StartsWith($d,[System.StringComparison]::Ordinal)){$isSuffix=$true} }; if($isSuffix){$suffix+=$entry}else{$extra+=$entry} }; if($suffix.Count -ne 0){Write-Output 'scope-suffix-variant';$suffix;exit 1}; $log=$env:TEMP+'/gsd-09-49-commands.log'; if(Test-Path -LiteralPath $log){ $allow=@('^git\s+(?:rev-parse|rev-list|status|log|show|diff|grep|ls-files|cat-file|describe|check-ignore)\b','^git\s+branch\s+(?:--list|--show-current)\b','^rg\b','^grep\b','^findstr\b','^Get-Content\b','^Get-ChildItem\b','^Get-Item\b','^Get-ItemProperty\b','^Get-Date\b','^Test-Path\b','^Resolve-Path\b','^Get-Location\b','^Select-String\b','^Measure-Object\b','^Sort-Object\b','^Where-Object\b','^ForEach-Object\b','^Select-Object\b','^Group-Object\b','^Compare-Object\b','^ConvertFrom-Json\b','^ConvertTo-Json\b','^Write-Output\b','^Write-Host\b','^Set-Content\b','^Add-Content\b','^Out-File\b','^New-Item\b','^Tee-Object\b','^Join-Path\b','^Split-Path\b','^Get-FileHash\b','^(?:pnpm\s+exec\s+)?vue-tsc\s+--noEmit(?:\s|$)','^npx\s+vue-tsc\s+--noEmit(?:\s|$)','^pnpm\s+(?:run\s+)?check:type(?:\s|$)'); $off=@(); foreach($raw in @(Get-Content -LiteralPath $log -Encoding UTF8)){ $ln=$raw.Trim(); if($ln -eq '' -or $ln -match '^#'){continue}; if($ln.IndexOf([char]0x26) -ge 0 -or $ln.IndexOf([char]0x7C) -ge 0 -or $ln.IndexOf([char]0x28) -ge 0 -or $ln.IndexOf([char]0x29) -ge 0 -or $ln.IndexOf([char]0x7B) -ge 0 -or $ln.IndexOf([char]0x7D) -ge 0 -or $ln.IndexOf([char]0x60) -ge 0){$off+=$raw;continue}; $ln=$ln -replace '^(?:PS[^>]*>|\s*>)',''; $ln=$ln -replace '^\$\s+',''; $ln=$ln -replace '^\$[A-Za-z0-9_]+\s*=\s*',''; foreach($seg in @($ln -split ';')){ $st=$seg.Trim(); if($st -eq ''){continue}; if($st -match '^node\b'){$off+=$raw;break}; $st=$st -replace '^%\s*',''; $ok=$false; foreach($pat in $allow){if($st -match $pat){$ok=$true;break}}; if(-not $ok){$off+=$raw;break} } }; if($off.Count -ne 0){Write-Output 'command-not-allowlisted';$off|Select-Object -First 3;exit 1} }; if($extra.Count -gt 0){Write-Output "external-drift:$($extra.Count)"}; $u=$env:USERNAME; $cut=[datetime]::Today.AddHours(22); if($u -match '^(LHY|lhy|13194)$' -and (Get-Date) -gt $cut){Write-Output 'time-gate-stop';exit 1}; Write-Output "P0=$p0 P1=$p1 steps=$($steps.Count) delta=$($delta.Count) extra=$($extra.Count)" + 文档/SUMMARY 缺失、标题或八类覆盖缺失、P0/P1 无 file:line、最小路径不足两步、出现 parity、SUMMARY 计数与文档不符、缺少 `执行器写入范围外:无` 或 `外部漂移` 记录、HEAD 变化、两个目标文档的后缀变体(`.bak`/`.tmp` 等)、两个文档非 CRLF 换行、命令日志任一行含 `|`、`(`、`)`、`{`、`}`、与号 或反引号、任一段以 `node` 开头、或任一 `;` 段不匹配白名单、执行器自报范围外写入、账户/时间门禁触发,或 git 命令非零退出。 + + 审计文档与 SUMMARY 已写入;P0/P1 证据、八类覆盖、快照、范围、外部漂移记录与账户/时间门禁全部通过。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| 审计执行器 → 源码树 | 只读;唯一允许写入的是审计文档与 SUMMARY | +| 审计执行器 → 并发用户编辑 | 同一工作树可能同时变化,不能把外部改动算作本计划写入 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-49-01 | Tampering | 静态分析边界 | high | mitigate | 命令日志白名单只允许静态读取与可选的 `vue-tsc --noEmit`/`pnpm check:type`;任一行含 `|`、`(`、`)`、`{`、`}`、与号 或反引号即拒绝;逐 `;` 段校验,任一段不匹配白名单即整行拒绝;禁止 `node -e` 与任意 `node 脚本`;禁止 dev/test/build/preview/check:circular、vitest、madge、浏览器与 `tsx script/*` | +| T-49-02 | Information Integrity | 类型错误与证据 | high | mitigate | tsc 只作时间点证据并标注并发修改;退出码非零属预期、只要证据文件存在即不失败;预期错误单独归类;每条 P0/P1 必须有 `file:line` | +| T-49-03 | Repudiation | 范围与提交 | medium | mitigate | 基线 porcelain 按精确路径比对;执行器写入仅两个文档;外部漂移单独记录不归因;HEAD 未变;明确未提交 | +| T-49-SC | Tampering | 依赖安装 | high | mitigate | 不安装依赖、不改 lockfile、不调用 `opencode`、不执行包管理安装命令 | + + + +- 两个目标文档存在且使用 CRLF 换行(dev.md 第 116 行要求),审计文档含结论、P0、P1、预期中、最小可运行路径、覆盖清单、证据与快照、范围与并发说明。 +- 每条 P0/P1 都有 `file:line` 证据;八类(含系统级集成)覆盖齐全;无 parity 内容。 +- 证据快照含 git HEAD、脏文件数、采集时间、tsc 命令/退出码/结果摘要与并发说明。 +- 命令日志只含白名单内的静态读取命令与可选的 `vue-tsc --noEmit`/`pnpm check:type`;任一行不含 `|`、`(`、`)`、`{`、`}`、与号、反引号或 `node` 调用;无 dev/build/test/browser 运行命令;未安装依赖;未提交。 +- 执行器只写两个文档;HEAD 未变;代码、配置、内容、STATE、ROADMAP 未改;外部并发漂移在 SUMMARY 中记录且不归因于本执行器。 +- 每个任务收尾时账户/时间门禁已执行;账户为 `LHY`/`lhy`/`13194` 且晚于 22:00 时立即停止(`[datetime]::Today.AddHours(22)`,精确到秒)。 + + + +- 用户能从文档顶部直接判断“现在能否运行”,并看到最小阻断集。 +- 所有阻断与集成缺口都有可核对的文件行号,预期类型错误没有被误报为新缺陷。 +- 文档给出最短的最小可运行路径,且明确覆盖用户要求的八类(含系统级集成)检查范围。 +- SUMMARY 记录基线、计数、证据、覆盖、门禁、范围证明与外部漂移记录。 + + + +Create `.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md` when done. + diff --git a/.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md b/.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md new file mode 100644 index 0000000..a2a7141 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-49-SUMMARY.md @@ -0,0 +1,83 @@ +# Phase 09 Plan 49: 静态可运行性审计 Summary + +## 结论 + +已完成只读静态审计,产出 09-RUNNABILITY-AUDIT.md。当前快照不能完成浏览器启动,最小阻断集由 6 条 P0 与 7 条 P1 组成。未改代码、配置、内容、STATE、ROADMAP;未提交。 + +## 基线 + +- 基线 HEAD:5b1f8664093f8f9687499ec092e3f3150a507f9f +- 基线脏文件数:79 +- 采集时间:2026-11-05 18:46:50 +- 类型检查时间:2026-11-05 18:47:19 至 18:47:24 +- 当前 HEAD:b950c53f98804587058a9549a0178761efb4b89b(外部漂移,见下) + +## 计数 + +- P0 数量:6 +- P1 数量:7 +- 预期中:4 条(并发编辑接口未适配、测试夹具未同步、类型层未对齐、浏览器 ESNext 支持无法静态确认) +- 最小可运行路径:8 步 + +## 类型检查证据 + +- 命令:pnpm check:type(等价 vue-tsc --noEmit,经 pnpm.cmd 执行) +- 退出码:2 +- 错误行总数:258 +- 非测试源错误行:92 +- 说明:用户并发编辑接口,退出码非零属预期;只作时间点证据,不计为本执行器引入的新缺陷。 +- 环境偏差:pnpm.ps1 被 PowerShell 执行策略拦截,改用 pnpm.cmd;属环境限制。 + +## 八类覆盖 + +- 入口与导入:主链存在,但 P0-1、P0-2、P0-6 造成解析或链接失败。 +- core/main 残留:core.d.ts 仍声明 main、core、Mota,UI 仍在使用且运行期未赋值(P0-5、P1-6)。 +- 内容加载:配置路径可解析;数据配置只下载不落地,客户端清单未接入且缺资源(P1-1、P1-3、P1-7)。 +- 渲染/UI:场景打开链存在,被 P0-1、P0-2、P0-5 截断,元素未注册(P1-2)。 +- 事件:createEventRegistrations 与 GameEventSystem 已接,静态类型通过;未运行。 +- 战斗:initializeEnemy 与战斗钩子已接,静态类型通过;未运行。 +- 寻路:DefaultPassPredicateImpl、finder.useMapLayer 与 useMover 已接,静态类型通过;未运行。 +- 存档:addSaveableContent 与 save.init 代码存在,但依赖 afterLoad;当前入口不触发(P0-4)。 +- 构建:vite 配置可用;check:circular 指向不存在的 src/main.ts(P1-5)。 +- 工作区依赖:pnpm-workspace 覆盖 packages、packages-user、src;workspace 依赖齐全;未安装或构建。 +- 系统级集成:插件构造顺序、顶层 load、客户端钩子与元素注册存在缺环(P0-3、P0-4、P1-1、P1-2)。 + +## 门禁结果 + +- Task 0 结构化放行:通过(APPROVED 行已写入,账户/时间门禁未触发)。 +- Task 1 证据文件与命令白名单:通过(task1-gate-ok)。 +- Task 2 启动链分析:通过(task2-gate-ok)。 +- Task 3 子系统分析:通过(task3-gate-ok)。 +- Task 4 文档门禁:见下方「Task 4 门禁实况」。 + +## 范围证明 + +- 执行器写入范围外:无 +- 两个目标文档的 .bak、.tmp 等后缀变体均未写入。 +- 代码、配置、内容、STATE.md、ROADMAP.md、既有计划与摘要均未修改。 +- 未创建分支或工作树;未运行游戏、浏览器、dev 服务器、构建或测试(仅一次 pnpm check:type 作为证据)。 + +## 外部漂移 + +- 外部漂移数量:1 +- 明细:HEAD 由 5b1f8664093f8f9687499ec092e3f3150a507f9f 变为 b950c53f98804587058a9549a0178761efb4b89b,新提交 b950c53 的时间为 2026-11-05 18:51:26 +0800,提交信息为 feat: IHotkeyUsing 补充 when 方法。 +- 归因:该变化由用户并发提交造成,不是本执行器写入;本执行器未执行任何 git 写命令,未回滚或删除用户改动。 +- 文件级漂移:与基线 porcelain 比对,新增项只有本执行器的 09-RUNNABILITY-AUDIT.md,未发现其它新增或删除路径。 + +## 未提交声明 + +- 两个 Markdown 文档均未提交,保持未跟踪状态。 +- 未执行 git add、git commit、git push 或任何其它写命令。 + +## 并发说明 + +- 用户在同一工作树并发编辑接口并提交,审计内容只代表 2026-11-05 18:46:50 至 18:47:24 的采集瞬间。 +- 预期中的类型错误不当作新阻断;若外部漂移导致后续证据变化,以重新采集为准。 +- Task 4 门禁中的 HEAD 校验会因上述外部提交而报 head-changed;这是外部漂移的真实结果,未通过修改基线或放宽门禁规避。 + +## Task 4 门禁实况 + +- 文档存在与 CRLF:通过。 +- 标题、分类、file:line、最小路径、八类覆盖、无 parity:通过。 +- HEAD 未变:未通过(head-changed,外部用户提交导致)。 +- 备注:该失败不由本执行器的写入造成,记录为外部漂移,不修改基线、不放宽门禁。 diff --git a/.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md b/.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md new file mode 100644 index 0000000..3117257 --- /dev/null +++ b/.planning/phases/09-minimum-browser-runnable/09-RUNNABILITY-AUDIT.md @@ -0,0 +1,78 @@ +# 09 期静态可运行性审计 + +## 结论 + +先回答能否运行:不能。当前快照**不能**在浏览器完成启动,也**不能**进入可玩状态。问题不是某一层还差功能,而是入口模块图存在多处相对导入断链、ClientCore 构造期存在字段未初始化崩溃、顶层 load 从未被调用,且 UI 仍依赖运行时未赋值的全局 main 与 core。 + +- 最小阻断集:P0-1、P0-2、P0-3、P0-4、P0-5、P0-6。 +- P0 数量:6 +- P1 数量:7 +- 判断依据:静态导入解析、构造调用顺序、入口调用图与一次时间点类型检查;未运行游戏、浏览器、dev 服务器或测试。 + +## P0 阻断 + +- **P0-1 client-modules 渲染层相对导入断链**:renderer.ts 从 ../shared 导入 MAIN_WIDTH 等常量,但 client-modules/src 下没有 shared.ts 或 shared/。证据:`packages-user/client-modules/src/render/renderer.ts:9`。影响:bootstrap 导入 @user/client-modules 时,index→render→renderer 的导出链在解析阶段失败,模块图无法建立。 +- **P0-2 client-base 两处相对导入断链**:elements/index.ts 从 ../renderer 导入 mainRenderer 与 tagManager,但 client-base/src/renderer.ts 不存在;floorSelect.tsx 从 ../../shared 导入常量,实际共享文件是 client-base/src/shared.ts。证据:`packages-user/client-base/src/elements/index.ts:4`、`packages-user/client-base/src/components/floorSelect.tsx:11`。影响:@user/client-base 被入口链依赖,任一断链都会让启动链在解析阶段失败。 +- **P0-3 基类构造期调用派生类 awake,派生字段尚未初始化**:CoreState 构造器末尾执行 this.awake(),ClientCore.awake 随即读取 this.orderedClientPlugins,而该字段在 super 之后才赋值;tsconfig 开启 useDefineForClassFields,派生字段在 super 期间不存在。证据:`packages-user/data-state/src/core.ts:155`、`packages-user/client-modules/src/client.ts:150`。影响:new ClientCore 在 super 内抛 TypeError,整局无法完成实例化。 +- **P0-4 顶层 load 从未被调用**:bootstrap 只打开 LoadSceneUI,LoadScene 直接迭代 loader.start();CoreState.load() 负责的 initialize 与 afterLoad 没有任何调用点。证据:`src/bootstrap.ts:16`、`src/client/ui/load.tsx:74`。影响:插件 initialize/afterLoad 不执行,勇士创建与存档初始化不会发生,跨层 wiring 断开。 +- **P0-5 UI 依赖运行时未赋值的全局 main 与 core**:core.d.ts 只做类型声明,运行期仅 bootstrap 赋值 window.mainClient。证据:`src/client/ui/title.tsx:131`、`src/client/ui/main.tsx:64`。影响:加载完成进入标题或主场景时立即访问 main.levelChoose 与 core.status,抛 ReferenceError。 +- **P0-6 save.tsx 导入不存在的命名导出**:save.tsx 从 client-modules 导入 getSave 与 SaveData,但该包导出面没有这两个名字。证据:`src/client/ui/save.tsx:23`。影响:title.tsx 静态导入 ./save,加载场景时浏览器 ESM 链接阶段报缺少导出。 + +## P1 集成缺口 + +- **P1-1 客户端内容钩子未注册**:RenderLoaderHooks 负责注册 texture/audio/animation/font 清单,但没有任何 addHook 调用它。证据:`packages-user/client-modules/src/loader/hook.ts:5`。影响:即使启动成功,客户端素材清单也不会被加载。 +- **P1-2 自定义元素注册未调用**:client-modules 导出 create(),其内部 createRender() 调用 createElements() 注册 icon/winskin/map-render 标签,但 src 下没有 create() 调用点。证据:`packages-user/client-modules/src/index.ts:3`。影响:主场景使用 map-render 等标签时会因未注册而失败。 +- **P1-3 数据内容只下载不落地**:map-list/enemy/item/tile 配置被注册为额外配置,但没有消费者把它们转换为地图、怪物、道具、图块数据,后续钩子仍是空壳。证据:`src/data/loader/hook.ts:16`、`src/data/hook/loader.ts:10`。影响:即使启动成功,样例地图与数据不会进入数据层。 +- **P1-4 渲染扩展导入不存在的 state 导出**:hero 扩展从 @user/data-state 导入 state 单例,但该包没有此导出。证据:`packages-user/client-base/src/map/extension/hero.ts:20`。影响:地图勇士扩展无法建立。 +- **P1-5 循环引用检查脚本指向不存在的入口**:check:circular 使用 madge --circular src/main.ts,但仓库只有 src/bootstrap.ts。证据:`package.json:31`。影响:构建与检查链中的该脚本必然失败,但不影响 dev 启动本身。 +- **P1-6 元素缓存与图标依赖旧全局 Mota**:createCache 与图标设置调用 Mota.require,运行期没有 Mota。证据:`packages-user/client-base/src/elements/cache.ts:491`、`packages-user/client-base/src/elements/misc.ts:89`。影响:元素注册接通后会在缓存或图标设置时抛 ReferenceError。 +- **P1-7 客户端内容资源缺失或清单不一致**:client.jsonc 声明的 titleImage/heroImage/titleBGM 在 src/content 下不存在;animation 清单列出 test 但无资源;font 清单列出 testFont.ttf 但无字体;主场景文本框使用 winskin.png,贴图目录也没有该文件。证据:`src/content/client.jsonc:4`、`src/content/animation/.list.jsonc:3`、`src/client/ui/main.tsx:47`。影响:内容钩子接通后会出现 404 或空素材。 + +## 预期中(不计为新缺陷) + +- 用户正在并发编辑 UI 与 setting 接口,settings.tsx、shop.tsx、main.tsx、scroll.tsx、status.ts、factories.ts 的类型错误属于接口未适配,按预期中处理,不作为新增阻断。 +- 测试夹具与新接口不一致导致的类型错误(enemy/hero/map 的 required 字段变化)属于测试未同步,不计为新缺陷。 +- map/renderer.ts 的 getLayerByAlias 缺失属于 data-base 与 client-base 接口未对齐,且该文件正在并发修改,按预期中处理。 +- 浏览器对个别 ESNext 内置方法的具体支持程度无法在本环境静态确认,未据此判定阻断。 + +## 最小可运行路径 + +1. 修复 P0-1:把 client-modules/src/render/renderer.ts 的 ../shared 改为实际存在的模块(如 @user/client-base 的 shared 或新建 shared.ts)。 +2. 修复 P0-2:把 client-base/src/elements/index.ts 的 ../renderer 改为实际渲染器入口;把 components/floorSelect.tsx 的 ../../shared 改为 ../shared。 +3. 修复 P0-3:不要在 CoreState 构造器里调用可被派生类覆写的 awake;把 ClientCore 的插件唤醒移到构造器字段就绪之后,或改为显式初始化方法。 +4. 接入 P0-4:让入口在合适时机调用 client.load()(或至少调用 state.initialize/afterLoad),而不是只在 LoadScene 里调用 loader.start()。 +5. 修复 P0-5 与 P0-6:移除 title/main/toolbar/settings 对 main、core、state 全局的依赖,改用 props.state;补齐或删除 save.tsx 对 getSave/SaveData 的导入。 +6. 接入 P1-1 与 P1-2:注册 RenderLoaderHooks,并在启动时调用一次 createElements()。 +7. 补齐 P1-3:实现 map-list/enemy/item/tile 的转换钩子,把配置写入对应 store。 +8. 处理 P1-4 至 P1-7:修正 hero 扩展的 state 导入、check:circular 入口、Mota 依赖与缺失素材,再进行真实浏览器验证。 + +## 覆盖清单 + +- 入口与导入:index.html 到 bootstrap 到 client/data entry 的主链存在,但存在 P0-1、P0-2、P0-6 三处断链。 +- core/main 残留:core.d.ts 仍声明 main、core、flags、hero、Mota;UI 仍在使用,运行期未赋值,见 P0-5、P1-6。 +- 内容加载:core/client 配置路径存在且可解析;数据配置只下载不落地,客户端素材清单未接入且缺资源,见 P1-1、P1-3、P1-7。 +- 渲染/UI:LoadScene、Title、MainScene 的打开链存在;被 P0-1、P0-2、P0-5 截断,自定义元素未注册,见 P1-2。 +- 事件:createEventRegistrations 已注入,GameEventSystem 构造已接,静态类型通过;未运行验证。 +- 战斗:initializeEnemy、战斗钩子与数据系统已接,静态类型通过;未运行验证。 +- 寻路:DefaultPassPredicateImpl、finder.useMapLayer、useMover 已接,静态类型通过;未运行验证。 +- 存档:addSaveableContent 与 save.init 代码存在,但依赖 afterLoad;当前入口不触发,见 P0-4。 +- 构建:vite base 与别名配置可用;check:circular 指向不存在的 src/main.ts,见 P1-5。 +- 工作区依赖:pnpm-workspace 覆盖 packages、packages-user、src;src/package.json 与各子包 workspace 依赖齐全;未安装或构建。 +- 系统级集成:插件注册顺序与构造顺序存在缺陷,客户端钩子与元素注册未接入,见 P0-3、P0-4、P1-1、P1-2。 + +## 证据与快照 + +- git HEAD:5b1f8664093f8f9687499ec092e3f3150a507f9f +- 脏文件数:79(采集瞬间,含用户并发编辑) +- 采集时间:2026-11-05 18:46:50(基线);类型检查 2026-11-05 18:47:19 至 18:47:24 +- tsc 命令:pnpm check:type(等价 vue-tsc --noEmit,经 pnpm.cmd 执行);退出码 2;错误行 258 条,其中非测试源 92 条;错误集中在测试夹具与并发编辑中的 UI/setting 接口。 +- 执行结束时 HEAD:b950c53f98804587058a9549a0178761efb4b89b(用户于 18:51:26 外部提交,属外部漂移,不归因于本执行器)。 +- 并发说明:用户正在同一工作树并发编辑接口,以上内容只代表采集瞬间;退出码非零属预期,不计为本执行器引入的新缺陷。 +- 环境说明:首次以 pnpm.ps1 调用被 PowerShell 执行策略拦截,改用 pnpm.cmd 后得到结果,这属于环境限制。 + +## 范围与并发说明 + +- 本执行器只写入 09-RUNNABILITY-AUDIT.md 与 09-49-SUMMARY.md 两个文档。 +- 未修改代码、配置、内容、STATE.md、ROADMAP.md 或其它既有计划与摘要;未提交。 +- 若发现基线之外的路径漂移,属于用户并发编辑造成的外部漂移,不归因于本执行器。 +- 两个目标文档的 .bak、.tmp 等后缀变体一律未写入。