mirror of
https://github.com/motajs/template.git
synced 2026-10-27 02:30:18 +08:00
fix(09-23): revise plan based on checker feedback
This commit is contained in:
parent
153bc8b373
commit
d30b5a74f6
@ -2,28 +2,29 @@
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 23
|
||||
type: execute
|
||||
wave: 20
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages/system/src/hotkey/command.ts
|
||||
- packages/system/src/hotkey/system.ts
|
||||
- packages/system/src/hotkey/type.ts
|
||||
- packages/system/src/hotkey/index.ts
|
||||
- packages/system/package.json
|
||||
- pnpm-lock.yaml
|
||||
- packages/common/src/logger.json
|
||||
- packages/system/src/hotkey/__test__/command.test.ts
|
||||
- packages/system/src/hotkey/__test__/system.test.ts
|
||||
- .planning/phases/09-minimum-browser-runnable/09-23-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
# 未校准估算(confidence: low),SUMMARY 须声明这一点
|
||||
tokens: 95000
|
||||
raw_tokens: 95000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「实现用户重新设计的按键系统」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(`command.ts` 新增、`system.ts` 新增、`type.ts` 核验、`index.ts` 导出、`packages/system/package.json` 补 `@motajs/animate`、两个测试文件、`09-23-SUMMARY.md`)、完整语义口径(默认模式 / 作用域 / 组合规则 / holding / 优先级与 block / 激励驱动时序 / listen)、验证方式、已知边界与待批准命名(OI-1 / OI-2 / OI-3 / OI-4),然后停下等待「可以执行」。"
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md / D-03)**:先汇报「实现用户重新设计的按键系统」的要做什么 / 为什么 / 大致怎么做 / 影响范围,列明最小范围(`command.ts` 新增、`system.ts` 新增、`type.ts` 核验、`index.ts` 导出、`packages/system/package.json` 补 `@motajs/animate`、`packages/common/src/logger.json` 在 `\"200\"` 与 `\"1001\"` 之间追加 warn 201–205(文案见 OI-5)、两个测试文件、`09-23-SUMMARY.md`)、完整语义口径(默认模式 / 作用域 / 组合规则 / holding / 优先级与 block / 激励驱动时序 / listen)、验证方式、已知边界与待批准命名(OI-1 / OI-2 / OI-3 / OI-4 / OI-5),然后停下等待「可以执行」。"
|
||||
- "**`type.ts` 为权威设计、不得改写(用户决策 1 / dev.md)**:`IHotkeyCommand` / `IHotkeySystem` / `IHotkeyCommandInit` / `IHotkeyEmitContext` / `IHotkeyEmitInfo` / `HotkeyEmitType` / `HotkeyEmitMode` 的定义与 jsDoc 一律逐字保留;**该接口已无 `emit` 成员**(HEAD `6c19e1f`「feat: 完善 Hotkey 类型」中已不存在),故用户决策 2「删除 `emit` 方法」以**核验代替删除**:若核验发现 `emit` 存在则只删该方法,其余成员不动;核验后 `type.ts` 与 HEAD 逐字节一致。"
|
||||
- "**`IExcitable<number>` 只加在类上(用户决策 1)**:`HotkeySystem` 的声明形如 `export class HotkeySystem implements IHotkeySystem, IExcitable<number>`;**`IHotkeySystem` 接口不得出现 `excited` 成员**,`type.ts` 不得因此新增任何接口成员。"
|
||||
- "**`addCommand` 语义(用户决策 5)**:`IHotkeyCommandInit` 的 `priority` / `block` 由构造器设置后**不可变**(类字段以 `readonly` 语义实现,接口本身即 `readonly`);`mode` 缺省为 `HotkeyEmitMode.KeyUp`,`timeout` / `interval` / `dt` 缺省为 0;`defaults` 深拷贝进指令(改调用方数组不影响指令);**重复 id 发 warn 204、不覆盖、返回已有对象**(注册表条目数不变)。"
|
||||
@ -33,26 +34,29 @@ must_haves:
|
||||
- "**KeyDown 类模式(Once / Continuous / Throttle)组合规则(用户决策 5)**:按键组合是 `holding` 的**子集**即匹配(`keys ⊆ holding`,组合为空视为不匹配);**修饰键只有写进 `keys` 才构成限制**(例如组合 `[KeyS]` 在按住 Ctrl 时仍触发;组合 `[Ctrl, KeyS]` 必须 Ctrl 与 S 同时按住)。"
|
||||
- "**`holding` 与上下文(用户决策 5)**:`down` / `up` 由系统自己维护 `holding`(按按下顺序的数组、去重);辅助键 `Ctrl` / `Shift` / `Alt` / `Meta` 同样进入 `holding`;`ctx.ctrl` / `shift` / `alt` / `meta` 由 `holding` 推导;**KeyDown 的 `holding` 含触发键,KeyUp 的 `holding` 不含被抬起的键**;浏览器 `event.repeat` 一律忽略(`listen` 中跳过,`down` 再去重兜底);`up` 针对未处于 `holding` 的键直接返回 `false`、不派发。"
|
||||
- "**优先级与 block(用户决策 5)**:同一触发批次内按 `priority` **降序**执行;同优先级全部触发(不保证相互顺序);`block === true` 且**实际触发**的指令阻断**严格更低**优先级;**block 只按当前作用域内已实现且实际触发的指令计算**——匹配但无实现的指令既不触发也不阻断;该规则对 `down` / `up` 与激励驱动的批次一视同仁。"
|
||||
- "**时序完全由激励源驱动(用户决策 5)**:时间取值一律是 `excited(payload)` 的 `payload`(内部记录最近一次 `now`,初值 0),**不使用任何浏览器定时器**;`timeout === 0` 时首次触发在 `down` / `up` 当刻执行,`timeout > 0` 时进入待触发数组、之后每个 `excited` 检查 `payload - 入队时间 >= timeout`;`interval` 为首次与第二次的最小间隔;`dt` 为 Throttle 下第二次之后相邻两次的最小间隔;`Once` 只触发一次;`KeyUp` 模式只在抬起时匹配、单次触发;`down` / `up` 返回「是否至少有一个已实现指令被触发(含将要延时触发的)」。"
|
||||
- "**时序完全由激励源驱动(用户决策 5)**:时间取值一律是 `excited(payload)` 的 `payload`(内部记录最近一次 `now`,初值 0),**不使用任何浏览器定时器**;`timeout === 0` 时首次触发在 `down` / `up` 当刻执行,`timeout > 0` 时进入待触发数组、之后每个 `excited` 检查 `payload - 入队时间 >= timeout`;`interval` 为首次与第二次的最小间隔;`dt` 为 Throttle 下第二次之后相邻两次的最小间隔;`Once` 只触发一次;`KeyUp` 模式只在抬起时匹配、单次触发;`down` / `up` 返回「是否至少有一个已实现指令被触发(含将要延时触发的)」;**延时项在真正触发时按当时的 `holding` 重新构造 `context`**(`createContext(pending.type, pending.key)`,不使用入队时的 `context` 快照),与 `type.ts` 中「当前所有正在按下的按键」的语义一致。"
|
||||
- "**后续触发与清理(用户决策 5 + 本计划假设)**:`Continuous` 首次触发后每次 `excited` 均触发;`Throttle` 首次后相邻两次间隔不小于 `dt`;待触发 / 持续项在以下三种情况被丢弃:**组合不再被 `holding` 满足**(KeyDown 类模式;KeyUp 类延时项不受此限)、**作用域已不是入队时的栈顶作用域**、**指令当前 `mode` 与入队时快照不同**。"
|
||||
- "**`excitedBy` / `listen`(用户决策 5)**:`excitedBy(excitation)` 只保留一个激励源,重复调用先 `revoke` 旧控制器再 `excitation.add(this)`(`IExcitable<number>` 只在类上实现);`listen(target)` 在 `target` 上注册 `keydown` / `keyup`,用 `EVENT_KEY_CODE_MAP[event.keyCode]` 映射(无映射跳过),`keydown` 忽略 `repeat`,`down` / `up` 返回 `true` 时 `preventDefault()`;`listen(null)` 移除监听;`listening` 暴露当前元素;同元素重复 `listen` 为空操作。"
|
||||
- "**`excitedBy` / `listen`(用户决策 5)**:`excitedBy(excitation)` 只保留一个激励源,重复调用先 `revoke` 旧控制器再 `excitation.add(this)`(`IExcitable<number>` 只在类上实现);`listen(target)` 在 `target` 上注册 `keydown` / `keyup`,**先做一个本地的手写映射把 `event.keyCode` 的 `91` / `92` / `224` 视为 `KeyCode.Meta`**(`EVENT_KEY_CODE_MAP` 没有任何 Meta 事件码映射,不改 `keyCodes.ts`),其余仍用 `EVENT_KEY_CODE_MAP[event.keyCode]` 映射(无映射跳过),`keydown` 忽略 `repeat`,`down` / `up` 返回 `true` 时 `preventDefault()`;`listen(null)` 移除监听;`listening` 暴露当前元素;同元素重复 `listen` 为空操作。"
|
||||
- "**依赖与导出(用户决策 3 / 1)**:`packages/system/package.json` 的 `dependencies` 补 `@motajs/animate: workspace:*`(`@motajs/common` 保留),并据 `pnpm install --lockfile-only` 更新 `pnpm-lock.yaml`(`packages/system` 导入器出现 animate 链接);`hotkey/index.ts` 追加 `export * from './command';` 与 `export * from './system';`,其余导出行不动;旧实现 `hotkey.ts` / `keyboard.ts` **零改动**。"
|
||||
- "**测试是必需产物(用户决策 + AGENTS.md)**:`packages/system/src/hotkey/__test__/command.test.ts` 与 `__test__/system.test.ts`,**每个 `it` 前一行注释**说明该用例覆盖什么;Node 环境无真实 DOM,`listen` 用最小 fake 元素(记录注册的监听器并手动派发伪事件);断言用 `vi.spyOn(logger, 'warn')` 校验 201–205 的码与参数,不依赖控制台文本。"
|
||||
- "**验证可运行且范围诚实(Nyquist)**:`cmd /c \"pnpm exec vitest run <两个测试文件>\"`、`cmd /c \"pnpm exec eslint <改动文件>\"`、按文件过滤的类型闸门(`vue-tsc --noEmit` 全仓现有 221 行错误中 `hotkey.ts` / `keyboard.ts` 各有 1 条 legacy 错误,属既定边界,闸门只对 `command.ts` / `system.ts` / `index.ts` / `__test__/**` 报错即失败)、结构闸门(导出形态、注释剥离后的负向断言、`it` 前注释、未改旧实现)与 `git status` 范围闸门;**不端到端跑 `build:game`**,不接线 `packages-user` 的 `IClientHotkey`。"
|
||||
- "**命名须先获批(AGENTS.md / D-04)**:OI-1(测试文件路径与文件名)、OI-2(导出的辅助函数名 `isAuxiliaryKey`)、OI-3(内部接口 / 类型别名 / 私有成员命名清单)、OI-4(三处语义确认项)。未获批前不得落码。"
|
||||
- "**日志文案投递(OI-5)**:`packages/common/src/logger.json` 的 `warn` 段在 `\"200\"` 之后、`\"1001\"` 之前追加 **201 / 202 / 203 / 204 / 205** 五条,文本**逐字**采用 OI-5 的批准版本;既有条目(含 `200` 与 `1001`)一字不动、不新增其它码。这条是功能必需:`Logger.warn` 在 `info.warn[code]` 缺失时会退化成 `error 16`,缺了它 201–205 全部失效。结构闸门 `JSON.parse` 后逐条比对文本,并断言 `warn` 段键数为 206、`200` / `1001` 前缀不变。"
|
||||
- "**测试是必需产物(用户决策 + AGENTS.md)**:`packages/system/src/hotkey/__test__/command.test.ts` 与 `__test__/system.test.ts`,**每个 `it` 前一行注释**说明该用例覆盖什么;Node 环境无真实 DOM,`listen` 用最小 fake 元素(记录注册的监听器并手动派发伪事件);断言用 `vi.spyOn(logger, 'warn')` 校验 201–205 的码与参数,不依赖控制台文本。**测试只从实现模块导入**:`import { HotkeyCommand, isAuxiliaryKey } from '../command'` 与 `import { HotkeySystem } from '../system'`;**禁止 `../index`**(`hotkey.ts` 在模块顶层执行 `document.addEventListener`,Node 环境下导入即炸),结构闸门对此做负向断言。"
|
||||
- "**验证可运行且范围诚实(Nyquist)**:`cmd /c \"pnpm exec vitest run <两个测试文件>\"`、`cmd /c \"pnpm exec eslint <改动文件>\"`、按文件过滤的类型闸门(`vue-tsc --noEmit` 全仓现有 221 行错误中 `hotkey.ts` / `keyboard.ts` 各有 1 条 legacy 错误,属既定边界,闸门只对 `command.ts` / `system.ts` / `index.ts` / `__test__/**` 报错即失败;**闸门必须先校验 `spawnSync` 的 `out.error` / `out.status` / `out.signal`**,命令没正常跑完(超时、找不到 `cmd`、进程被杀)一律失败,不能只看过滤后的输出行)、结构闸门(导出形态、`logger.json` 的 201–205 逐字文案、注释剥离后的负向断言、测试导入路径、`it` 前注释、未改旧实现)、范围闸门(白名单由 `files_modified` 派生、含 `logger.json`,用户既有 WIP 显式豁免;禁改清单含 `hotkey.ts` / `keyboard.ts` / `type.ts` / `keyCodes.ts`);**不端到端跑 `build:game`**,不接线 `packages-user` 的 `IClientHotkey`。"
|
||||
- "**命名与文案须先获批(AGENTS.md / D-04)**:OI-1(测试文件路径与文件名)、OI-2(导出的辅助函数名 `isAuxiliaryKey`)、OI-3(内部接口 / 类型别名 / 私有成员命名清单,含 `HotkeyCommand` 的 `combinations` 与私有方法 `resolveKeyCode` / `isPendingDue`)、OI-4(四处语义确认项)、OI-5(warn 201–205 的日志文案,逐字报备)。未获批前不得落码。"
|
||||
artifacts:
|
||||
- path: packages/system/src/hotkey/command.ts
|
||||
provides: "`export class HotkeyCommand implements IHotkeyCommand`:构造器接收指令 id 与 `IHotkeyCommandInit`(`priority` / `block` 只读,`mode` 缺省 `KeyUp`,`timeout` / `interval` / `dt` 缺省 0,`defaults` 深拷贝并经 `setKeys` + `setKeyCombination` 落地);按键组合数组(`length` 同步)、`setKeys` 扩容补空组合 / 缩容截断、`setKeyCombination`(越界索引忽略、写入副本、`mode` 为 `KeyUp` 时校验并发 warn 203)、`getKeyCombination`(越界返回空数组、返回副本)、`iterateKeyCombinations`(索引升序、产出副本);`keyUp` / `once` / `continuous` / `throttle` 设置 `mode` 与对应时间参数(未传参时保留现值),`keyUp()` 额外校验现有组合并在存在非法组合时发一次 warn 203;导出辅助函数 `isAuxiliaryKey`(`Ctrl` / `Shift` / `Alt` / `Meta` 判定)供 `system.ts` 复用(命名待批准,OI-2)"
|
||||
provides: "`export class HotkeyCommand implements IHotkeyCommand`:构造器接收指令 id 与 `IHotkeyCommandInit`(`priority` / `block` 只读,`mode` 缺省 `KeyUp`,`timeout` / `interval` / `dt` 缺省 0,`defaults` 深拷贝并经 `setKeys` + `setKeyCombination` 落地);按键组合数组 `combinations`(`length` 同步)、`setKeys` 扩容补空组合 / 缩容截断、`setKeyCombination`(越界索引忽略、写入副本、`mode` 为 `KeyUp` 时校验并发 warn 203)、`getKeyCombination`(越界返回空数组、返回副本)、`iterateKeyCombinations`(索引升序、产出副本);`keyUp` / `once` / `continuous` / `throttle` 设置 `mode` 与对应时间参数(未传参时保留现值),`keyUp()` 额外校验现有组合并在存在非法组合时发一次 warn 203;导出辅助函数 `isAuxiliaryKey`(`Ctrl` / `Shift` / `Alt` / `Meta` 判定)供 `system.ts` 复用(命名待批准,OI-2)"
|
||||
- path: packages/system/src/hotkey/system.ts
|
||||
provides: "`export class HotkeySystem implements IHotkeySystem, IExcitable<number>`:指令注册表(`addCommand` 含 warn 204 与返回已有对象、`getCommand`、`deleteCommand` 连带清理各作用域实现与待触发项、`iterateCommands`);作用域栈(`enterScope` / `leaveScope` 含 warn 205、`when` / `implement` 含 warn 201 / 202、仅栈顶作用域生效);`holding` 维护与上下文构造;统一派发(`down` / `up` → 收集栈顶已实现匹配 → 优先级降序 → block 只按实际触发计算 → 立即执行或入队);KeyUp 组合匹配;KeyDown 子集匹配;`excited` / `excitedBy` 的激励驱动时序(timeout / interval / dt,无定时器);`listen` 的映射、`repeat` 跳过、`preventDefault`、`listening` 与解绑"
|
||||
provides: "`export class HotkeySystem implements IHotkeySystem, IExcitable<number>`:指令注册表(`addCommand` 含 warn 204 与返回已有对象、`getCommand`、`deleteCommand` 连带清理各作用域实现与待触发项、`iterateCommands`);作用域栈(`enterScope` / `leaveScope` 含 warn 205、`when` / `implement` 含 warn 201 / 202、仅栈顶作用域生效);`holding` 维护与上下文构造;统一派发(`down` / `up` → 收集栈顶已实现匹配 → 优先级降序 → block 只按实际触发计算 → 立即执行或入队);KeyUp 组合匹配;KeyDown 子集匹配;`excited` / `excitedBy` 的激励驱动时序(timeout / interval / dt,无定时器);`listen` 的映射(含 `91` / `92` / `224` → `KeyCode.Meta` 的本地 `resolveKeyCode`)、`repeat` 跳过、`preventDefault`、`listening` 与解绑;待触发项不保存 `context`,触发时按当时 `holding` 重算"
|
||||
- path: packages/system/src/hotkey/type.ts
|
||||
provides: "权威设计文件:**保持零改动**(接口已无 `emit` 成员);仅在核验发现 `emit` 方法存在时删除该方法,其余成员与 jsDoc 逐字保留"
|
||||
provides: "权威设计文件:**保持零改动**(接口已无 `emit` 成员);仅在核验发现 `emit` 方法存在时删除该方法,其余成员与 jsDoc 逐字保留。**故意不在 `files_modified` 中**:它由 LEGACY / SCOPE 闸门的零改动断言守护,而不是作为交付物"
|
||||
- path: packages/system/src/hotkey/index.ts
|
||||
provides: "追加 `export * from './command';` 与 `export * from './system';`,保留既有 4 行导出(`hotkey` / `keyboard` / `keyCodes` / `type`)"
|
||||
- path: packages/system/package.json
|
||||
provides: "`dependencies` 增加 `\"@motajs/animate\": \"workspace:*\"`(置于 `@motajs/common` 之前,保持 4 空格缩进)"
|
||||
- path: pnpm-lock.yaml
|
||||
provides: "`packages/system` 导入器新增 animate 依赖链接(仅由 `pnpm install --lockfile-only` 生成,不手改)"
|
||||
- path: packages/common/src/logger.json
|
||||
provides: "`warn` 段在 \"200\" 与 \"1001\" 之间追加 201–205 五条,文本逐字采用 OI-5;既有条目(`200` / `1001` 等)零改动。**这是功能必需**:`Logger.warn` 在 `info.warn[code]` 缺失时退化为 `error 16`,不追加则 201–205 全部失效"
|
||||
- path: packages/system/src/hotkey/__test__/command.test.ts
|
||||
provides: "指令对象单测:缺省值 / defaults 深拷贝 / `setKeys` 扩缩容 / `setKeyCombination` 越界与副本 / `iterateKeyCombinations` / KeyUp 非法组合 warn 203(`setKeyCombination` 与 `keyUp()` 两个时机)/ 四个模式方法的字段与 `mode` 变更;每个 `it` 前一行注释"
|
||||
- path: packages/system/src/hotkey/__test__/system.test.ts
|
||||
@ -64,16 +68,16 @@ must_haves:
|
||||
- "`HotkeyCommand` 的 `mode`(可经 `keyUp` / `once` / `continuous` / `throttle` 变更)↔ `system.ts` 的 `down` / `up` 路由 —— 同一指令在 KeyUp 与 KeyDown 两类派发中不会重复触发"
|
||||
- "`isAuxiliaryKey`(`command.ts`)↔ `system.ts` 的 KeyUp 组合拆分与 KeyDown 子集判定 —— 辅助键集合(含 `Meta`)只定义一次,避免两套口径"
|
||||
- "`holding`(`system.ts`)↔ `IHotkeyEmitContext` 的 `holding` / `ctrl` / `shift` / `alt` / `meta` —— KeyDown 含触发键、KeyUp 不含被抬起键,四个布尔值全部由 `holding` 推导"
|
||||
- "待触发项(入队快照:`mode` / `timeout` / `interval` / `dt` / 作用域 `symbol` / `holding` 上下文)↔ `excited(payload)` 的 `payload` —— 时间推进、丢弃与首次 / 后续间隔判定的唯一入口"
|
||||
- "待触发项(入队时记录的快照:`mode` / `timeout` / `interval` / `dt` / 作用域 `symbol` / `type` / `key`,**不保存 `context`**)↔ `excited(payload)` 的 `payload` —— 时间推进、丢弃与首次 / 后续间隔判定的唯一入口;真正触发时用当时的 `holding` 重新构造 `context`"
|
||||
- "`excitedBy` 持有的 `IExcitableController` ↔ `excitation.add(this)` —— 切换激励源时先 `revoke`,保证只有一个激励源在驱动本系统"
|
||||
- "`listen` 注册的 `keydown` / `keyup` 与 `EVENT_KEY_CODE_MAP`(`keyCodes.ts`)↔ `down` / `up` 的布尔返回 —— 只有真正触发(含入队)时才 `preventDefault()`"
|
||||
- "`listen` 注册的 `keydown` / `keyup`(含 `91` / `92` / `224` → `KeyCode.Meta` 的本地映射)与 `EVENT_KEY_CODE_MAP`(`keyCodes.ts`)↔ `down` / `up` 的布尔返回 —— 只有真正触发(含入队)时才 `preventDefault()`;`keyCodes.ts` 本身零改动"
|
||||
- "`hotkey/index.ts` 的新增导出 ↔ `packages-user/client-base/src/hotkey/types.ts` 中用户 WIP 的 `IClientHotkey.system: IHotkeySystem` —— 本计划只提供实现,不接线"
|
||||
assumptions:
|
||||
- "**`type.ts` 已无 `emit`(已核验):** 当前 HEAD `6c19e1f` 的 `IHotkeySystem` 只有 `listening` / `excitedBy` / `enterScope` / `leaveScope` / `getCommand` / `addCommand` / `deleteCommand` / `iterateCommands` / `when` / `implement` / `down` / `up` / `listen`,无 `emit`;因此用户决策 2 的落地形态是「核验 + 零改动」。"
|
||||
- "**测试收集与解析:** 根 `vite.config.ts` 的 `test` 未设 `include`,vitest 默认收集 `**/*.test.ts`,故 `packages/system/src/hotkey/__test__/*.test.ts` 会被 `vitest run <路径>` 直接命中;`@motajs/*` 前缀由 `vite.config.ts` 的 glob 别名指向 `packages/*/src`,`tsconfig.json` 的 `paths` 同理,故新增依赖声明不影响测试解析。"
|
||||
- "**类型闸门基线(已实测):** `cmd /c \"pnpm exec vue-tsc --noEmit\"` 当前退出码 2、共 221 行错误,其中与本计划相关目录只有 `hotkey.ts` / `keyboard.ts` 各 1 条 legacy 错误(`Module '@motajs/client-base' has no exported member 'KeyCode'`)。闸门按文件过滤到 `command.ts` / `system.ts` / `index.ts` / `__test__/**`,避免被既有 legacy 错误与用户 WIP 干扰。"
|
||||
- "**`logger` 在 Node 下可用:** `packages/common/src/logger.ts` 用 `typeof document` / `typeof main` 守卫浏览器宿主;测试沿用既有约定 `vi.hoisted` 里 `vi.stubGlobal('main', { replayChecking: true })` 与 `vi.stubGlobal('location', { origin: 'http://localhost' })`;断言直接 spy `logger.warn`。"
|
||||
- "**`EVENT_KEY_CODE_MAP` 为稀疏数组(`new Array(230)`):** 无映射的 `keyCode` 取到 `undefined`,`KeyCode.Unknown` 为 0;两者统一按「无映射」跳过(写作 `if (!key) return;`,不引入 `lodash-es`——`packages/system` 不声明该依赖,用户决策 3 只批准补 `@motajs/animate`)。"
|
||||
- "**`logger` 在 Node 下可用:** `packages/common/src/logger.ts` 用 `typeof document` / `typeof main` 守卫浏览器宿主;测试沿用既有约定 `vi.hoisted` 里 `vi.stubGlobal('main', { replayChecking: true })` 与 `vi.stubGlobal('location', { origin: 'http://localhost' })`;断言直接 spy `logger.warn`。**`logger.warn` 的码必须存在于 `logger.json` 的 `warn` 段**,否则会转报 `error 16`,因此 201–205 的追加是 Task 1 的交付项而不是文档动作。"
|
||||
- "**`EVENT_KEY_CODE_MAP` 为稀疏数组(`new Array(230)`)且没有 Meta 事件码:** 无映射的 `keyCode` 取到 `undefined`,`KeyCode.Unknown` 为 0;两者统一按「无映射」跳过(写作 `if (!key) return;`,不引入 `lodash-es`——`packages/system` 不声明该依赖,用户决策 3 只批准补 `@motajs/animate`)。实测 Meta 的两条映射的 `eventKeyCode` 都是 0(`MetaLeft` / `MetaRight`),浏览器实际派发的是 `91` / `92`(Windows)或 `224`(部分 Mac 浏览器),因此**只有 `listen` 内把 `91` / `92` / `224` 本地映射为 `KeyCode.Meta`,Meta 才能进入 `holding` 并让 `ctx.meta` 为真**;`keyCodes.ts` 零改动。"
|
||||
- "**同一事件对同一指令只触发一次:** 一条指令可能有多组组合同时命中(如 `[KeyS]` 与 `[Ctrl, KeyS]`),本计划取**最小匹配索引**触发一次,避免同一按键事件重复执行同一指令。"
|
||||
- "**组合读取返回副本:** `getKeyCombination` / `iterateKeyCombinations` 返回拷贝,调用方修改不影响指令内部组合;`setKeyCombination` 同样写入拷贝。"
|
||||
- "**作用域实现随作用域一起消失:** `leaveScope` 删除的作用域中的实现与条件不再参与判定;同时丢弃属于已退出作用域或父作用域的待触发项,保证「只在栈顶生效」的一致语义。"
|
||||
@ -85,7 +89,7 @@ must_haves:
|
||||
- "**不得改 `type.ts` 的既有定义:** 接口成员、`const enum`、jsDoc 一律逐字保留;除「`emit` 方法存在时删除该方法」外不得有任何改动,也不得因为 `IExcitable<number>` 而往 `IHotkeySystem` 加成员。"
|
||||
- "**不得改旧实现:** `packages/system/src/hotkey/hotkey.ts` 与 `keyboard.ts` 零改动(含其 `document.addEventListener` 顶层副作用、`isAssist` 等导出),即使它们与新模式命名相近。"
|
||||
- "**不得用定时器驱动时序:** 时间只能来自 `excited(payload)` 的 `payload`;不得引入任何浏览器定时器或 `Date.now()` 计时(`Date.now()` 只允许不出现)。"
|
||||
- "**不得新增日志码:** 只使用已批准的 warn 201 / 202 / 203 / 204 / 205,消息文本逐字采用用户批准版本;不得改 `logger.json` 既有条目、不得新增 error 段条目。"
|
||||
- "**日志码只允许追加 201–205,且文案逐字采用 OI-5:** 唯一允许的 `logger.json` 改动是在 `warn` 段 `\"200\"` 与 `\"1001\"` 之间插入 201 / 202 / 203 / 204 / 205 五条,文本与 OI-5 逐字符一致;不得改任何既有条目(含 `200` / `1001`)、不得新增其它码、不得新增 error 段条目。"
|
||||
- "**不得新增外部依赖:** 除 `@motajs/animate`(workspace)外不改 `packages/system/package.json`;不引入 `lodash-es`、不手改 `pnpm-lock.yaml`(只由 `pnpm install --lockfile-only` 生成)。"
|
||||
- "**不得越界接线:** 不改 `packages-user/**`(含用户 WIP 的 `client-base/src/hotkey/types.ts`)、不改 `src/**`、`public/**`、`script/**`、`vite.config.ts`、`tsconfig.json`、`eslint.config.js`、`dev.md`、`AGENTS.md`。"
|
||||
- "**不得跑全量构建或全仓类型修复:** 不执行 `build:game` / `build:packages` / `check:type` 的修复动作,不修 `hotkey.ts` / `keyboard.ts` 的 legacy 类型错误(09-21 已记录为既定边界)。"
|
||||
@ -96,8 +100,9 @@ must_haves:
|
||||
open_items:
|
||||
- "**OI-1(测试文件路径与文件名):** 提议 `packages/system/src/hotkey/__test__/command.test.ts` 与 `packages/system/src/hotkey/__test__/system.test.ts`(与被测文件同名、camelCase、与 `packages-user/*/src/**/__test__` 既有约定一致)。备选:合并为 `__test__/hotkey.test.ts`,或按行为命名(`keyUp.test.ts` / `mode.test.ts`)。**报备待批准**。"
|
||||
- "**OI-2(导出的辅助函数):** 提议 `export function isAuxiliaryKey(key: KeyCode): boolean`(`Ctrl` / `Shift` / `Alt` / `Meta`),供 `system.ts` 复用;备选:`isModifierKey`、`isAssistKey`(注意 `hotkey.ts` 已有 `isAssist`,仅覆盖三键、语义不同,不复用)。**待批准**。"
|
||||
- "**OI-3(内部命名清单,均不导出):** 接口 `IHotkeyScope`(`symbol` / `condition` / `handlers`)、`IHotkeyMatch`(`id` / `command` / `index` / `keys` / `handler` / 可选 `entry`)、`IHotkeyPending`(`id` / `command` / `index` / `keys` / `handler` / `context` / `scope` / `mode` / `timeout` / `interval` / `dt` / `time` / `last` / `fired`);类型别名 `HotkeyHandler`;`HotkeyCommand` 私有字段 `command`(指令 id,供 warn 203 的 `$1`);`HotkeySystem` 私有字段 `commands` / `scopes` / `holding` / `pending` / `now` / `controller` / `keyDownListener` / `keyUpListener`,私有方法 `getScope` / `createContext` / `dispatch` / `runMatches` / `matchKeyDown` / `matchKeyUp` / `flushPending` / `dropPending`。**待批准**(若不批准则按用户命名,门禁不受影响)。"
|
||||
- "**OI-4(三处语义请确认,未回复即按提议执行):** ①**松键取消待触发项**:KeyDown 类模式(Once / Continuous / Throttle)的待触发项在其组合不再被 `holding` 满足时丢弃(含 `Once` 的 `timeout` 未到就松键的情形),KeyUp 类的延时项不受此限;②**同一事件同一指令只触发一次**(多组组合同时命中时取最小索引);③**空栈无参 `leaveScope()`** 也发 warn 205(复用同一条「不存在作用域」消息)。"
|
||||
- "**OI-3(内部命名清单,均不导出):** 接口 `IHotkeyScope`(`symbol` / `condition` / `handlers`)、`IHotkeyMatch`(`id` / `command` / `index` / `keys` / `handler` / 可选 `entry`)、`IHotkeyPending`(`id` / `command` / `index` / `keys` / `handler` / `type` / `key` / `scope` / `mode` / `timeout` / `interval` / `dt` / `time` / `last` / `fired`,**不含 `context`**——触发时按当时 `holding` 重建);类型别名 `HotkeyHandler`;`HotkeyCommand` 私有字段 **`command`(指令 id,供 warn 203 的 `$1`)** 与 **`combinations`(二维 `KeyCode[][]` 组合数组,`length` 与它同步)**;`HotkeySystem` 私有字段 `commands` / `scopes` / `holding` / `pending` / `now` / `controller` / `keyDownListener` / `keyUpListener`,私有方法(Task 1)`getScope` / `createContext` / **`resolveKeyCode`** / `dispatch` / `runMatches` / `matchKeyDown` / `matchKeyUp` / `flushPending` / `dropPending`,Task 2 新增私有方法 **`isPendingDue`(单个待触发项的到期判定,供 `flushPending` 复用;不再新增其它私有方法)**。**待批准**(若不批准则按用户命名,门禁不受影响)。"
|
||||
- "**OI-4(四处语义请确认,未回复即按提议执行):** ①**松键取消待触发项**:KeyDown 类模式(Once / Continuous / Throttle)的待触发项在其组合不再被 `holding` 满足时丢弃(含 `Once` 的 `timeout` 未到就松键的情形),KeyUp 类的延时项不受此限;②**同一事件同一指令只触发一次**(多组组合同时命中时取最小索引);③**空栈无参 `leaveScope()`** 也发 warn 205(复用同一条「不存在作用域」消息);④**延时项的 `context` 按触发时刻重算**:`flushPending` 触发时用当时的 `holding` 调 `createContext(pending.type, pending.key)`,**不使用入队时的 `context` 快照**(对齐 `type.ts` 第 29 行「当前所有正在按下的按键,按照按下的顺序排列」;因此延时触发能看到期间新按下的键,也不会把已松开的键算进去)。"
|
||||
- "**OI-5(warn 201–205 的日志文案,逐字采用,待批准):** 下列五条必须**逐字符**写入 `packages/common/src/logger.json` 的 `warn` 段(`\"200\"` 之后、`\"1001\"` 之前):201 `Cannot set hotkey condition or implement command since no scope is active.`;202 `Cannot implement nonexistent hotkey command '$1'.`;203 `Hotkey command '$1' has an invalid KeyUp combination: at most one non-auxiliary key is allowed.`;204 `Duplicate hotkey command '$1', the existing command will be used.`;205 `Cannot leave nonexistent hotkey scope.`。201 / 205 无占位符;202 / 203 / 204 的 `$1` 就是指令 id(`HotkeyCommand` 的私有 `command` 字段 / `addCommand` 的 `command` 参数)。**待批准**;若用户改文案,先按新文案修订本计划(含结构闸门的逐字比对)再执行。"
|
||||
- "**已知边界(不在本计划):** ①不接线 `packages-user/client-base/src/hotkey`(用户 WIP 的 `IClientHotkey.system` 只依赖接口,不依赖实现);②不端到端跑 `build:game`(09-21 / 09-22 已记录:渲染端仍有用户 WIP 的 stale import);③`hotkey.ts` / `keyboard.ts` 的 2 条 legacy 类型错误保持原状;④v1 不提供存档序列化(`type.ts` 第 4 行的 TODO 仍保留)。"
|
||||
---
|
||||
<objective>
|
||||
@ -109,12 +114,13 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
- `packages/system/src/hotkey/command.ts`(新):`HotkeyCommand implements IHotkeyCommand`,负责按键组合集合、四个模式方法(`keyUp` / `once` / `continuous` / `throttle`)与 KeyUp 非法组合告警 203;导出辅助函数 `isAuxiliaryKey` 供系统复用。
|
||||
- `packages/system/src/hotkey/system.ts`(新):`HotkeySystem implements IHotkeySystem, IExcitable<number>`,负责指令注册表、作用域栈(`when` / `implement` 只在栈顶生效)、`holding` 维护、统一派发(优先级 / block)、KeyUp 与 KeyDown 两套组合规则、由激励源驱动的 `timeout` / `interval` / `dt` 时序、`excitedBy` 与 `listen`。
|
||||
- `packages/system/src/hotkey/index.ts`:追加两个导出;`packages/system/package.json` + `pnpm-lock.yaml`:补 `@motajs/animate`(workspace)。
|
||||
- `type.ts`:核验已无 `emit` 成员(HEAD `6c19e1f`),保持零改动;`hotkey.ts` / `keyboard.ts` **原样保留**。
|
||||
- 测试:`packages/system/src/hotkey/__test__/command.test.ts` 与 `__test__/system.test.ts`(Node 环境、fake 元素、每个 `it` 前一行注释)。
|
||||
- `packages/common/src/logger.json`:在 `warn` 段 `"200"` 与 `"1001"` 之间追加 201–205 五条(文案逐字采用 OI-5,不改既有条目)——没有这五条,`Logger.warn` 会退化成 `error 16`。
|
||||
- `type.ts`:核验已无 `emit` 成员(HEAD `6c19e1f`),保持零改动(**不在 `files_modified`**,由零改动闸门守护);`hotkey.ts` / `keyboard.ts` **原样保留**。
|
||||
- 测试:`packages/system/src/hotkey/__test__/command.test.ts` 与 `__test__/system.test.ts`(Node 环境、fake 元素、每个 `it` 前一行注释、只从 `../command` / `../system` 导入)。
|
||||
|
||||
**约束:** 只动上述文件;不改旧实现、不改 `type.ts` 定义、不接线 user 代码、不跑全量构建、不新增外部依赖。
|
||||
**约束:** 只动 `files_modified` 内的文件;不改旧实现、不改 `type.ts` 定义、不改 `logger.json` 既有条目、不接线 user 代码、不跑全量构建、不新增外部依赖。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含文件写入(新增 2 个实现文件 + 2 个测试文件 + SUMMARY,修改 `index.ts` / `package.json` / `pnpm-lock.yaml`),由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
|
||||
**修改方式声明(AGENTS.md):** 本计划执行含文件写入(新增 2 个实现文件 + 2 个测试文件 + SUMMARY,修改 `index.ts` / `package.json` / `pnpm-lock.yaml` / `logger.json`),由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@ -147,14 +153,14 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
<tasks>
|
||||
<task type="checkpoint:decision" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报新按键系统的实现范围、完整语义口径、验证方式与待批准命名,等待「可以执行」</name>
|
||||
<precondition>`packages/system/src/hotkey/type.ts` 为 HEAD `6c19e1f` 的权威设计且 `IHotkeySystem` 无 `emit` 成员;`logger.json` 的 `warn` 段最大编号为 200(201–205 空闲);`packages/system/package.json` 仅声明 `@motajs/common`;`@motajs/animate` 包存在且 `packages/animate/src/types.ts` 含 `IExcitable` / `IExcitableController` / `IExcitation`;`command.ts` / `system.ts` / `hotkey/__test__/` 均尚不存在。</precondition>
|
||||
<precondition>`packages/system/src/hotkey/type.ts` 为 HEAD `6c19e1f` 的权威设计且 `IHotkeySystem` 无 `emit` 成员;`logger.json` 的 `warn` 段编号为 1–200 与 1001(**201–205 空闲**,`1001` 位于 `200` 之后);`packages/system/package.json` 仅声明 `@motajs/common`;`@motajs/animate` 包存在且 `packages/animate/src/types.ts` 含 `IExcitable` / `IExcitableController` / `IExcitation`;`command.ts` / `system.ts` / `hotkey/__test__/` 均尚不存在。</precondition>
|
||||
<files>(不改仓库文件)</files>
|
||||
<read_first>
|
||||
- packages/system/src/hotkey/type.ts(权威设计:`IHotkeyCommand` / `IHotkeySystem` / `IHotkeyCommandInit` / `IHotkeyEmitContext` / `IHotkeyEmitInfo` / `HotkeyEmitMode`)
|
||||
- packages/system/src/hotkey/keyCodes.ts(`KeyCode` 枚举与 `EVENT_KEY_CODE_MAP`)
|
||||
- packages/system/src/hotkey/hotkey.ts、keyboard.ts(旧实现,供对照,仅确认「原样保留」的边界)
|
||||
- packages/animate/src/types.ts、excitation.ts(`IExcitable` / `IExcitableController` / `IExcitation` / `excitation.add` 的返回与 `revoke` 语义)
|
||||
- packages/common/src/logger.ts、logger.json(`logger.warn(code, ...params)` 与 warn 200 的基线位置)
|
||||
- packages/common/src/logger.ts、logger.json(`logger.warn(code, ...params)` 与 warn 200 的基线位置;`200` 与 `1001` 相邻,201–205 将插在两者之间)
|
||||
- packages/system/package.json、pnpm-lock.yaml(`packages/system` 导入器现状)
|
||||
- packages-user/client-base/src/hotkey/types.ts(用户 WIP:`IClientHotkey.system: IHotkeySystem`,本计划不接线)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 / D-03 / D-04 / D-06)、09-22-SUMMARY.md(既有边界)
|
||||
@ -165,7 +171,7 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
|
||||
① **要解决的问题:** `type.ts` 已完成新按键系统设计(`IHotkeyCommand` / `IHotkeySystem`),但仓库无实现;旧 `hotkey.ts` / `keyboard.ts` 是 legacy 接口(`emitKey` + `assist` 位掩码 + 模块顶层 `document` 监听),无法对接;用户 WIP 的 `IClientHotkey.system` 无对象可用。
|
||||
|
||||
② **范围(最小):** 新增 `packages/system/src/hotkey/command.ts`(`HotkeyCommand implements IHotkeyCommand`)、新增 `packages/system/src/hotkey/system.ts`(`HotkeySystem implements IHotkeySystem, IExcitable<number>`)、`hotkey/index.ts` 追加两个导出、`packages/system/package.json` 补 `@motajs/animate`(workspace)并由 `pnpm install --lockfile-only` 更新 `pnpm-lock.yaml`、新增两个测试文件、写 `09-23-SUMMARY.md`。**`type.ts` 预期零改动**(核验 `emit`:若存在则只删该方法)、`hotkey.ts` / `keyboard.ts` **零改动**;不改 `packages-user/**`、`src/**`、`public/**`、`script/**`、`vite.config.ts`、`tsconfig.json`、`eslint.config.js`、`dev.md`、`AGENTS.md`。
|
||||
② **范围(最小):** 新增 `packages/system/src/hotkey/command.ts`(`HotkeyCommand implements IHotkeyCommand`)、新增 `packages/system/src/hotkey/system.ts`(`HotkeySystem implements IHotkeySystem, IExcitable<number>`)、`hotkey/index.ts` 追加两个导出、`packages/system/package.json` 补 `@motajs/animate`(workspace)并由 `pnpm install --lockfile-only` 更新 `pnpm-lock.yaml`、**`packages/common/src/logger.json` 的 `warn` 段在 `"200"` 与 `"1001"` 之间追加 201–205 五条(逐字采用 OI-5 文案)**、新增两个测试文件、写 `09-23-SUMMARY.md`。**`type.ts` 预期零改动**(核验 `emit`:若存在则只删该方法)、`hotkey.ts` / `keyboard.ts` **零改动**;不改 `packages-user/**`、`src/**`、`public/**`、`script/**`、`vite.config.ts`、`tsconfig.json`、`eslint.config.js`、`dev.md`、`AGENTS.md`。
|
||||
|
||||
③ **指令对象口径:** `addCommand(command, init)` 用 `IHotkeyCommandInit`;`priority` / `block` 构造后不可变;`mode` 缺省 `KeyUp`,`timeout` / `interval` / `dt` 缺省 0;`defaults` 深拷贝;重复 id → warn 204、不覆盖、返回已有对象。`setKeys(count)` 扩容补空组合 / 缩容截断;`setKeyCombination(index, keys)` 越界索引忽略、写入副本,当前 `mode` 为 `KeyUp` 时校验并发 warn 203;`getKeyCombination` 越界返回空数组、返回副本;`iterateKeyCombinations` 索引升序。`keyUp(t)` / `once(t)` / `continuous(i, t)` / `throttle(dt, i, t)` 设置 `mode` 与对应时间参数(未传参保留现值),`keyUp()` 另外校验现有全部组合、存在非法组合时发一次 warn 203。
|
||||
|
||||
@ -173,28 +179,28 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
|
||||
⑤ **触发口径:** `holding` 由系统维护(按下顺序、去重、辅助键 `Ctrl` / `Shift` / `Alt` / `Meta` 也在内),`ctx.ctrl` / `shift` / `alt` / `meta` 由 `holding` 推导;KeyDown 的 `holding` 含触发键、KeyUp 的不含被抬起键;`event.repeat` 忽略;`up` 对不在 `holding` 的键直接返回 `false`。**KeyDown 类模式(Once / Continuous / Throttle)** 组合为 `holding` 子集即匹配(修饰键写进 `keys` 才构成限制,空组合不匹配);**KeyUp 模式** 规则为「可选辅助键 + 至多一个常规键」:1 个常规键 → 该键抬起且其余辅助键仍在 `holding` 时触发;0 个常规键 → 组合恰好 1 个辅助键时该键抬起触发;≥2 个常规键 → 非法、warn 203、永不触发;0 常规 + ≥2 辅助 → 不触发;空组合 → 不触发、不 warn。优先级降序执行,同优先级全部触发,`block` 阻断严格更低优先级且**只按已实现且实际触发的指令计算**;`down` / `up` 返回是否至少有一个已实现指令被触发(含将要延时触发的)。
|
||||
|
||||
⑥ **时序口径:** 时间只来自 `excited(payload)` 的 `payload`(内部记录最近 `now`,初值 0),不使用任何浏览器定时器、不用 `Date.now()`;`timeout === 0` 在 `down` / `up` 当刻执行,`timeout > 0` 入待触发数组、其后每个 `excited` 检查 `payload - 入队时间 >= timeout`;`interval` 是首次与第二次的最小间隔,`dt` 是 Throttle 第二次之后相邻两次的最小间隔;`Once` 单次、`KeyUp` 只在抬起时单次。待触发项在「组合不再被 `holding` 满足(仅 KeyDown 类)」「作用域已不是入队时栈顶」「指令当前 `mode` 与入队快照不同」三种情况被丢弃。`excitedBy` 只保留一个激励源(先 `revoke` 旧控制器再 `excitation.add(this)`);`listen(target)` 注册 `keydown` / `keyup`、用 `EVENT_KEY_CODE_MAP[event.keyCode]` 映射、跳过 `repeat`、触发时 `preventDefault()`、`listen(null)` 解绑、`listening` 暴露当前元素。
|
||||
⑥ **时序口径:** 时间只来自 `excited(payload)` 的 `payload`(内部记录最近 `now`,初值 0),不使用任何浏览器定时器、不用 `Date.now()`;`timeout === 0` 在 `down` / `up` 当刻执行,`timeout > 0` 入待触发数组、其后每个 `excited` 检查 `payload - 入队时间 >= timeout`;`interval` 是首次与第二次的最小间隔,`dt` 是 Throttle 第二次之后相邻两次的最小间隔;`Once` 单次、`KeyUp` 只在抬起时单次;**延时项真正触发时用当时的 `holding` 重新构造 `context`(不入队快照)**。待触发项在「组合不再被 `holding` 满足(仅 KeyDown 类)」「作用域已不是入队时栈顶」「指令当前 `mode` 与入队快照不同」三种情况被丢弃。`excitedBy` 只保留一个激励源(先 `revoke` 旧控制器再 `excitation.add(this)`);`listen(target)` 注册 `keydown` / `keyup`,**先把 `event.keyCode` 的 `91` / `92` / `224` 本地映射为 `KeyCode.Meta`(`EVENT_KEY_CODE_MAP` 没有 Meta 事件码,且不改 `keyCodes.ts`)**,其余用 `EVENT_KEY_CODE_MAP[event.keyCode]` 映射、跳过 `repeat`、触发时 `preventDefault()`、`listen(null)` 解绑、`listening` 暴露当前元素。
|
||||
|
||||
⑦ **验证方式:** `cmd /c "pnpm exec vitest run packages/system/src/hotkey/__test__/command.test.ts packages/system/src/hotkey/__test__/system.test.ts"`;`cmd /c "pnpm exec eslint <改动文件>"`;**按文件过滤的类型闸门**(`vue-tsc --noEmit` 全仓现有 221 行错误、其中 `hotkey.ts` / `keyboard.ts` 各 1 条 legacy 错误属既定边界,闸门只对本计划的 `command.ts` / `system.ts` / `index.ts` / `__test__/**` 报错即失败);结构闸门(导出形态、注释剥离后的负向断言、`it` 前一行注释);旧实现与 `type.ts` 的 `git diff --exit-code` 零改动闸门;`git status` 范围闸门(只允许本计划的文件 + 用户既有 WIP)。**不端到端跑 `build:game`**,不接线 `packages-user/**`。
|
||||
⑦ **验证方式:** `cmd /c "pnpm exec vitest run packages/system/src/hotkey/__test__/command.test.ts packages/system/src/hotkey/__test__/system.test.ts"`;`cmd /c "pnpm exec eslint <改动文件>"`;**按文件过滤的类型闸门**(`vue-tsc --noEmit` 全仓现有 221 行错误、其中 `hotkey.ts` / `keyboard.ts` 各 1 条 legacy 错误属既定边界,闸门只对本计划的 `command.ts` / `system.ts` / `index.ts` / `__test__/**` 报错即失败,**且先校验 `spawnSync` 的 `out.error` / `out.status` / `out.signal`,命令没正常跑完直接失败**);结构闸门(导出形态、**`logger.json` 的 201–205 逐字文案与 `200` / `1001` 完好**、测试只从 `../command` / `../system` 导入且禁 `../index`、注释剥离后的负向断言、`it` 前一行注释);旧实现与 `type.ts` 的 `git diff --exit-code` 零改动闸门;**范围闸门(白名单由本计划 `files_modified` 派生,含 `logger.json`,用户既有 WIP 显式豁免)**。**不端到端跑 `build:game`**,不接线 `packages-user/**`。
|
||||
|
||||
⑧ **已知边界(原样告知):** ①不接线用户 WIP 的 `IClientHotkey`(只提供实现);②不跑全量构建(渲染端仍有用户 WIP 的 stale import,09-21 / 09-22 已记录);③`hotkey.ts` / `keyboard.ts` 的 2 条 legacy 类型错误保持原状、不修;④不提供存档序列化(`type.ts` 第 4 行 TODO 保留)。
|
||||
|
||||
⑨ **待批准命名(须用户确认):** OI-1 测试文件(`__test__/command.test.ts` 与 `__test__/system.test.ts`);OI-2 导出辅助函数 `isAuxiliaryKey`;OI-3 内部接口 `IHotkeyScope` / `IHotkeyMatch` / `IHotkeyPending`、类型别名 `HotkeyHandler`、`HotkeyCommand` 私有字段 `command`、`HotkeySystem` 私有字段 `commands` / `scopes` / `holding` / `pending` / `now` / `controller` / `keyDownListener` / `keyUpListener` 与私有方法 `getScope` / `createContext` / `dispatch` / `runMatches` / `matchKeyDown` / `matchKeyUp` / `flushPending` / `dropPending`;OI-4 三处语义确认(松键取消 KeyDown 类待触发项、同一事件同一指令只触发一次且取最小索引、空栈无参 `leaveScope()` 也发 205)。**未获批前不落码**;若用户改的是文件名或两个类名,先按新命名修订本计划(含 `files_modified` 与门禁命令)再重新汇报。
|
||||
⑨ **待批准命名与文案(须用户确认):** OI-1 测试文件(`__test__/command.test.ts` 与 `__test__/system.test.ts`);OI-2 导出辅助函数 `isAuxiliaryKey`;OI-3 内部接口 `IHotkeyScope` / `IHotkeyMatch` / `IHotkeyPending`、类型别名 `HotkeyHandler`、`HotkeyCommand` 私有字段 `command`(指令 id)与 **`combinations`(二维组合数组)**、`HotkeySystem` 私有字段 `commands` / `scopes` / `holding` / `pending` / `now` / `controller` / `keyDownListener` / `keyUpListener` 与私有方法 `getScope` / `createContext` / `resolveKeyCode` / `dispatch` / `runMatches` / `matchKeyDown` / `matchKeyUp` / `flushPending` / `dropPending`,Task 2 新增私有方法 **`isPendingDue`**;OI-4 四处语义确认(松键取消 KeyDown 类待触发项、同一事件同一指令只触发一次且取最小索引、空栈无参 `leaveScope()` 也发 205、延时项的 `context` 按触发时刻重算而非入队快照);**OI-5 五条日志文案逐字报备**:201 `Cannot set hotkey condition or implement command since no scope is active.`、202 `Cannot implement nonexistent hotkey command '$1'.`、203 `Hotkey command '$1' has an invalid KeyUp combination: at most one non-auxiliary key is allowed.`、204 `Duplicate hotkey command '$1', the existing command will be used.`、205 `Cannot leave nonexistent hotkey scope.`。**未获批前不落码**;若用户改的是文件名、两个类名或日志文案,先按新命名 / 新文案修订本计划(含 `files_modified`、门禁命令与结构闸门的逐字比对)再重新汇报。
|
||||
|
||||
⑩ **不在本增量:** 不改旧实现、不改 `type.ts` 既有定义、不新增日志码(只用 201–205)、不新增外部依赖、不新增 vitest 配置、不加存档序列化、不改构建脚本。
|
||||
⑩ **不在本增量:** 不改旧实现、不改 `type.ts` 既有定义、不新增 201–205 以外的日志码、不改 `logger.json` 既有条目、不新增外部依赖、不新增 vitest 配置、不加存档序列化、不改构建脚本。
|
||||
</action>
|
||||
<decision>是否按本计划(09-23,最小范围)实现用户重新设计的按键系统:新增 `packages/system/src/hotkey/command.ts`(`HotkeyCommand implements IHotkeyCommand`)与 `packages/system/src/hotkey/system.ts`(`HotkeySystem implements IHotkeySystem, IExcitable<number>`,接口本身不含 `excited`),从 `hotkey/index.ts` 导出,`packages/system/package.json` 补 `@motajs/animate`(workspace)并更新 lockfile,补两个测试文件与 SUMMARY;`type.ts` 保持零改动(已无 `emit`)、旧实现 `hotkey.ts` / `keyboard.ts` 原样保留、不接线 user 代码、不跑全量构建。</decision>
|
||||
<decision>是否按本计划(09-23,最小范围)实现用户重新设计的按键系统:新增 `packages/system/src/hotkey/command.ts`(`HotkeyCommand implements IHotkeyCommand`)与 `packages/system/src/hotkey/system.ts`(`HotkeySystem implements IHotkeySystem, IExcitable<number>`,接口本身不含 `excited`),从 `hotkey/index.ts` 导出,`packages/system/package.json` 补 `@motajs/animate`(workspace)并更新 lockfile,`packages/common/src/logger.json` 的 `warn` 段在 `"200"` 与 `"1001"` 之间追加 201–205 五条(文案见 OI-5),补两个测试文件与 SUMMARY;`type.ts` 保持零改动(已无 `emit`)、旧实现 `hotkey.ts` / `keyboard.ts` 原样保留、不接线 user 代码、不跑全量构建。</decision>
|
||||
<context>
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo` 且 `auto_advance: false`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作(新增 2 个实现 + 2 个测试 + SUMMARY,修改 `index.ts` / `package.json` / `pnpm-lock.yaml`),无删除(`type.ts` 无 `emit`,故无删除动作)。已核验:`packages/system/src/hotkey/type.ts` 为 HEAD `6c19e1f` 的设计且无 `emit`;`logger.json` warn 段止于 200(201–205 空闲);`packages/system/package.json` 仅声明 `@motajs/common`;`packages/` 下此前无任何 `__test__` 或 `*.test.ts`;`vue-tsc --noEmit` 现有 221 行错误、`packages/system/src/hotkey/` 只有 `hotkey.ts` / `keyboard.ts` 各 1 条 `KeyCode` 导出缺失的 legacy 错误。
|
||||
AGENTS.md / D-03 / D-04 要求执行前与命名先获批;`.planning/config.json` 为 `mode: yolo` 且 `auto_advance: false`,故本关卡显式 `blocking-human` 且 `autonomous: false`。执行期含写操作(新增 2 个实现 + 2 个测试 + SUMMARY,修改 `index.ts` / `package.json` / `pnpm-lock.yaml` / `logger.json`),无删除(`type.ts` 无 `emit`,故无删除动作)。已核验:`packages/system/src/hotkey/type.ts` 为 HEAD `6c19e1f` 的设计且无 `emit`;`logger.json` 的 `warn` 段编号为 1–200 与 1001(**201–205 空闲,`1001` 紧跟在 `200` 之后**);`packages/system/package.json` 仅声明 `@motajs/common`;`packages/` 下此前无任何 `__test__` 或 `*.test.ts`;`vue-tsc --noEmit` 现有 221 行错误、`packages/system/src/hotkey/` 只有 `hotkey.ts` / `keyboard.ts` 各 1 条 `KeyCode` 导出缺失的 legacy 错误。
|
||||
</context>
|
||||
<options>
|
||||
<option id="execute-default">
|
||||
<name>可以执行(按本计划默认口径,OI-1 / OI-2 / OI-3 采用提议命名,OI-4 三条按提议执行)</name>
|
||||
<pros>把已定型的接口一次落地为可用实现:KeyUp 与 KeyDown 两类模式、优先级 / block、作用域隔离、激励驱动的 timeout / interval / dt、listen 全部可单测;旧实现零风险保留;只补一个 workspace 依赖</pros>
|
||||
<cons>实现面较大(两个类约 600+ 行、测试约 700+ 行);未接线 user 代码,浏览器里暂时无人调用;`hotkey.ts` / `keyboard.ts` 的 legacy 错误仍在</cons>
|
||||
<name>可以执行(按本计划默认口径,OI-1 / OI-2 / OI-3 采用提议命名,OI-4 四条与 OI-5 五条文案按提议执行)</name>
|
||||
<pros>把已定型的接口一次落地为可用实现:KeyUp 与 KeyDown 两类模式、优先级 / block、作用域隔离、激励驱动的 timeout / interval / dt、listen 全部可单测;旧实现零风险保留;只补一个 workspace 依赖;201–205 的文案随计划一并写入 `logger.json`,告警不会退化成 error 16</pros>
|
||||
<cons>实现面较大(两个类约 600+ 行、测试约 700+ 行);会改动 `packages/common/src/logger.json`(仅追加 5 条);未接线 user 代码,浏览器里暂时无人调用;`hotkey.ts` / `keyboard.ts` 的 legacy 错误仍在</cons>
|
||||
</option>
|
||||
<option id="revise">
|
||||
<name>修订 OI-1 / OI-2 / OI-3 命名或 OI-4 的语义口径后重新汇报</name>
|
||||
<name>修订 OI-1 / OI-2 / OI-3 命名、OI-4 的语义口径或 OI-5 的日志文案后重新汇报</name>
|
||||
<pros>避免带着未确认的命名或语义落码</pros>
|
||||
<cons>须修订 PLAN.md 后重新汇报,执行延后</cons>
|
||||
</option>
|
||||
@ -204,15 +210,17 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
<cons>先落地的 KeyUp 通路仍需要 `system.ts` 骨架,拆开后两次改同一文件;本计划的 Task 1 / Task 2 已按此顺序切分,拆分收益有限</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>回复「可以执行」即可;如需调整命名或 OI-4 的语义口径,请在回复中一并指明。</resume-signal>
|
||||
<resume-signal>回复「可以执行」即可;如需调整命名、OI-4 的语义口径或 OI-5 的日志文案,请在回复中一并指明。</resume-signal>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const need=['packages/system/src/hotkey/type.ts','packages/system/src/hotkey/keyCodes.ts','packages/system/src/hotkey/hotkey.ts','packages/system/src/hotkey/keyboard.ts','packages/system/src/hotkey/index.ts','packages/system/package.json','packages/common/src/logger.json','packages/animate/src/types.ts','packages/animate/package.json','dev.md','AGENTS.md'];for(const p of need){if(!fs.existsSync(p))bad.push('missing '+p)}const type=fs.readFileSync('packages/system/src/hotkey/type.ts','utf8');if(/emit\s*\(/.test(type))bad.push('type.ts still declares an emit method - this plan expects it already removed at HEAD 6c19e1f');if(type.indexOf('interface IHotkeySystem')<0||type.indexOf('interface IHotkeyCommand')<0||type.indexOf('excitedBy')<0)bad.push('type.ts is not the expected hotkey design');const log=JSON.parse(fs.readFileSync('packages/common/src/logger.json','utf8'));for(const c of [201,202,203,204,205]){if(log.warn[c]!==undefined)bad.push('warn '+c+' already used')}if(!log.warn[200])bad.push('warn 200 missing - logger.json is not the expected baseline');const pkg=JSON.parse(fs.readFileSync('packages/system/package.json','utf8'));if(pkg.dependencies&&pkg.dependencies['@motajs/animate'])bad.push('@motajs/animate already declared in packages/system');const anim=JSON.parse(fs.readFileSync('packages/animate/package.json','utf8'));if(anim.name!=='@motajs/animate')bad.push('animate package name mismatch');for(const p of ['packages/system/src/hotkey/command.ts','packages/system/src/hotkey/system.ts','packages/system/src/hotkey/__test__']){if(fs.existsSync(p))bad.push('must not exist yet: '+p)}const kc=fs.readFileSync('packages/system/src/hotkey/keyCodes.ts','utf8');if(kc.indexOf('EVENT_KEY_CODE_MAP')<0||kc.indexOf('export enum KeyCode')<0)bad.push('keyCodes.ts is not the expected baseline');if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate: design present, emit already removed, warn 201-205 free, animate undeclared, new files absent')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / `type.ts` 含 `emit` 方法(说明用户又改了设计,需先核对本计划)/ `type.ts` 不是预期设计 / warn 201–205 已被占用 / warn 200 缺失(`logger.json` 不是预期基线)/ `packages/system` 已声明 animate / animate 包名不符 / `command.ts`、`system.ts`、`__test__/` 已存在(说明实现已提前开始)/ `keyCodes.ts` 缺 `EVENT_KEY_CODE_MAP` 或 `KeyCode` 枚举</fails_when>
|
||||
<human-check>用户明确回复「可以执行」;若就 OI-1 / OI-2 / OI-3 / OI-4 指定了不同命名或口径,须先按新命名或新口径修订本计划(门禁命令、测试用例与 SUMMARY 关键字同步)并重新获批,未修订前不得执行</human-check>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const need=['packages/system/src/hotkey/type.ts','packages/system/src/hotkey/keyCodes.ts','packages/system/src/hotkey/hotkey.ts','packages/system/src/hotkey/keyboard.ts','packages/system/src/hotkey/index.ts','packages/system/package.json','packages/common/src/logger.json','packages/animate/src/types.ts','packages/animate/package.json','dev.md','AGENTS.md'];for(const p of need){if(!fs.existsSync(p))bad.push('missing '+p)}const type=fs.readFileSync('packages/system/src/hotkey/type.ts','utf8');if(/emit\s*\(/.test(type))bad.push('type.ts still declares an emit method - this plan expects it already removed at HEAD 6c19e1f');if(type.indexOf('interface IHotkeySystem')<0||type.indexOf('interface IHotkeyCommand')<0||type.indexOf('excitedBy')<0)bad.push('type.ts is not the expected hotkey design');const log=JSON.parse(fs.readFileSync('packages/common/src/logger.json','utf8'));for(const c of [201,202,203,204,205]){if(log.warn[c]!==undefined)bad.push('warn '+c+' already used')}if(!log.warn[200]||!log.warn[1001])bad.push('warn 200 or warn 1001 missing - logger.json is not the expected baseline');const pkg=JSON.parse(fs.readFileSync('packages/system/package.json','utf8'));if(pkg.dependencies&&pkg.dependencies['@motajs/animate'])bad.push('@motajs/animate already declared in packages/system');const anim=JSON.parse(fs.readFileSync('packages/animate/package.json','utf8'));if(anim.name!=='@motajs/animate')bad.push('animate package name mismatch');for(const p of ['packages/system/src/hotkey/command.ts','packages/system/src/hotkey/system.ts','packages/system/src/hotkey/__test__']){if(fs.existsSync(p))bad.push('must not exist yet: '+p)}const kc=fs.readFileSync('packages/system/src/hotkey/keyCodes.ts','utf8');if(kc.indexOf('EVENT_KEY_CODE_MAP')<0||kc.indexOf('export enum KeyCode')<0)bad.push('keyCodes.ts is not the expected baseline');if(bad.length){console.error('PRESTATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK prestate: design present, emit already removed, warn 201-205 free, animate undeclared, new files absent')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "PRESTATE FAILED":必需文件缺失 / `type.ts` 含 `emit` 方法(说明用户又改了设计,需先核对本计划)/ `type.ts` 不是预期设计 / warn 201–205 已被占用 / warn 200 或 1001 缺失(`logger.json` 不是预期基线)/ `packages/system` 已声明 animate / animate 包名不符 / `command.ts`、`system.ts`、`__test__/` 已存在(说明实现已提前开始)/ `keyCodes.ts` 缺 `EVENT_KEY_CODE_MAP` 或 `KeyCode` 枚举</fails_when>
|
||||
<human-check>用户明确回复「可以执行」;若就 OI-1 / OI-2 / OI-3 / OI-4 / OI-5 指定了不同命名、口径或文案,须先按新命名 / 新口径 / 新文案修订本计划(门禁命令、结构闸门的逐字比对、测试用例与 SUMMARY 关键字同步)并重新获批,未修订前不得执行</human-check>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 用户回复中明确包含「可以执行」
|
||||
- 汇报涵盖:要解决的问题、最小范围与「其余文件一律不动」清单、指令对象口径、系统口径(作用域 / 201 / 202 / 204 / 205)、触发口径(holding / KeyUp 四条规则 / KeyDown 子集 / 优先级与 block)、时序口径(激励驱动、无定时器)、验证方式、四项已知边界、OI-1 / OI-2 / OI-3 / OI-4
|
||||
- 汇报涵盖:要解决的问题、最小范围与「其余文件一律不动」清单、指令对象口径、系统口径(作用域 / 201 / 202 / 204 / 205)、触发口径(holding / KeyUp 四条规则 / KeyDown 子集 / 优先级与 block)、时序口径(激励驱动、无定时器、延时触发时重算 context)、验证方式、四项已知边界、OI-1 / OI-2 / OI-3 / OI-4 / OI-5
|
||||
- 已逐字展示 OI-5 的五条文案(201–205),并说明它们写入 `logger.json` 的 `warn` 段 `"200"` 与 `"1001"` 之间、既有条目一字不动;说明缺少这些条目会让 `logger.warn` 退化成 `error 16`
|
||||
- 已说明 `HotkeyCommand` 的私有组合数组命名为 `combinations`、Task 2 只新增 `isPendingDue` 一个私有方法(OI-3 清单已同步)
|
||||
- 已说明 `type.ts` 当前**无 `emit` 成员**(HEAD `6c19e1f`),决策 2 以核验代替删除,核验后与 HEAD 逐字节一致
|
||||
- 已说明 `IExcitable<number>` **只加在 `HotkeySystem` 类上**,`IHotkeySystem` 接口不新增成员
|
||||
- 已说明旧实现 `hotkey.ts` / `keyboard.ts` **零改动**(含其模块顶层 `document` 监听)
|
||||
@ -224,9 +232,9 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
</task>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: 端到端打通「一条 KeyUp 热键」—— `HotkeyCommand` + `HotkeySystem` 骨架(作用域 / 注册 / KeyUp 派发 / 优先级与 block / listen)+ 依赖与导出</name>
|
||||
<precondition>Task 0 已闭环(用户回复「可以执行」;OI-1 / OI-2 / OI-3 / OI-4 已获批或按提议默认执行)。`packages/system/src/hotkey/type.ts` 为权威设计且无 `emit` 成员;`logger.json` 的 warn 201–205 仍空闲;`command.ts` / `system.ts` / `hotkey/__test__/` 尚不存在;`@motajs/animate` 尚未在 `packages/system/package.json` 声明。</precondition>
|
||||
<files>packages/system/src/hotkey/command.ts, packages/system/src/hotkey/system.ts, packages/system/src/hotkey/type.ts, packages/system/src/hotkey/index.ts, packages/system/package.json, pnpm-lock.yaml, packages/system/src/hotkey/__test__/command.test.ts, packages/system/src/hotkey/__test__/system.test.ts</files>
|
||||
<name>Task 1: 端到端打通「一条 KeyUp 热键」—— `HotkeyCommand` + `HotkeySystem` 骨架(作用域 / 注册 / KeyUp 派发 / 优先级与 block / listen)+ 依赖、导出与日志码 201–205</name>
|
||||
<precondition>Task 0 已闭环(用户回复「可以执行」;OI-1 / OI-2 / OI-3 / OI-4 / OI-5 已获批或按提议默认执行)。`packages/system/src/hotkey/type.ts` 为权威设计且无 `emit` 成员;`logger.json` 的 warn 201–205 仍空闲且 `200` / `1001` 是既有条目;`command.ts` / `system.ts` / `hotkey/__test__/` 尚不存在;`@motajs/animate` 尚未在 `packages/system/package.json` 声明。</precondition>
|
||||
<files>packages/system/src/hotkey/command.ts, packages/system/src/hotkey/system.ts, packages/system/src/hotkey/index.ts, packages/system/package.json, pnpm-lock.yaml, packages/common/src/logger.json, packages/system/src/hotkey/__test__/command.test.ts, packages/system/src/hotkey/__test__/system.test.ts</files>
|
||||
<read_first>
|
||||
- packages/system/src/hotkey/type.ts(全部接口与 jsDoc;`HotkeyEmitMode` 的四个成员顺序决定了数值,实现不得依赖字面量)
|
||||
- packages/system/src/hotkey/keyCodes.ts(`KeyCode` 的 `Ctrl` / `Shift` / `Alt` / `Meta` 与 `EVENT_KEY_CODE_MAP`)
|
||||
@ -237,15 +245,23 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
- AGENTS.md(命名须先获批、既有注释不得修改、测试注释要求)、dev.md(命名、jsDoc、无循环引用、类必须先 interface、显式成员类型、禁止类型专用导入、限制 `?.` 与非空判断口径、CRLF)
|
||||
</read_first>
|
||||
<action>
|
||||
**本任务交付一个可运行的最小闭环:一条 KeyUp 热键能在 `listen` 的元素上被按下 / 抬起并触发实现,且作用域、优先级 / block、告警 201 / 202 / 204 / 205 与 KeyUp 规则(含 203)全部就位。KeyDown 类模式的触发与时序留给 Task 2,本任务只在派发处预留接缝,不实现其触发。**
|
||||
**本任务交付一个可运行的最小闭环:一条 KeyUp 热键能在 `listen` 的元素上被按下 / 抬起并触发实现,且作用域、优先级 / block、告警 201 / 202 / 203 / 204 / 205(含 `logger.json` 的文案投递)全部就位。KeyDown 类模式的触发与时序留给 Task 2,本任务只在派发处预留接缝,不实现其触发。**
|
||||
|
||||
(1) **`packages/system/package.json`:** `dependencies` 增加 `"@motajs/animate": "workspace:*"`(排在 `@motajs/common` 之前,保持 4 空格缩进与既有键序风格),`@motajs/common` 保留不动。随后执行 `cmd /c "pnpm install --lockfile-only"` 更新 `pnpm-lock.yaml`(`packages/system` 导入器出现 animate 链接),**不得手改 lockfile**。
|
||||
|
||||
(2) **`packages/system/src/hotkey/type.ts`:** 核验 `IHotkeySystem` 无 `emit` 成员(当前 HEAD 已无)→ **零改动**;仅当核验发现 `emit` 方法存在时才删除该方法,其余成员、`const enum`、jsDoc 逐字保留。不得为 `IExcitable` 增加任何接口成员。
|
||||
(2) **`packages/system/src/hotkey/type.ts`:** 核验 `IHotkeySystem` 无 `emit` 成员(当前 HEAD 已无)→ **零改动**;仅当核验发现 `emit` 方法存在时才删除该方法,其余成员、`const enum`、jsDoc 逐字保留。不得为 `IExcitable` 增加任何接口成员。该文件**不在 `files_modified`**,只受 LEGACY GATE 守护。
|
||||
|
||||
(3) **`packages/system/src/hotkey/command.ts`(新建):** 首行起连续 `import`(`@motajs/common` 的 `logger`、`./keyCodes` 的 `KeyCode`、`./type` 的 `HotkeyEmitMode` / `IHotkeyCommand` / `IHotkeyCommandInit`),`import` 之间不得插注释或空行。
|
||||
(3) **`packages/common/src/logger.json`:** 在 `warn` 段 `"200"` 条目之后、`"1001"` 条目之前**追加** 201–205 五条,文本**逐字符**采用 OI-5(`$1` 保留为占位符):
|
||||
- `"201": "Cannot set hotkey condition or implement command since no scope is active."`
|
||||
- `"202": "Cannot implement nonexistent hotkey command '$1'."`
|
||||
- `"203": "Hotkey command '$1' has an invalid KeyUp combination: at most one non-auxiliary key is allowed."`
|
||||
- `"204": "Duplicate hotkey command '$1', the existing command will be used."`
|
||||
- `"205": "Cannot leave nonexistent hotkey scope."`
|
||||
只允许新增这五行:既有条目(含 `200` / `1001` 与 error 段)一字不动、不调整键序、不重排缩进(沿用 8 空格缩进与既有逗号风格)。改完必须 `JSON.parse` 通过。
|
||||
|
||||
(4) **`packages/system/src/hotkey/command.ts`(新建):** 首行起连续 `import`(`@motajs/common` 的 `logger`、`./keyCodes` 的 `KeyCode`、`./type` 的 `HotkeyEmitMode` / `IHotkeyCommand` / `IHotkeyCommandInit`),`import` 之间不得插注释或空行。
|
||||
- 导出辅助函数 `isAuxiliaryKey`(OI-2 命名):判断 `Ctrl` / `Shift` / `Alt` / `Meta`;只写一行说明性 jsDoc。
|
||||
- 导出 `class HotkeyCommand implements IHotkeyCommand`:私有字段 `command`(指令 id,供 warn 203 的 `$1`);`priority` / `block` 为只读字段(构造器赋值后不再变更);`mode` / `interval` / `timeout` / `dt` 为可写字段(接口侧仍是 `readonly`,实现侧允许模式方法改写);`length` 与内部组合数组同步;私有组合数组以二维 `KeyCode` 数组保存。
|
||||
- 导出 `class HotkeyCommand implements IHotkeyCommand`:私有字段 `command`(指令 id,供 warn 203 的 `$1`)与 **`combinations`**(二维 `KeyCode[][]`,OI-3 提议名);`priority` / `block` 为只读字段(构造器赋值后不再变更);`mode` / `interval` / `timeout` / `dt` 为可写字段(接口侧仍是 `readonly`,实现侧允许模式方法改写);`length` 与 `combinations.length` 同步。
|
||||
- 构造器:接收指令 id 与 `IHotkeyCommandInit`;`mode` 取 `init.mode` 缺省 `HotkeyEmitMode.KeyUp`,`timeout` / `interval` / `dt` 缺省 0;用 `setKeys(init.defaults.length)` + 逐个 `setKeyCombination` 落地 `defaults`(**因此缺省 `KeyUp` 模式下非法默认组合会自然触发 203**),不直接引用调用方数组。
|
||||
- `setKeys(count)`:负值或非整数忽略;扩容补空组合;缩容截断;同步 `length`。
|
||||
- `setKeyCombination(index, keys)`:索引非整数或越界忽略(不告警);写入拷贝;若当前 `mode` 为 `HotkeyEmitMode.KeyUp` 且组合含 **2 个及以上**常规键(用 `isAuxiliaryKey` 拆分)→ 调用 warn 203 并传指令 id(组合仍保存,供读取与后续 `keyUp()` 复核)。
|
||||
@ -255,8 +271,8 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
- `once(timeout?)` / `continuous(interval?, timeout?)` / `throttle(dt, interval?, timeout?)`:置对应 `mode`,参数有值则更新对应字段(未传则保留现值);`throttle` 的 `dt` 为必填。
|
||||
- jsDoc:接口已声明的成员**不重复写**(dev.md 继承注释规则);只为新增的非接口成员与 `isAuxiliaryKey` 写注释。私有方法置于调用它的方法之前。
|
||||
|
||||
(4) **`packages/system/src/hotkey/system.ts`(新建):** 首行起连续 `import`(`@motajs/animate` 的 `IExcitable` / `IExcitableController` / `IExcitation`、`@motajs/common` 的 `logger`、`./command` 的 `HotkeyCommand` / `isAuxiliaryKey`、`./keyCodes` 的 `EVENT_KEY_CODE_MAP` / `KeyCode`、`./type` 的 `HotkeyEmitMode` / `HotkeyEmitType` / `IHotkeyCommand` / `IHotkeyCommandInit` / `IHotkeyEmitContext` / `IHotkeyEmitInfo` / `IHotkeySystem`)。
|
||||
- 文件内声明内部 `interface IHotkeyScope`(`symbol` / `condition: (() => boolean) | null` / `handlers: Map<string, HotkeyHandler>`)、`interface IHotkeyMatch`(`id` / `command` / `index` / `keys` / `handler` / 可选 `entry`)、`interface IHotkeyPending`(字段见 `must_haves` 的 OI-3)、类型别名 `HotkeyHandler`;**均不导出**。
|
||||
(5) **`packages/system/src/hotkey/system.ts`(新建):** 首行起连续 `import`(`@motajs/animate` 的 `IExcitable` / `IExcitableController` / `IExcitation`、`@motajs/common` 的 `logger`、`./command` 的 `HotkeyCommand` / `isAuxiliaryKey`、`./keyCodes` 的 `EVENT_KEY_CODE_MAP` / `KeyCode`、`./type` 的 `HotkeyEmitMode` / `HotkeyEmitType` / `IHotkeyCommand` / `IHotkeyCommandInit` / `IHotkeyEmitContext` / `IHotkeyEmitInfo` / `IHotkeySystem`)。
|
||||
- 文件内声明内部 `interface IHotkeyScope`(`symbol` / `condition: (() => boolean) | null` / `handlers: Map<string, HotkeyHandler>`)、`interface IHotkeyMatch`(`id` / `command` / `index` / `keys` / `handler` / 可选 `entry`)、`interface IHotkeyPending`(字段见 frontmatter 的 OI-3:`id` / `command` / `index` / `keys` / `handler` / `type` / `key` / `scope` / `mode` / `timeout` / `interval` / `dt` / `time` / `last` / `fired`,**不含 `context`**)、类型别名 `HotkeyHandler`;**均不导出**。
|
||||
- 导出 `class HotkeySystem implements IHotkeySystem, IExcitable<number>`:字段 `listening: HTMLElement | null = null`;私有 `commands: Map<string, IHotkeyCommand>`、`scopes: IHotkeyScope[]`、`holding: KeyCode[]`、`pending: IHotkeyPending[]`、`now: number`、`controller: IExcitableController<number> | null`、`keyDownListener` / `keyUpListener`(显式类型的箭头成员,内部调用统一的事件处理路径)。
|
||||
- 指令注册表:`addCommand`(已存在 → warn 204 并返回已有对象;否则 `new HotkeyCommand` 存入并返回)、`getCommand`(缺失返回 `null`)、`deleteCommand`(删注册表条目,并清理各作用域 `handlers` 中该 id 与 `pending` 中该指令的项,不告警)、`iterateCommands`(生成器遍历注册表)。
|
||||
- 作用域:`getScope()` 返回栈顶或 `null`(私有,置于调用它的方法之前);`enterScope()` 建新作用域(`condition` 为 `null`、空 `handlers`)压栈并返回其 `symbol`;`leaveScope(sym?)` 无参时弹栈顶(空栈 → warn 205)、有参时按 `symbol` 找到索引、把栈截到该索引 + 1(未找到 → warn 205),两种退出后都调用 `dropPending()`;`dropPending()` 丢弃 `pending` 中「作用域已不是当前栈顶」「指令当前 `mode` 与入队快照不同」「KeyDown 类模式组合已不被 `holding` 满足」的项(KeyUp 类延时项不受 `holding` 条件)。`when` / `implement` 无作用域 → warn 201(返回 `this`);`implement` 再校验 id 是否存在,缺失 → warn 202(返回 `this`)。
|
||||
@ -264,32 +280,32 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
- 派发接缝:`dispatch(type, key)` 收集**当前栈顶作用域**内所有已注册、条件为真(`condition` 为 `null` 视为真)、且 `mode` 与 `type` 同类的匹配项(`matchKeyDown` / `matchKeyUp`,同一指令多组组合命中时取**最小索引**一条);按 `command.priority` 降序排序后交给 `runMatches`;`runMatches` 维护一个「已封锁优先级」值:某条指令执行(或入队)后若其 `block` 为真则记录其 `priority`,后续 `priority` 严格更低的匹配项直接结束循环(同优先级全部执行)。**匹配但无实现(不在栈顶作用域 `handlers` 中)的指令不得进入匹配集**,因此不阻断。`down(key)`:键已在 `holding` 中则返回 `false`;否则入 `holding` 后派发 KeyDown 类(本任务对 KeyDown 类返回 `false`,仅为 Task 2 预留);`up(key)`:键不在 `holding` 中直接返回 `false`;否则先从 `holding` 移除、再 `dropPending()`、再派发 KeyUp 类。
|
||||
- `matchKeyUp(keys, key)`:按 `must_haves` 的四条规则(0 常规 + 恰好 1 辅助、1 常规且等于被抬起键且其余辅助键仍在 `holding`、≥2 常规非法、空组合不匹配)返回布尔。
|
||||
- `matchKeyDown(keys)`:本任务先实现「空组合不匹配 + `keys` 每项都在 `holding` 中」的子集判定,供 Task 2 直接复用。
|
||||
- `runMatches` 的执行分支本任务只需处理 `mode` 为 `KeyUp` 的情形:`timeout` 为 0 时当刻以 `{ command: id, index, keys, context }` 调用 handler;`timeout` 大于 0 时入 `pending` 快照(`fired` 0、`time` 为当前 `now`),返回值计入「将要延时触发」。其余模式的执行分支留空(返回 `false`),不得写成会误触发的实现。
|
||||
- `runMatches` 的执行分支本任务只需处理 `mode` 为 `KeyUp` 的情形:`timeout` 为 0 时当刻以 `{ command: id, index, keys, context: createContext(type, key) }` 调用 handler;`timeout` 大于 0 时入 `pending`(记录 `type` / `key` / `scope` / `mode` / `timeout` / `interval` / `dt` / `keys` / `index` / `handler`,`fired` 0、`time` 为当前 `now`,**不记录 `context`**),返回值计入「将要延时触发」。其余模式的执行分支留空(返回 `false`),不得写成会误触发的实现。
|
||||
- `excitedBy(excitation)`:先 `revoke` 旧控制器(若有)再 `excitation.add(this)`,保存返回的控制器(可能为 `null`);不保存激励源对象本身。`excited(payload)`:记录 `now`(本任务不做待触发处理,函数体只更新时间;Task 2 在同一位置扩展)。
|
||||
- `listen(target)`:与 `listening` 相同则空操作;否则先解绑旧元素的两个监听、再设置 `listening`、再为新元素(非 `null` 时)注册;`keydown` 处理跳过 `repeat`、用 `EVENT_KEY_CODE_MAP[event.keyCode]` 映射、无映射(`undefined` 与 0)跳过、`down` 返回真时 `preventDefault()`;`keyup` 同理调用 `up`。
|
||||
- `listen(target)`:与 `listening` 相同则空操作;否则先解绑旧元素的两个监听、再设置 `listening`、再为新元素(非 `null` 时)注册;两个处理函数共用一个 `resolveKeyCode(event.keyCode)` 私有判定:**先按 `91` / `92` / `224` → `KeyCode.Meta` 做本地映射**(`EVENT_KEY_CODE_MAP` 没有 Meta 的事件码,且 `keyCodes.ts` 零改动),否则取 `EVENT_KEY_CODE_MAP[event.keyCode]`;`keydown` 处理跳过 `event.repeat`、无映射(`undefined` 与 `0`)跳过、`down` 返回真时 `preventDefault()`;`keyup` 同理调用 `up`。(`resolveKeyCode` 属 OI-3 清单内的 Task 1 私有方法。)
|
||||
- jsDoc:只为新增的非接口成员写注释;接口成员不重复注释。不得出现循环引用(`command.ts` 不引入 `system.ts`)。
|
||||
|
||||
(5) **`packages/system/src/hotkey/index.ts`:** 在既有 4 行导出中按字母序插入 `export * from './command';`(首位)与 `export * from './system';`(`./keyCodes` 之后),其余行不动。
|
||||
(6) **`packages/system/src/hotkey/index.ts`:** 在既有 4 行导出中按字母序插入 `export * from './command';`(首位)与 `export * from './system';`(`./keyCodes` 之后),其余行不动。
|
||||
|
||||
(6) **`packages/system/src/hotkey/__test__/command.test.ts`(新建):** 文件头一行注释说明覆盖范围;`vi.hoisted` 中 stub `main` / `location`(沿用既有测试约定);`afterEach` 恢复 spy、`afterAll` 还原全局。用例(每个 `it` 前一行注释):缺省值与 `defaults` 深拷贝;`setKeys` 扩容与缩容;`setKeyCombination` 越界忽略与返回副本;`iterateKeyCombinations` 顺序与拷贝;`mode` 为 `KeyUp` 时非法组合 warn 203 一次且组合仍可读;`mode` 为 `KeyUp` 时「单常规键 + 辅助键」不告警;`keyUp()` 切换 `mode` 并对已有非法组合发一次 203;四个模式方法分别设置 `mode` 与 `timeout` / `interval` / `dt`、未传参时保留现值。断言一律 `vi.spyOn(logger, 'warn')` 校验码与参数。
|
||||
(7) **`packages/system/src/hotkey/__test__/command.test.ts`(新建):** 文件头一行注释说明覆盖范围;**`import { HotkeyCommand, isAuxiliaryKey } from '../command';`——只从实现模块导入,禁止 `../index`**(`hotkey.ts` 在模块顶层执行 `document.addEventListener`,Node 下导入即抛错);`vi.hoisted` 中 stub `main` / `location`(沿用既有测试约定);`afterEach` 恢复 spy、`afterAll` 还原全局。用例(每个 `it` 前一行注释):`isAuxiliaryKey` 对 `Ctrl` / `Shift` / `Alt` / `Meta` 返回真、对常规键返回假;缺省值与 `defaults` 深拷贝;`setKeys` 扩容与缩容;`setKeyCombination` 越界忽略与返回副本;`iterateKeyCombinations` 顺序与拷贝;`mode` 为 `KeyUp` 时非法组合 warn 203 一次且组合仍可读;`mode` 为 `KeyUp` 时「单常规键 + 辅助键」不告警;`keyUp()` 切换 `mode` 并对已有非法组合发一次 203;四个模式方法分别设置 `mode` 与 `timeout` / `interval` / `dt`、未传参时保留现值。断言一律 `vi.spyOn(logger, 'warn')` 校验码与参数。
|
||||
|
||||
(7) **`packages/system/src/hotkey/__test__/system.test.ts`(新建):** 文件头一行注释;构造 fake 元素(`addEventListener` / `removeEventListener` 记录处理器)与伪事件(`keyCode` / `repeat` / `preventDefault`)。用例(每个 `it` 前一行注释):无作用域时 `when` / `implement` 各发 201 且 `when` 返回 `this`;`implement` 未注册指令发 202;重复 `addCommand` 发 204 且返回已有对象、注册表大小不变;`deleteCommand` 后 `getCommand` 返回 `null` 且实现被清理;`iterateCommands` 遍历;`enterScope` 返回 `symbol`、`leaveScope()` 回上一级、`leaveScope(sym)` 保留 `sym` 并删除其上方作用域、未知 `symbol` 发 205、空栈无参发 205;子作用域激活时父作用域的条件与实现都不生效、退出后恢复;KeyUp 四条规则(单常规 + 辅助、0 常规 + 1 辅助、0 常规 + 2 辅助不触发、≥2 常规永不触发);`ctx` 的 `type` / `key` / `holding`(KeyUp 不含被抬起键)/ `ctrl` 等布尔;优先级降序、`block` 阻断严格更低、同优先级全部执行、匹配但无实现不阻断;`down` / `up` 的布尔返回;`listen` 的映射、`repeat` 跳过、`preventDefault` 只在触发时调用、`listen(null)` 解绑。**KeyDown 类模式的触发与时序不在本任务断言范围内。**
|
||||
(8) **`packages/system/src/hotkey/__test__/system.test.ts`(新建):** 文件头一行注释;**`import { HotkeySystem } from '../system';`——禁止 `../index`**;构造 fake 元素(`addEventListener` / `removeEventListener` 记录处理器)与伪事件(`keyCode` / `repeat` / `preventDefault`)。用例(每个 `it` 前一行注释):无作用域时 `when` / `implement` 各发 201 且 `when` 返回 `this`;`implement` 未注册指令发 202;重复 `addCommand` 发 204 且返回已有对象、注册表大小不变;`deleteCommand` 后 `getCommand` 返回 `null` 且实现被清理;`iterateCommands` 遍历;`enterScope` 返回 `symbol`、`leaveScope()` 回上一级、`leaveScope(sym)` 保留 `sym` 并删除其上方作用域、未知 `symbol` 发 205、空栈无参发 205;子作用域激活时父作用域的条件与实现都不生效、退出后恢复;KeyUp 四条规则(单常规 + 辅助、0 常规 + 1 辅助、0 常规 + 2 辅助不触发、≥2 常规永不触发);`ctx` 的 `type` / `key` / `holding`(KeyUp 不含被抬起键)/ `ctrl` 等布尔;优先级降序、`block` 阻断严格更低、同优先级全部执行、匹配但无实现不阻断;`down` / `up` 的布尔返回;`listen` 的映射、`repeat` 跳过、`preventDefault` 只在触发时调用、`listen(null)` 解绑;**`listen` 把 `keyCode` `91` / `92` / `224` 映射为 `KeyCode.Meta`(`holding` 含 Meta、`ctx.meta` 为真,且不依赖改 `keyCodes.ts`)**。**KeyDown 类模式的触发与时序不在本任务断言范围内。**
|
||||
|
||||
(8) 全部新文件以 CRLF 保存(`.prettierrc` 的 `endOfLine`),并在写完后对改动文件执行一次 `cmd /c "pnpm exec prettier --write <文件>"`。
|
||||
(9) 全部新文件以 CRLF 保存(`.prettierrc` 的 `endOfLine`),并在写完后对改动文件执行一次 `cmd /c "pnpm exec prettier --write <文件>"`。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec vitest run packages/system/src/hotkey/__test__/command.test.ts packages/system/src/hotkey/__test__/system.test.ts"</automated>
|
||||
<fails_when>non-zero exit,或任一用例失败:KeyUp 规则、作用域、告警 201 / 202 / 203 / 204 / 205、优先级 / block、`listen` 行为与设计不符</fails_when>
|
||||
<automated>cmd /c "pnpm exec eslint packages/system/src/hotkey/command.ts packages/system/src/hotkey/system.ts packages/system/src/hotkey/index.ts packages/system/src/hotkey/__test__/command.test.ts packages/system/src/hotkey/__test__/system.test.ts"</automated>
|
||||
<fails_when>non-zero exit:存在 eslint 报告(含未使用变量、格式、`no-console`)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const strip=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const cmdSrc=fs.readFileSync('packages/system/src/hotkey/command.ts','utf8');const sysSrc=fs.readFileSync('packages/system/src/hotkey/system.ts','utf8');if(!/class HotkeyCommand[\s\S]{0,40}implements IHotkeyCommand/.test(cmdSrc))bad.push('command.ts must declare class HotkeyCommand implementing IHotkeyCommand');if(!/class HotkeySystem[\s\S]{0,80}implements IHotkeySystem,\s*IExcitable<number>/.test(sysSrc))bad.push('system.ts must declare class HotkeySystem implementing IHotkeySystem and IExcitable<number> on the class');const idx=fs.readFileSync('packages/system/src/hotkey/index.ts','utf8');for(const m of ['./command','./system','./hotkey','./keyboard','./keyCodes','./type']){if(idx.indexOf(m)<0)bad.push('index.ts missing export for '+m)}const pkg=JSON.parse(fs.readFileSync('packages/system/package.json','utf8'));if(!pkg.dependencies||pkg.dependencies['@motajs/animate']!=='workspace:*')bad.push('packages/system must depend on @motajs/animate workspace');const lock=fs.readFileSync('pnpm-lock.yaml','utf8');const seg=lock.slice(lock.indexOf('packages/system:'),lock.indexOf('packages/types:'));if(seg.indexOf('@motajs/animate')<0)bad.push('pnpm-lock.yaml packages/system importer lacks animate');for(const p of ['packages/system/src/hotkey/command.ts','packages/system/src/hotkey/system.ts']){const c=strip(p);if(/(^|[^A-Za-z])import\s+type\s/.test(c))bad.push('type-only import in '+p);if(/(^|[^A-Za-z])(set)(Timeout|Interval)\s*\(/.test(c))bad.push('browser timer used in '+p);if(c.indexOf('Date.now(')>=0)bad.push('Date.now used in '+p)}for(const c of [201,202,203,204,205]){if(cmdSrc.indexOf('warn('+c)<0&&sysSrc.indexOf('warn('+c)<0)bad.push('warn '+c+' not referenced by command.ts or system.ts')}const tests=['packages/system/src/hotkey/__test__/command.test.ts','packages/system/src/hotkey/__test__/system.test.ts'];for(const p of tests){if(!fs.existsSync(p)){bad.push('missing '+p);continue}const lines=fs.readFileSync(p,'utf8').split(/\r?\n/);lines.forEach((l,i)=>{if(/(^|[^A-Za-z.])it\(/.test(l)){let j=i-1;while(j>=0&&lines[j].trim()==='')j--;if(j<0||!lines[j].trim().startsWith('//'))bad.push(p+':'+(i+1)+' it() without a preceding comment line')}})}if(bad.length){console.error('STRUCTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK structure: classes/exports/dependency/warns/tests/comment-discipline')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "STRUCTURE GATE FAILED":两个类的 `implements` 形态不符 / `index.ts` 导出缺失 / `package.json` 未声明 animate / lockfile 未更新 / 类型专用导入 / 浏览器定时器 / `Date.now` / 201–205 未全部被引用 / 测试文件缺失 / 某 `it` 前没有注释行</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const out=cp.spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'],{encoding:'utf8',timeout:170000});const text=(out.stdout||'')+(out.stderr||'');const bad=text.split(/\r?\n/).filter(l=>/packages[\\/]system[\\/]src[\\/]hotkey[\\/](command|system|index|__test__)/.test(l));if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey files type-check clean (repo has 2 pre-existing legacy errors in hotkey.ts/keyboard.ts, out of scope)')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "TYPECHECK FAILED":`command.ts` / `system.ts` / `index.ts` / `__test__/**` 存在类型错误(`hotkey.ts` / `keyboard.ts` 的 2 条既有 legacy 错误不在此列)</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const strip=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const cmdSrc=fs.readFileSync('packages/system/src/hotkey/command.ts','utf8');const sysSrc=fs.readFileSync('packages/system/src/hotkey/system.ts','utf8');if(!/class HotkeyCommand[\s\S]{0,40}implements IHotkeyCommand/.test(cmdSrc))bad.push('command.ts must declare class HotkeyCommand implementing IHotkeyCommand');if(!/class HotkeySystem[\s\S]{0,80}implements IHotkeySystem,\s*IExcitable<number>/.test(sysSrc))bad.push('system.ts must declare class HotkeySystem implementing IHotkeySystem and IExcitable<number> on the class');const idx=fs.readFileSync('packages/system/src/hotkey/index.ts','utf8');for(const m of ['./command','./system','./hotkey','./keyboard','./keyCodes','./type']){if(idx.indexOf(m)<0)bad.push('index.ts missing export for '+m)}const pkg=JSON.parse(fs.readFileSync('packages/system/package.json','utf8'));if(!pkg.dependencies||pkg.dependencies['@motajs/animate']!=='workspace:*')bad.push('packages/system must depend on @motajs/animate workspace');const lock=fs.readFileSync('pnpm-lock.yaml','utf8');const seg=lock.slice(lock.indexOf('packages/system:'),lock.indexOf('packages/types:'));if(seg.indexOf('@motajs/animate')<0)bad.push('pnpm-lock.yaml packages/system importer lacks animate');for(const p of ['packages/system/src/hotkey/command.ts','packages/system/src/hotkey/system.ts']){const c=strip(p);if(/(^|[^A-Za-z])import\s+type\s/.test(c))bad.push('type-only import in '+p);if(/(^|[^A-Za-z])(set)(Timeout|Interval)\s*\(/.test(c))bad.push('browser timer used in '+p);if(c.indexOf('Date.now(')>=0)bad.push('Date.now used in '+p)}for(const c of [201,202,203,204,205]){if(cmdSrc.indexOf('warn('+c)<0&&sysSrc.indexOf('warn('+c)<0)bad.push('warn '+c+' not referenced by command.ts or system.ts')}const log=JSON.parse(fs.readFileSync('packages/common/src/logger.json','utf8'));const D=String.fromCharCode(36),Q=String.fromCharCode(39);const want={201:'Cannot set hotkey condition or implement command since no scope is active.',202:'Cannot implement nonexistent hotkey command '+Q+D+'1'+Q+'.',203:'Hotkey command '+Q+D+'1'+Q+' has an invalid KeyUp combination: at most one non-auxiliary key is allowed.',204:'Duplicate hotkey command '+Q+D+'1'+Q+', the existing command will be used.',205:'Cannot leave nonexistent hotkey scope.'};for(const c of [201,202,203,204,205]){if(log.warn[c]!==want[c])bad.push('logger.json warn '+c+' missing or text mismatch')}if(typeof log.warn[200]!=='string'||log.warn[200].indexOf('Cannot buy or sell item since target item')!==0)bad.push('logger.json warn 200 was changed');if(typeof log.warn[1001]!=='string'||log.warn[1001].indexOf('Event(setBlock): Unknown tile')!==0)bad.push('logger.json warn 1001 was changed');if(Object.keys(log.warn).length!==206)bad.push('logger.json warn key count is '+Object.keys(log.warn).length+', expected 206');if(sysSrc.indexOf('KeyCode.Meta')<0)bad.push('system.ts must map keyCode 91/92/224 to KeyCode.Meta inside listen');const tests=['packages/system/src/hotkey/__test__/command.test.ts','packages/system/src/hotkey/__test__/system.test.ts'];const cmdTest=fs.existsSync(tests[0])?fs.readFileSync(tests[0],'utf8'):'';const sysTest=fs.existsSync(tests[1])?fs.readFileSync(tests[1],'utf8'):'';if(cmdTest&&(cmdTest.indexOf('../command')<0||cmdTest.indexOf('HotkeyCommand')<0||cmdTest.indexOf('isAuxiliaryKey')<0))bad.push('command.test.ts must import HotkeyCommand and isAuxiliaryKey from ../command');if(sysTest&&(sysTest.indexOf('../system')<0||sysTest.indexOf('HotkeySystem')<0))bad.push('system.test.ts must import HotkeySystem from ../system');if(cmdTest&&sysTest&&(cmdTest.indexOf('../index')>=0||sysTest.indexOf('../index')>=0))bad.push('tests must not import ../index (hotkey.ts registers document listeners at module scope)');for(const p of tests){if(!fs.existsSync(p)){bad.push('missing '+p);continue}const lines=fs.readFileSync(p,'utf8').split(/\r?\n/);lines.forEach((l,i)=>{if(/(^|[^A-Za-z.])it\(/.test(l)){let j=i-1;while(j>=0&&lines[j].trim()==='')j--;if(j<0||!lines[j].trim().startsWith('//'))bad.push(p+':'+(i+1)+' it() without a preceding comment line')}})}if(bad.length){console.error('STRUCTURE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK structure: classes/exports/dependency/logger-201-205/tests');"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "STRUCTURE GATE FAILED":两个类的 `implements` 形态不符 / `index.ts` 导出缺失 / `package.json` 未声明 animate / lockfile 未更新 / 类型专用导入 / 浏览器定时器 / `Date.now` / `logger.json` 的 201–205 文案不符或缺条 / `200` / `1001` 被改 / warn 键数不是 206 / `system.ts` 没有 `KeyCode.Meta` 映射 / 201–205 未全部被代码引用 / 测试文件缺失 / 测试未从 `../command` / `../system` 导入或误用 `../index` / 某 `it` 前没有注释行</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const out=cp.spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'],{encoding:'utf8',timeout:170000});const text=(out.stdout||'')+(out.stderr||'');const bad=[];if(out.error||out.status===null||out.signal)bad.push('vue-tsc did not run to completion: '+((out.error&&out.error.code)||out.signal||'status=null'));if(out.status!==0&&text.trim()==='')bad.push('vue-tsc exited with status '+out.status+' and produced no output - the command did not execute normally');const errs=text.split(/\r?\n/).filter(l=>/packages[\\/]system[\\/]src[\\/]hotkey[\\/](command|system|index|__test__)/.test(l));for(const e of errs)bad.push(e);if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey files type-check clean (vue-tsc exit '+out.status+'); 2 pre-existing legacy errors in hotkey.ts/keyboard.ts are out of scope')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "TYPECHECK FAILED":`vue-tsc` 没能正常跑完(`spawnSync` 报 `out.error` / `out.status === null` / 被信号终止),或非零退出且没有任何输出(命令没真正执行),或 `command.ts` / `system.ts` / `index.ts` / `__test__/**` 存在类型错误(`hotkey.ts` / `keyboard.ts` 的 2 条既有 legacy 错误不在此列)</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const r=cp.spawnSync('git',['status','--porcelain','--','packages/system/src/hotkey/hotkey.ts','packages/system/src/hotkey/keyboard.ts','packages/system/src/hotkey/type.ts'],{encoding:'utf8'});const out=(r.stdout||'').trim();if(r.status!==0||out.length>0){console.error('LEGACY GATE FAILED :: status='+r.status+' dirty='+out);process.exit(1)}console.log('OK legacy hotkey.ts/keyboard.ts/type.ts unchanged vs HEAD')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "LEGACY GATE FAILED":旧实现或 `type.ts` 在工作树中被改动(`type.ts` 预期零改动,因接口已无 `emit`);本闸门刻意针对「未提交工作树 vs HEAD」,退出码由 `spawnSync` 显式捕获,不依赖管道最后一段状态</fails_when>
|
||||
</verify>
|
||||
<done>两个测试文件全绿;eslint、结构闸门、按文件过滤的类型闸门与「旧实现 / `type.ts` 零改动」闸门全部通过;`HotkeyCommand` 与 `HotkeySystem` 已从 `hotkey/index.ts` 导出,`@motajs/animate` 已声明且 lockfile 已更新;KeyUp 热键端到端可用(含作用域、201 / 202 / 203 / 204 / 205、优先级与 block、`listen`)</done>
|
||||
<done>两个测试文件全绿;eslint、结构闸门、按文件过滤的类型闸门与「旧实现 / `type.ts` 零改动」闸门全部通过;`HotkeyCommand` 与 `HotkeySystem` 已从 `hotkey/index.ts` 导出,`@motajs/animate` 已声明且 lockfile 已更新;`logger.json` 已按 OI-5 追加 201–205 且 `200` / `1001` 原样;KeyUp 热键端到端可用(含作用域、201 / 202 / 203 / 204 / 205、优先级与 block、`listen` 与 Meta 映射)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
@ -308,12 +324,12 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
(1) **`down` 路由:** `down(key)` 在 `holding` 入栈后派发 KeyDown 类时,`runMatches` 按 `command.mode` 分派:`KeyUp` 不参与(保持 Task 1 行为);`Once` / `Continuous` / `Throttle` 参与。
|
||||
|
||||
(2) **`runMatches` 的执行分支(按模式与 `timeout`):**
|
||||
- `timeout` 为 0:当刻以 `{ command: id, index, keys, context }` 调用 handler;计数为「已触发」。
|
||||
- `timeout` 大于 0:入 `pending`(`fired` 0、`time` 为当前 `now`、快照 `mode` / `timeout` / `interval` / `dt` / 作用域 `symbol` / `handler` / `context` / `keys` / `index` / `id`),计数为「将要延时触发」。返回值因此为真。
|
||||
- `timeout` 为 0:当刻以 `{ command: id, index, keys, context: createContext(type, key) }` 调用 handler;计数为「已触发」。
|
||||
- `timeout` 大于 0:入 `pending`(`fired` 0、`time` 为当前 `now`,记录 `mode` / `timeout` / `interval` / `dt` / 作用域 `symbol` / `handler` / `type` / `key` / `keys` / `index` / `id`,**不记录 `context`**),计数为「将要延时触发」。返回值因此为真。
|
||||
- 立即触发的后续项:`Once` 不入队(只此一次);`KeyUp` 不入队(Task 1 行为不变);`Continuous` / `Throttle` 入队且 `fired` 置 1、`last` 置当前 `now`,以便第二次及之后由激励推进。
|
||||
- `block` 记录逻辑不变(同一批次里执行或入队都算「实际触发」)。
|
||||
|
||||
(3) **`excited(payload)`:** 更新 `now` 后调用 `flushPending()`;`flushPending` 先 `dropPending()`,再把到期项按其 `command.priority` 降序组成一批(同优先级全部执行,`block` 阻断严格更低优先级,规则与 `dispatch` 一致),执行时以上次快照的 `context` 作为 `IHotkeyEmitInfo.context`。到期判定:`fired` 为 0 → `payload - time >= timeout`;`fired` 为 1 → `payload - last >= interval`;`fired` 大于等于 1 时 `Continuous` 之后每个 `excited` 都触发、`Throttle` 需 `payload - last >= dt`。执行后更新 `last` 与 `fired`;`Once` 与 `KeyUp` 模式的项在触发后从 `pending` 移除,`Continuous` / `Throttle` 保留直到被 `dropPending` 丢弃。**时间只能来自 `payload`,不得引入浏览器定时器或 `Date.now()`。**
|
||||
(3) **`excited(payload)`:** 更新 `now` 后调用 `flushPending()`;`flushPending` 先 `dropPending()`,再把到期项按其 `command.priority` 降序组成一批(同优先级全部执行,`block` 阻断严格更低优先级,规则与 `dispatch` 一致)。**执行时用「当前 `holding`」重新构造 `IHotkeyEmitInfo.context`(`createContext(item.type, item.key)`),绝不使用入队时的 `context` 快照**——对齐 `type.ts` 第 29 行「当前所有正在按下的按键,按照按下的顺序排列」:延时期间新按下的键会出现在 `holding` 里,已松开的键不会。到期判定抽成私有方法 **`isPendingDue(item, payload)`**(OI-3):`fired` 为 0 → `payload - time >= timeout`;`fired` 为 1 → `payload - last >= interval`;`fired` 大于等于 1 时 `Continuous` 之后每个 `excited` 都触发、`Throttle` 需 `payload - last >= dt`。执行后更新 `last` 与 `fired`;`Once` 与 `KeyUp` 模式的项在触发后从 `pending` 移除,`Continuous` / `Throttle` 保留直到被 `dropPending` 丢弃。**时间只能来自 `payload`,不得引入浏览器定时器或 `Date.now()`。**
|
||||
|
||||
(4) **`dropPending` 扩展:** 在 Task 1 的三条丢弃条件之外,确保 `up` 时对整个 `holding` 重新评估(KeyDown 类项的组合必须仍被 `holding` 满足;KeyUp 类延时项不受 `holding` 限制),并在 `enterScope` / `leaveScope` 之后同样调用(栈顶变化即丢弃旧作用域项)。
|
||||
|
||||
@ -321,7 +337,7 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
|
||||
(6) **`command.ts` 不改**:若发现 Task 1 的模式方法未正确写入 `mode` / `timeout` / `interval` / `dt`,只做最小修正,并同步 `command.test.ts`(该文件不新增用例,除非修正行为)。
|
||||
|
||||
(7) **`system.test.ts` 追加用例(每个 `it` 前一行注释):** `Once` 按下即触发一次、重复 `down` 不重复触发、抬起后再按下可再次触发;KeyDown 子集语义(`[Ctrl, KeyS]` 需两键同时按住;`[KeyS]` 在按住 Ctrl 时仍触发,证明辅助键未写进组合不构成限制);`Continuous` 在 `timeout` 为 0 时当刻触发、后续按 `interval` 与逐帧触发;`Continuous` 在 `timeout` 大于 0 时未到期不触发、到期触发首次、再按 `interval` 触发第二次;`Throttle` 第二次需 `interval`、之后需 `dt`;`up` 后不再触发(组合不再满足即丢弃);`Once` 的 `timeout` 未到就松键则不触发(OI-4 ①);`excitedBy` 重复调用时旧控制器被 `revoke`、新控制器被 `add`,且未绑定激励源时 `timeout` 项永不触发;模式被改为 `KeyUp` 后旧待触发项被丢弃;`down` / `up` 在入队场景下仍返回真。假激励源用最小对象实现(`payload` / `excite` / `add` / `remove` / `destroy`),`add` 返回带 `revoke` 的控制器并记录。
|
||||
(7) **`system.test.ts` 追加用例(每个 `it` 前一行注释):** `Once` 按下即触发一次、重复 `down` 不重复触发、抬起后再按下可再次触发;KeyDown 子集语义(`[Ctrl, KeyS]` 需两键同时按住;`[KeyS]` 在按住 Ctrl 时仍触发,证明辅助键未写进组合不构成限制);`Continuous` 在 `timeout` 为 0 时当刻触发、后续按 `interval` 与逐帧触发;`Continuous` 在 `timeout` 大于 0 时未到期不触发、到期触发首次、再按 `interval` 触发第二次;`Throttle` 第二次需 `interval`、之后需 `dt`;`up` 后不再触发(组合不再满足即丢弃);`Once` 的 `timeout` 未到就松键则不触发(OI-4 ①);`excitedBy` 重复调用时旧控制器被 `revoke`、新控制器被 `add`,且未绑定激励源时 `timeout` 项永不触发;模式被改为 `KeyUp` 后旧待触发项被丢弃;`down` / `up` 在入队场景下仍返回真;**延时触发时的 `context` 按触发时刻重算**(`Continuous` 项入队后、`excited` 前再按下另一个键,触发时 `ctx.holding` 含新键;KeyUp 延时项触发时 `ctx.holding` 不含被抬起的键)。假激励源用最小对象实现(`payload` / `excite` / `add` / `remove` / `destroy`),`add` 返回带 `revoke` 的控制器并记录。
|
||||
|
||||
(8) 改动文件以 CRLF 保存,写完后执行 `cmd /c "pnpm exec prettier --write packages/system/src/hotkey/system.ts packages/system/src/hotkey/__test__/system.test.ts"`。
|
||||
</action>
|
||||
@ -330,14 +346,14 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
<fails_when>non-zero exit,或任一用例失败:`Once` / `Continuous` / `Throttle` 的触发次数与间隔、`timeout` / `interval` / `dt` 的边界、松键与作用域退出后的丢弃、模式切换后的丢弃、返回值语义与设计不符</fails_when>
|
||||
<automated>cmd /c "pnpm exec eslint packages/system/src/hotkey/system.ts packages/system/src/hotkey/__test__/system.test.ts"</automated>
|
||||
<fails_when>non-zero exit:存在 eslint 报告</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const strip=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const c=strip('packages/system/src/hotkey/system.ts');for(const m of ['HotkeyEmitMode.Once','HotkeyEmitMode.Continuous','HotkeyEmitMode.Throttle']){if(c.indexOf(m)<0)bad.push('system.ts does not reference '+m)}if(/(^|[^A-Za-z])(set)(Timeout|Interval)\s*\(/.test(c))bad.push('browser timer used in system.ts');if(c.indexOf('Date.now(')>=0)bad.push('Date.now used in system.ts');if(/(^|[^A-Za-z])import\s+type\s/.test(c))bad.push('type-only import in system.ts');if(c.indexOf('flushPending')<0||c.indexOf('dropPending')<0)bad.push('pending lifecycle helpers missing');const lines=fs.readFileSync('packages/system/src/hotkey/__test__/system.test.ts','utf8').split(/\r?\n/);let its=0;lines.forEach((l,i)=>{if(/(^|[^A-Za-z.])it\(/.test(l)){its++;let j=i-1;while(j>=0&&lines[j].trim()==='')j--;if(j<0||!lines[j].trim().startsWith('//'))bad.push('system.test.ts:'+(i+1)+' it() without a preceding comment line')}});if(its<20)bad.push('system.test.ts has only '+its+' it() cases, expected at least 20 covering both tasks');if(bad.length){console.error('MODE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK mode gate: three KeyDown modes wired, excitation-driven timing, '+its+' system test cases')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "MODE GATE FAILED":三类 KeyDown 模式未被引用 / 出现浏览器定时器或 `Date.now` / 类型专用导入 / 待触发生命周期方法缺失 / `it` 前无注释 / 用例少于 20 条</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const out=cp.spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'],{encoding:'utf8',timeout:170000});const text=(out.stdout||'')+(out.stderr||'');const bad=text.split(/\r?\n/).filter(l=>/packages[\\/]system[\\/]src[\\/]hotkey[\\/](command|system|index|__test__)/.test(l));if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey files type-check clean')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "TYPECHECK FAILED":本计划文件出现类型错误</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const bad=[];const lines=cp.execSync('git status --porcelain',{encoding:'utf8'}).split(/\r?\n/).filter(Boolean);const allowed=['packages/system/src/hotkey/command.ts','packages/system/src/hotkey/system.ts','packages/system/src/hotkey/index.ts','packages/system/src/hotkey/__test__/','packages/system/package.json','pnpm-lock.yaml','.planning/phases/09-minimum-browser-runnable/09-23-'];for(const l of lines){const p=l.slice(3).trim();if(!p.startsWith('packages/'))continue;if(!allowed.some(a=>p===a||p.startsWith(a)))bad.push('out-of-scope change: '+p)}for(const l of lines){const p=l.slice(3).trim();if(['packages/system/src/hotkey/hotkey.ts','packages/system/src/hotkey/keyboard.ts','packages/system/src/hotkey/type.ts'].indexOf(p)>=0)bad.push('must not be modified: '+p)}if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope gate: only plan files changed under packages/')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "SCOPE GATE FAILED":`packages/` 下出现计划外改动,或旧实现 / `type.ts` 被修改</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const strip=p=>fs.readFileSync(p,'utf8').split(/\r?\n/).filter(l=>!/^\s*(\/\/|\*|\/\*)/.test(l)).join('\n');const c=strip('packages/system/src/hotkey/system.ts');for(const m of ['HotkeyEmitMode.Once','HotkeyEmitMode.Continuous','HotkeyEmitMode.Throttle']){if(c.indexOf(m)<0)bad.push('system.ts does not reference '+m)}if(/(^|[^A-Za-z])(set)(Timeout|Interval)\s*\(/.test(c))bad.push('browser timer used in system.ts');if(c.indexOf('Date.now(')>=0)bad.push('Date.now used in system.ts');if(/(^|[^A-Za-z])import\s+type\s/.test(c))bad.push('type-only import in system.ts');if(c.indexOf('flushPending')<0||c.indexOf('dropPending')<0||c.indexOf('isPendingDue')<0)bad.push('pending lifecycle helpers missing (flushPending/dropPending/isPendingDue)');const lines=fs.readFileSync('packages/system/src/hotkey/__test__/system.test.ts','utf8').split(/\r?\n/);let its=0;lines.forEach((l,i)=>{if(/(^|[^A-Za-z.])it\(/.test(l)){its++;let j=i-1;while(j>=0&&lines[j].trim()==='')j--;if(j<0||!lines[j].trim().startsWith('//'))bad.push('system.test.ts:'+(i+1)+' it() without a preceding comment line')}});if(its<20)bad.push('system.test.ts has only '+its+' it() cases, expected at least 20 covering both tasks');if(bad.length){console.error('MODE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK mode gate: three KeyDown modes wired, excitation-driven timing, '+its+' system test cases')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "MODE GATE FAILED":三类 KeyDown 模式未被引用 / 出现浏览器定时器或 `Date.now` / 类型专用导入 / 待触发生命周期方法(`flushPending` / `dropPending` / `isPendingDue`)缺失 / `it` 前无注释 / 用例少于 20 条</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const out=cp.spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'],{encoding:'utf8',timeout:170000});const text=(out.stdout||'')+(out.stderr||'');const bad=[];if(out.error||out.status===null||out.signal)bad.push('vue-tsc did not run to completion: '+((out.error&&out.error.code)||out.signal||'status=null'));if(out.status!==0&&text.trim()==='')bad.push('vue-tsc exited with status '+out.status+' and produced no output - the command did not execute normally');const errs=text.split(/\r?\n/).filter(l=>/packages[\\/]system[\\/]src[\\/]hotkey[\\/](command|system|index|__test__)/.test(l));for(const e of errs)bad.push(e);if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey files type-check clean (vue-tsc exit '+out.status+')')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "TYPECHECK FAILED":`vue-tsc` 没能正常跑完(`out.error` / `out.status === null` / 信号终止),或非零退出且无任何输出(命令未真正执行),或本计划文件出现类型错误</fails_when>
|
||||
<automated>node -e "const fs=require('fs'),cp=require('child_process');const bad=[];const plan=fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-23-PLAN.md','utf8');const start=plan.indexOf('files_modified:');const end=plan.indexOf('autonomous:');if(start<0||end<0)bad.push('cannot read files_modified from the plan');const allow=start>=0&&end>start?plan.slice(start,end).split(/\r?\n/).slice(1).map(s=>s.replace(/^\s*-\s*/,'').trim()).filter(Boolean):[];if(allow.indexOf('packages/common/src/logger.json')<0)bad.push('allowlist derived from files_modified must include logger.json');const wipUntracked=['packages-user/client-base/src/hotkey/'];const wipAny=['docs/advanced/','.planning/phases/10-docs-restructure/'];const lines=cp.execSync('git status --porcelain',{encoding:'utf8'}).split(/\r?\n/).filter(Boolean);for(const l of lines){const p=l.slice(3).trim();if(allow.some(a=>p===a||a.startsWith(p)||p.startsWith(a)))continue;if(p.startsWith('.planning/'))continue;if(l.slice(0,2)==='??'&&wipUntracked.some(a=>p.startsWith(a)))continue;if(wipAny.some(a=>p.startsWith(a)))continue;bad.push('out-of-scope change: '+p)}for(const p of ['packages/system/src/hotkey/hotkey.ts','packages/system/src/hotkey/keyboard.ts','packages/system/src/hotkey/type.ts','packages/system/src/hotkey/keyCodes.ts']){if(lines.some(l=>l.slice(3).trim()===p))bad.push('must not be modified: '+p)}if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope gate: every change matches files_modified ('+allow.length+' entries, user WIP excepted)')"</automated>
|
||||
<fails_when>non-zero exit,或 stderr 含 "SCOPE GATE FAILED":工作树出现 `files_modified` 之外的改动(白名单从本计划 `files_modified` 派生,含 `logger.json`;例外为 `.planning/**` 流程产物、用户既有 WIP(`packages-user/client-base/src/hotkey/` 仅限未跟踪项,`docs/advanced/`、`.planning/phases/10-docs-restructure/` 原样)),或旧实现 / `type.ts` / `keyCodes.ts` 被修改,或白名单里缺 `logger.json`</fails_when>
|
||||
</verify>
|
||||
<done>`Once` / `Continuous` / `Throttle` 三类按下模式与 `timeout` / `interval` / `dt` 全部由激励源 payload 驱动且无定时器;待触发项在松键、作用域退出、模式切换时被正确丢弃;`down` / `up` 返回值含入队场景;`system.test.ts` 至少 20 条用例全绿;eslint、结构闸门、类型闸门与范围闸门全部通过</done>
|
||||
<done>`Once` / `Continuous` / `Throttle` 三类按下模式与 `timeout` / `interval` / `dt` 全部由激励源 payload 驱动且无定时器;待触发项在松键、作用域退出、模式切换时被正确丢弃;**延时触发时 `context` 按当时 `holding` 重算(无入队快照)**;`down` / `up` 返回值含入队场景;`system.test.ts` 至少 20 条用例全绿;eslint、`MODE GATE`、类型闸门与范围闸门全部通过</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@ -352,7 +368,8 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
| 激励源 → 系统 | `excited(payload)` 的 `payload` 决定全部时序,外部激励源可推进任意时间步 |
|
||||
| 作用域 → 实现 | 只有栈顶作用域的实现与条件参与判定,作用域切换即撤销上方实现 |
|
||||
| 依赖边界 | `packages/system` 新增 `@motajs/animate`(workspace)依赖,lockfile 由 pnpm 生成 |
|
||||
| 写入边界 | 本增量是写操作,只允许写 `files_modified` 白名单 |
|
||||
| 日志文案 → 运行期 | `packages/common/src/logger.json` 的 `warn` 段是告警文案的唯一来源:条目缺失会把 201–205 变成 `error 16`,条目被改则输出错误文本 |
|
||||
| 写入边界 | 本增量是写操作,只允许写 `files_modified` 白名单(含 `logger.json`) |
|
||||
|
||||
**威胁级别说明(security_enforcement = ASVS L1,block on high):** 本计划为**纯内存按键系统实现**,不执行第三方代码、不引入网络面、不新增外部依赖(仅同仓库 workspace 依赖),**不存在未缓解的 `high` 级威胁**;`T-09-23-SC` 虽标 `high`,已由「workspace 依赖 + lockfile 由 pnpm 生成 + 不新增第三方包」缓解。
|
||||
|
||||
@ -360,20 +377,20 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-23-01 | Tampering | `listen` 的事件映射(`EVENT_KEY_CODE_MAP`)与 `event.repeat` 处理 | medium | mitigate | `repeat` 一律跳过;无映射(`undefined` / 0)跳过;`down` 对已在 `holding` 的键去重兜底;`listen(null)` 完整解绑,避免残留处理器 |
|
||||
| T-09-23-01 | Tampering | `listen` 的事件映射(`EVENT_KEY_CODE_MAP` + 本地 `91` / `92` / `224` → `Meta`)与 `event.repeat` 处理 | medium | mitigate | `repeat` 一律跳过;无映射(`undefined` / 0)跳过;Meta 靠 `listen` 内的本地映射进入 `holding`(不改 `keyCodes.ts`),单测覆盖;`down` 对已在 `holding` 的键去重兜底;`listen(null)` 完整解绑,避免残留处理器 |
|
||||
| T-09-23-02 | Elevation of privilege | 作用域隔离(`when` / `implement` 只在栈顶生效) | high | mitigate | 派发只遍历栈顶作用域的 `handlers`;`enterScope` / `leaveScope` 后调用 `dropPending()` 丢弃旧作用域项;测试覆盖「子作用域激活时父作用域条件与实现均不生效」与「退出后恢复」 |
|
||||
| T-09-23-03 | Denial of service | `holding` / `pending` 无界增长 | medium | mitigate | `holding` 去重且长度受物理按键数限制;`pending` 在松键、作用域退出、模式切换时被 `dropPending` 清理,`Once` / `KeyUp` 触发后移除;测试覆盖丢弃路径 |
|
||||
| T-09-23-04 | Tampering | `type.ts` / 旧实现被误改 | high | mitigate | `git diff --exit-code` 针对 `hotkey.ts` / `keyboard.ts` / `type.ts`;`SCOPE GATE` 校验 `packages/` 下无计划外改动;prohibitions 明列 |
|
||||
| T-09-23-05 | Repudiation | 告警码与消息被改写或新增 | low | mitigate | 只用已批准的 warn 201–205,消息逐字采用;结构闸门校验 201–205 均被引用;不改 `logger.json` 既有条目 |
|
||||
| T-09-23-04 | Tampering | `type.ts` / 旧实现被误改 | high | mitigate | `git status --porcelain` 对 `hotkey.ts` / `keyboard.ts` / `type.ts` / `keyCodes.ts` 做零改动断言;`SCOPE GATE` 用 `files_modified` 派生的白名单校验全部工作树改动;prohibitions 明列 |
|
||||
| T-09-23-05 | Repudiation | 告警码与消息被改写或新增 | low | mitigate | 只用已批准的 warn 201–205,文案逐字采用 OI-5 写入 `logger.json`;结构闸门 `JSON.parse` 后逐条比对文本、断言 `warn` 键数为 206 且 `200` / `1001` 前缀未变;不做其它 `logger.json` 改动(该文件不在「不得触碰」清单,但被范围闸门的 `files_modified` 白名单限定为唯一允许的日志改动) |
|
||||
| T-09-23-06 | Information disclosure | 无网络 / 无存储 / 无日志输出敏感数据 | low | accept | 系统只在内存中保存按键状态与指令 id,不写存储、不发网络请求;`logger.warn` 只输出指令 id |
|
||||
| T-09-23-SC | Tampering | 依赖安装(`@motajs/animate`) | high | mitigate | 只新增同仓库 `workspace:*` 依赖,不引入第三方包;`pnpm-lock.yaml` 由 `pnpm install --lockfile-only` 生成,不手改;`SCOPE GATE` 限制 `packages/` 改动范围 |
|
||||
| T-09-23-SC | Tampering | 依赖安装(`@motajs/animate`) | high | mitigate | 只新增同仓库 `workspace:*` 依赖,不引入第三方包;`pnpm-lock.yaml` 由 `pnpm install --lockfile-only` 生成,不手改;`SCOPE GATE` 白名单由 `files_modified` 派生并含 `logger.json` |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **Task 0**:`PRESTATE` 闸门通过;用户明确回复「可以执行」(含 OI-1 / OI-2 / OI-3 / OI-4 的裁定)。
|
||||
- **Task 1**:`command.test.ts` + `system.test.ts` 全绿;eslint;结构闸门(类 `implements` 形态、`index.ts` 六个导出、animate 依赖与 lockfile、无类型专用导入 / 无定时器 / 无 `Date.now`、201–205 均被引用、`it` 前注释);按文件过滤的类型闸门;`hotkey.ts` / `keyboard.ts` / `type.ts` 零改动。
|
||||
- **Task 2**:两个测试文件全绿(`system.test.ts` 不少于 20 条用例);eslint;`MODE GATE`(三类 KeyDown 模式接线、激励驱动、生命周期方法);类型闸门;`SCOPE GATE`(`packages/` 下无计划外改动)。
|
||||
- **范围诚实**:不跑 `build:game` / `build:packages` / `check:type` 的修复动作;不修 `hotkey.ts` / `keyboard.ts` 的 legacy 类型错误;不接线 `packages-user/**`;SUMMARY 须如实记录这三条边界,不得表述为「浏览器端已可用」。
|
||||
- **Task 0**:`PRESTATE` 闸门通过(含 `warn 200` / `warn 1001` 均在、201–205 空闲);用户明确回复「可以执行」(含 OI-1 / OI-2 / OI-3 / OI-4 / OI-5 的裁定)。
|
||||
- **Task 1**:`command.test.ts` + `system.test.ts` 全绿;eslint;结构闸门(类 `implements` 形态、`index.ts` 六个导出、animate 依赖与 lockfile、无类型专用导入 / 无定时器 / 无 `Date.now`、**`logger.json` 201–205 逐字文案 + `200` / `1001` 完好 + warn 键数 206**、`system.ts` 含 `KeyCode.Meta` 映射、测试只从 `../command` / `../system` 导入且禁 `../index`、`it` 前注释);按文件过滤的类型闸门(先校验 `spawnSync` 的 `out.error` / `out.status` / `out.signal`);`hotkey.ts` / `keyboard.ts` / `type.ts` 零改动。
|
||||
- **Task 2**:两个测试文件全绿(`system.test.ts` 不少于 20 条用例);eslint;`MODE GATE`(三类 KeyDown 模式接线、激励驱动、`flushPending` / `dropPending` / `isPendingDue`);类型闸门(同样校验命令是否正常执行);`SCOPE GATE`(白名单由 `files_modified` 派生、含 `logger.json`,用户既有 WIP 例外)。
|
||||
- **范围诚实**:不跑 `build:game` / `build:packages` / `check:type` 的修复动作;不修 `hotkey.ts` / `keyboard.ts` 的 legacy 类型错误;不接线 `packages-user/**`;SUMMARY 须如实记录这三条边界,不得表述为「浏览器端已可用」;**`estimate.tokens=95000` 是未校准估算(`confidence: low`),SUMMARY 须显式声明**。
|
||||
- **既有注释**:`type.ts` / `hotkey.ts` / `keyboard.ts` / `keyCodes.ts` 的既有 jsDoc 与注释零修改。
|
||||
</verification>
|
||||
|
||||
@ -383,12 +400,13 @@ Phase 9 · 第 23 个计划 —— **实现用户重新设计的按键系统**
|
||||
- 无作用域时 `when` / `implement` → warn 201;`implement` 未注册指令 → warn 202;`leaveScope` 未知作用域 → warn 205。
|
||||
- 作用域:`enterScope` 压栈返回 `symbol`、新作用域无额外条件、`leaveScope(sym)` 保留 `sym`、只有栈顶作用域生效。
|
||||
- KeyUp 四条规则与 203 的两个发出时机符合口径;KeyDown 组合为 `holding` 子集即触发,辅助键写进组合才构成限制。
|
||||
- `holding` / `ctx` 推导正确(KeyDown 含触发键、KeyUp 不含被抬起键);`event.repeat` 被忽略。
|
||||
- `holding` / `ctx` 推导正确(KeyDown 含触发键、KeyUp 不含被抬起键);`event.repeat` 被忽略;`listen` 把 `91` / `92` / `224` 映射为 `KeyCode.Meta`(`keyCodes.ts` 零改动)。
|
||||
- 优先级降序、同优先级全触发、`block` 只按已实现且实际触发的指令阻断严格更低优先级。
|
||||
- 全时序由 `excited(payload)` 驱动(`timeout` / `interval` / `dt`),无任何定时器与 `Date.now`。
|
||||
- 全时序由 `excited(payload)` 驱动(`timeout` / `interval` / `dt`),无任何定时器与 `Date.now`;**延时触发时 `context` 按当时 `holding` 重算**。
|
||||
- `excitedBy` 单激励源切换(先 `revoke` 再 `add`);`listen` 映射 / `repeat` / `preventDefault` / 解绑 / `listening` 符合口径。
|
||||
- `packages/system/package.json` 声明 `@motajs/animate: workspace:*`,`pnpm-lock.yaml` 已由 pnpm 更新;`hotkey/index.ts` 导出新模块;旧实现与 `type.ts` 零改动。
|
||||
- SUMMARY 记录最终语义口径(含 OI-4 裁定)、命名获批情形、门禁结果与已知边界(未接线 user 代码、未跑全量构建、legacy 类型错误保留),并声明 `estimate.tokens=95000` 为未校准估算(`confidence: low`)。
|
||||
- `packages/common/src/logger.json` 的 `warn` 段按 OI-5 追加 201–205(逐字一致),`200` / `1001` 等既有条目零改动。
|
||||
- SUMMARY 记录最终语义口径(含 OI-4 裁定与 OI-5 文案落地情形)、命名获批情形、门禁结果与已知边界(未接线 user 代码、未跑全量构建、legacy 类型错误保留),并声明 `estimate.tokens=95000` 为未校准估算(`confidence: low`)。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user