docs(09-24): fix tautological FINAL GATE OI-3 assertion (revision 4)

This commit is contained in:
unanmed 2026-10-23 14:34:51 +08:00
parent 622b48e0bc
commit 51bbc047c1

View File

@ -65,7 +65,7 @@ must_haves:
- "**`context.holding` 是活引用,只有修饰键布尔量是匹配时刻快照(已核验实现)**:`createContext()`(`system.ts:110-118`)返回 `{ holding: this.holding, ctrl: this.holdingSet.has(KeyCode.Ctrl), ... }`,`holding` 直接指向系统内部数组;`up()` 在 `holdingSet.delete` 之后、构造 `toEmit` 与 `emit` 之前执行 `this.holding.splice(index, 1)`。因此 KeyUp 触发时 `info.context.holding` **不含**被抬起的主键:`[Ctrl, KeyS]` 抬 S → `[Ctrl]`;`[KeyS]` 抬 S → `[]`。`keyUp(timeout)` 的延时项读取的仍是同一个活数组,所以延时到期的断言同样按派发时刻的内容写(`[]`)。**不得改 `createContext`**(`holding: this.holding` 逐字保留)。"
- "**`down` / `up` 返回值按当前实现**:KeyUp 在 `up` 内立即触发后 `holdingCmds` 已删除该指令、`waitTimeout` 为空 → `up` 返回 `false`;而 `down` 建立起待触发项 → 返回 `true`。测试按此断言并注明「返回值反映调用后仍存在的待触发项」。本轮**不在 `down` / `up` 上设任何条件闸口**(条件随 pending 保存,只在 `emit` 与 block 循环生效),因此这两种返回值与修正前逐字一致;条件为假时 `down` 仍返回 `true`(pending 已建立),这也是「条件为假不阻止跟踪」的观测点。"
- "**Task 3 的 `excited` 断言只覆盖不会踩到 OI-3 的形态**:`Once + timeout` 只走 `pending.emitted === 0` 分支(`Once` 不进 `Continuous` 分支,故 99 不触发 / 100 触发 / 之后不再触发是确定的);`Continuous + timeout === 0` 在 `addPending` 已立即触发(`emitted` 从 1 起算,随后同一次 `excited` 调用里 `dt2 = payload - lastEmit`),因此 `interval` / `dt` 断言不受 OI-3 影响。**`Continuous + timeout > 0` 不在本计划断言范围内**(见 OI-3,未裁定),Task 3 也不得声称已修正该组合。"
- "**本轮修订前基线是已提交的 09-23 实现(INFO)**:`git log` 中 `cfdca2f` 与 `b79fc54`(`docs(09-24)` 的两版计划)都是纯计划提交,其共同祖先 `3fd339f feat: 抬起按键的优先级` 即 09-23 交付的实现与测试;Task 0 的前置校验与 `LEGACY GATE` 都以该已提交内容为基线。修订 3 只改 `09-24-PLAN.md`,未触碰任何代码 / 测试,`git status --porcelain` 对 `system.ts` / `command.ts` / 两个测试文件为空。"
- "**本轮修订前基线是已提交的 09-23 实现(INFO)**:`git log` 中 `cfdca2f` 与 `b79fc54`(`docs(09-24)` 的两版计划)都是纯计划提交,其共同祖先 `3fd339f feat: 抬起按键的优先级` 即 09-23 交付的实现与测试;Task 0 的前置校验与 `LEGACY GATE` 都以该已提交内容为基线。修订 3 / 修订 4 只改 `09-24-PLAN.md`,未触碰任何代码 / 测试,`git status --porcelain` 对 `system.ts` / `command.ts` / 两个测试文件为空。"
prohibitions:
- "不得在用户回复通过 Task 0 关卡之前执行任何任务或派发执行子代理。"
- "**不得修正确认清单以外的任何行为**:只能落地 5 处修正;`down` / `up` 返回值、`createContext()` 的 `holding: this.holding` 活引用与四个修饰键布尔量的匹配时刻快照、`priority` / `block` 的生效范围、`waitTimeout` 的计时起点、未实现指令的待触发项、`excited` 的分支结构一律按现状保留。**疑似问题 OI-3 未经用户确认前不得修改,也不得写成已确认缺陷。**"
@ -98,7 +98,7 @@ Phase 9 · 第 24 个计划 —— **修正用户重写后按键系统(`comman
- `__test__/command.test.ts`:整体重写(Task 2 与修正 5 同批交付并跑绿)。
- 不碰 `type.ts` / `hotkey.ts` / `keyboard.ts` / `keyCodes.ts` / `index.ts` / `logger.json` / `package.json` / lockfile / `packages-user/**` / `src/**`。
**修订 3(本轮,按用户对 `when` 语义的裁定重写修正 2):**
**修订 3(按用户对 `when` 语义的裁定重写修正 2):**
- ① 作废先前「私有判定方法 + `down` / `up` / `emit` 三处闸口」的方案(它在派发时刻求值,与用户裁定不符);改为**匹配时刻求值 + 随 pending 保存**,且不新增任何私有方法。
- ② `IHotkeyCommandPending` 新增 `matchedWhen`(OI-1 待批准),表达式 `this.getScope()?.condition?.() ?? true`(无作用域 / `null` / 缺失一律视为 `true`)落在与 `createContext()` 完全相同的三处捕获点:`addPending`、`down` 的再匹配分支、`up` 的换组合分支。
- ③ 条件为假**不阻止**建立 `holdingCmds`(`up` 依赖它);`down` 不再有任何条件闸口,`down` / `up` 返回值与修正前逐字一致。
@ -108,6 +108,8 @@ Phase 9 · 第 24 个计划 —— **修正用户重写后按键系统(`comman
- ⑦ 测试新增 10 条 `when` 用例:条件为假仍建立 pending、KeyDown 无新匹配则永不触发、新匹配重新求值后触发、松键重按不补发、KeyUp 抬起不重新求值、KeyUp 新匹配后抬起触发、带 `timeout` 的假值不补发 / 真值按捕获值触发、`block` 只统计会触发的指令(顺序化条件区分同一次 `down` 内的两条指令)、`up` 换组合时重新求值。
- ⑧ 沿用上一版仍然有效的裁定:KeyUp 的 `context.holding` 是「**活引用 + 修饰键快照**」(`[Ctrl, KeyS]` 抬 S → `[Ctrl]`,`[KeyS]` 抬 S → `[]`,`createContext` 不改);Task 1 不导入 `@motajs/animate`、不写激励用例,Task 3 再加回 `createExcitation()`;修正 4 的承诺范围仅 `Once + timeout > 0` 与 `Continuous + timeout === 0`,`Continuous + timeout > 0` 按 **OI-3** 记录为未裁定、保持现状且不写断言;SUMMARY 须声明 `estimate` 为未校准估算(`confidence: low`),并注明修订前基线是已提交的 09-23 实现(`3fd339f`)。
**修订 4(本轮,仅修订 Task 3 的 `FINAL GATE`):** 上一版 Task 3 `FINAL GATE` 里守护 OI-3 的断言 `/pending\.emitted === 0 && dt >= command\.timeout[\s\S]{0,80}else if/` 是**恒不匹配**的(`system.ts` 的 `excited` 里没有 `else if`),等于没有任何守护;改为**分支形态断言**:要求 `sys.includes('if (pending.emitted === 1) {')`、`sys.includes('if (dt2 >= command.dt)')`,并禁止 `/emitted\s*>\s*1/`,未获批准改动该分支时断言即失败。其余门禁(`matchedWhen` 成员、`condition?.()` 恰好 3 处、`emit` 首行闸口、`up` 的 block 循环、`down` 内无 `condition`、`dt >= timeout`、测试卫生与用例数、SUMMARY 存在)一字不动。本轮只改 `09-24-PLAN.md`,不触碰代码 / 测试。
**约束:** 只动 `files_modified` 内的 5 个文件;除 5 处修正外不改任何行为(含 `down` / `up` 返回值、`createContext()` 的 `holding` 活引用与修饰键快照、`priority` / `block` 的生效范围);不把疑似问题 OI-3 当成已确认缺陷;不跑全量构建。
**修改方式声明(AGENTS.md):** 本计划执行含文件写入(改写 2 个实现文件的部分行、整体重写 2 个测试文件、写 1 个 SUMMARY),由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。
@ -423,7 +425,7 @@ Phase 9 · 第 24 个计划 —— **修正用户重写后按键系统(`comman
<fails_when>non-zero exit,或任一用例失败:`when` 未在匹配时刻捕获(条件为假仍触发 / KeyUp 抬起又去重新求值 / 新匹配后 pending 的捕获值未更新 / 条件为假的 `block` 指令把低优先级项 `break` 掉)/ `timeout` 边界(99 与 100)/ `interval` / `dt` 的触发次数 / 松键后仍触发 / `excitedBy` 双源同时驱动 / KeyUp 的 `context.holding` 不是「活引用、不含被抬起的主键」</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 sys=fs.readFileSync('packages/system/src/hotkey/system.ts','utf8');if(!/interface IHotkeyCommandPending[\s\S]{0,800}matchedWhen: boolean/.test(sys))bad.push('IHotkeyCommandPending lacks the matchedWhen member');const evals=(sys.match(/this\.getScope\(\)\?\.condition\?\.\(\)\s*\?\?\s*true/g)||[]).length;if(evals!==3)bad.push('expected exactly 3 when-capture sites (addPending + down re-match + up re-match), found '+evals);if(!/private emit\(pending: IHotkeyCommandPending\)\s*\{\s*if \(!pending\.matchedWhen\) return;/.test(sys))bad.push('emit must skip pendings whose captured when is false');if(!/priority < blocking\) break;\s*if \(!pending\.matchedWhen\) continue;/.test(sys))bad.push('the block/priority loop must skip pendings whose captured when is false, after the break check');const downBody=sys.slice(sys.indexOf('down(key: KeyCode): boolean'), sys.indexOf('up(key: KeyCode): boolean'));if(/if\s*\([^)]*(condition|matchedWhen)/.test(downBody))bad.push('down must not gate on when (pendings must still be tracked for up)');if(sys.indexOf('dt >= command.timeout')<0)bad.push('excited first-trigger comparison is not dt >= timeout');if(sys.indexOf('dt < command.timeout')>=0)bad.push('old dt < timeout comparison still present');if(/pending\.emitted === 0 && dt >= command\.timeout[\s\S]{0,80}else if/.test(sys))bad.push('OI-3 branch was modified without approval');const t=fs.readFileSync('packages/system/src/hotkey/__test__/system.test.ts','utf8');for(const s of ['HotkeyEmitType','HotkeyEmitMode.Throttle','.throttle(','context.type','context.key','../index']){if(t.indexOf(s)>=0)bad.push('system.test.ts still references obsolete design token: '+s)}if(t.indexOf('matchedWhen')>=0)bad.push('system.test.ts must not depend on the private pending member name');for(const s of ['when(','excitedBy(','excite(','timeout']){if(t.indexOf(s)<0)bad.push('system.test.ts lacks coverage for '+s)}const lines=t.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<36)bad.push('system.test.ts has only '+its+' it() cases, expected at least 36 across Task 1 + Task 3');if(!fs.existsSync('.planning/phases/09-minimum-browser-runnable/09-24-SUMMARY.md'))bad.push('09-24-SUMMARY.md missing');if(bad.length){console.error('FINAL GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK final gate: when captured at 3 match-time sites, emit/block honour it, timeout comparison fixed, '+its+' system cases, SUMMARY present')"</automated>
<automated>node -e "const fs=require('fs');const bad=[];const sys=fs.readFileSync('packages/system/src/hotkey/system.ts','utf8');if(!/interface IHotkeyCommandPending[\s\S]{0,800}matchedWhen: boolean/.test(sys))bad.push('IHotkeyCommandPending lacks the matchedWhen member');const evals=(sys.match(/this\.getScope\(\)\?\.condition\?\.\(\)\s*\?\?\s*true/g)||[]).length;if(evals!==3)bad.push('expected exactly 3 when-capture sites (addPending + down re-match + up re-match), found '+evals);if(!/private emit\(pending: IHotkeyCommandPending\)\s*\{\s*if \(!pending\.matchedWhen\) return;/.test(sys))bad.push('emit must skip pendings whose captured when is false');if(!/priority < blocking\) break;\s*if \(!pending\.matchedWhen\) continue;/.test(sys))bad.push('the block/priority loop must skip pendings whose captured when is false, after the break check');const downBody=sys.slice(sys.indexOf('down(key: KeyCode): boolean'), sys.indexOf('up(key: KeyCode): boolean'));if(/if\s*\([^)]*(condition|matchedWhen)/.test(downBody))bad.push('down must not gate on when (pendings must still be tracked for up)');if(sys.indexOf('dt >= command.timeout')<0)bad.push('excited first-trigger comparison is not dt >= timeout');if(sys.indexOf('dt < command.timeout')>=0)bad.push('old dt < timeout comparison still present');const branchShape=sys.includes('if (pending.emitted === 1) {')&&sys.includes('if (dt2 >= command.dt)')&&!/emitted\s*>\s*1/.test(sys);if(!branchShape)bad.push('OI-3 branch shape changed without approval: require if (pending.emitted === 1) { and if (dt2 >= command.dt), forbid emitted > 1');const t=fs.readFileSync('packages/system/src/hotkey/__test__/system.test.ts','utf8');for(const s of ['HotkeyEmitType','HotkeyEmitMode.Throttle','.throttle(','context.type','context.key','../index']){if(t.indexOf(s)>=0)bad.push('system.test.ts still references obsolete design token: '+s)}if(t.indexOf('matchedWhen')>=0)bad.push('system.test.ts must not depend on the private pending member name');for(const s of ['when(','excitedBy(','excite(','timeout']){if(t.indexOf(s)<0)bad.push('system.test.ts lacks coverage for '+s)}const lines=t.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<36)bad.push('system.test.ts has only '+its+' it() cases, expected at least 36 across Task 1 + Task 3');if(!fs.existsSync('.planning/phases/09-minimum-browser-runnable/09-24-SUMMARY.md'))bad.push('09-24-SUMMARY.md missing');if(bad.length){console.error('FINAL GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK final gate: when captured at 3 match-time sites, emit/block honour it, timeout comparison fixed, '+its+' system cases, SUMMARY present')"</automated>
<fails_when>non-zero exit,或 stderr 含 "FINAL GATE FAILED":`matchedWhen` 成员缺失 / `condition?.()` 不是恰好 3 处(多了说明又在触发时刻现算,少了说明漏了捕获点)/ `emit` 首行闸口缺失 / `up` 的 block 循环未在 `break` 之后跳过假值项 / `down` 内出现条件判断 / `excited` 比较符未改或旧写法残留 / 未获批准就改了 OI-3 的分支 / 测试仍含旧设计 token 或依赖 `matchedWhen` 这个私有名 / 缺少条件或激励覆盖 / 某 `it` 前无注释 / 用例少于 36 条 / SUMMARY 缺失</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.stdout : '')+(out.stderr ? 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);const legacy=text.split(/\r?\n/).filter(l=>/packages[\\/]system[\\/]src[\\/]hotkey[\\/](hotkey|keyboard)\.ts/.test(l));if(legacy.length!==2)bad.push('expected exactly 2 known-baseline legacy diagnostics in hotkey.ts/keyboard.ts, got '+legacy.length+' - vue-tsc output is not the known baseline (false green?)');if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hotkey implementation and tests type-check clean (vue-tsc exit '+out.status+'); 2 pre-existing legacy errors are out of scope')"</automated>
<fails_when>non-zero exit,或 stderr 含 "TYPECHECK FAILED":`vue-tsc` 没能正常跑完(`out.error` / `out.status === null` / 被信号终止)、非零退出且无任何输出、本计划文件(`command.ts` / `system.ts` / `index.ts` / `__test__/**`)存在类型错误,或 `hotkey.ts` / `keyboard.ts` 的 legacy 诊断数不等于 2(输出不是已知基线)</fails_when>
@ -456,7 +458,7 @@ Phase 9 · 第 24 个计划 —— **修正用户重写后按键系统(`comman
| T-09-24-02 | Tampering | `matchKeyUp` 的辅助键 / 常规键拆分 | medium | mitigate | 解构顺序由 `matchArray` 的 `[match, unmatch]` 契约决定,改为 `[auxiliary, main]`;用例覆盖四条 KeyUp 规则与 203;结构闸门同时断言新写法存在、旧写法不存在 |
| T-09-24-03 | Denial of service | 无参 `leaveScope` 误发 205 后继续执行 | medium | mitigate | 无参分支 `pop` + 两次 `clear` 后立即 `return;`;用例断言「无参退出不告警、只弹一级」,空栈仍 205;结构闸门断言 `pop()` 后 160 字符内出现 `return;` |
| T-09-24-04 | Tampering | 权威设计与旧实现被误改 | high | mitigate | `type.ts` / `hotkey.ts` / `keyboard.ts` / `keyCodes.ts` / `index.ts` / `logger.json` / 依赖文件 / lockfile 全部由 `git status --porcelain` 零改动闸门守护;范围闸门由 `files_modified` 派生白名单 |
| T-09-24-05 | Repudiation | 时序判定被改错或超范围改动 | medium | mitigate | 只改 `dt < timeout` → `dt >= timeout` 一个比较符;`FINAL GATE` 同时断言新写法存在、旧写法不存在、OI-3 的 `Continuous` 分支未被改动;用例锁定 99 / 100 / 150 的触发次数 |
| T-09-24-05 | Repudiation | 时序判定被改错或超范围改动 | medium | mitigate | 只改 `dt < timeout` → `dt >= timeout` 一个比较符;`FINAL GATE` 同时断言新写法存在、旧写法不存在,并以**分支形态**守护 OI-3 的 `Continuous` 分支(要求 `if (pending.emitted === 1) {` 与 `if (dt2 >= command.dt)` 存在、禁止 `emitted > 1`);用例锁定 99 / 100 / 150 的触发次数 |
| T-09-24-06 | Information disclosure | 无网络 / 无存储 / 无敏感日志 | low | accept | 系统只在内存中保存按键状态与指令 id;`logger.warn` 只输出指令 id;本计划不改 `logger.json` |
| T-09-24-SC | Tampering | 依赖安装(npm/pip/cargo) | high | accept | 本计划**不执行任何安装**、不改 `package.json` / lockfile;`LEGACY GATE` 对两者做零改动断言,故无供应链面 |
</threat_model>
@ -465,7 +467,7 @@ Phase 9 · 第 24 个计划 —— **修正用户重写后按键系统(`comman
- **Task 0**:`PRESTATE` 闸门通过(5 处缺陷仍在、旧测试仍是旧设计、warn 201–207 齐全);用户明确回复「可以执行」(含 OI-1 命名与 OI-3 处置的裁定)。
- **Task 1**:`system.test.ts` 全绿;eslint;`TRACER GATE`(`matchKeyUp` 解构正确且旧写法消失、无参 `leaveScope` 有 `return`、测试无旧设计 token、只从 `../system` 导入、未提前写激励用例、`it` 前注释、≥18 条);`LEGACY GATE`。
- **Task 2**:两个测试文件全绿;eslint;`COMMAND GATE`(`setKeys` 只拦负数、207 两分支保留、测试无旧设计 token、含 `setKeys(0)` 与 203 断言、`it` 前注释、≥12 条);`LEGACY GATE`。
- **Task 3**:两个测试文件全绿(`system.test.ts` ≥ 36 条);eslint;`FINAL GATE`(`matchedWhen` 成员存在、`condition?.()` 恰好 3 处、`emit` 首行闸口、`up` 的 block 循环在 `break` 之后跳过假值项、`down` 内无 `condition`、`dt >= timeout`、OI-3 分支未改、测试无旧设计 token 且不依赖 `matchedWhen` 私有名、含 `when` / `excite` / `excitedBy` / `timeout` 覆盖、SUMMARY 存在);类型闸门(`vue-tsc --noEmit`,先校验 `spawnSync` 的 `out.error` / `out.status` / `out.signal`;本计划文件 0 条诊断;`hotkey.ts` / `keyboard.ts` 恰 2 条 legacy 作为输出完整的基线证据);`SCOPE GATE`(白名单由 `files_modified` 派生)。
- **Task 3**:两个测试文件全绿(`system.test.ts` ≥ 36 条);eslint;`FINAL GATE`(`matchedWhen` 成员存在、`condition?.()` 恰好 3 处、`emit` 首行闸口、`up` 的 block 循环在 `break` 之后跳过假值项、`down` 内无 `condition`、`dt >= timeout`、OI-3 分支形态与基线一致(要求 `if (pending.emitted === 1) {` 与 `if (dt2 >= command.dt)` 存在、禁止 `emitted > 1`)、测试无旧设计 token 且不依赖 `matchedWhen` 私有名、含 `when` / `excite` / `excitedBy` / `timeout` 覆盖、SUMMARY 存在);类型闸门(`vue-tsc --noEmit`,先校验 `spawnSync` 的 `out.error` / `out.status` / `out.signal`;本计划文件 0 条诊断;`hotkey.ts` / `keyboard.ts` 恰 2 条 legacy 作为输出完整的基线证据);`SCOPE GATE`(白名单由 `files_modified` 派生)。
- **范围诚实**:不跑 `build:game` / `build:packages`,不修 `hotkey.ts` / `keyboard.ts` 的 2 条 legacy 类型错误,不接线 `packages-user/**`,不做浏览器 E2E;SUMMARY 须如实记录这些边界,不得表述为「浏览器端已验证」;**`estimate.tokens=70000` 是未校准估算(`confidence: low`),SUMMARY 须显式声明**。
- **既有注释**:`system.ts` / `command.ts` / `type.ts` / `hotkey.ts` / `keyboard.ts` / `keyCodes.ts` 的既有注释与 jsDoc 零修改(`matchKeyUp` 的「抬起按键仅允许一个非辅助按键」在修正后依然成立)。
</verification>