diff --git a/.planning/WINDOWS.md b/.planning/WINDOWS.md index 32272b6..5397c57 100644 --- a/.planning/WINDOWS.md +++ b/.planning/WINDOWS.md @@ -1,10 +1,10 @@ --- schema_version: 1 -open_count: 9 +open_count: 10 waived_count: 0 fixed_count: 8 -total_count: 17 -last_updated: 2026-09-11T06:11:49.370Z +total_count: 18 +last_updated: 2026-09-11T06:27:22.412Z --- # Broken Windows Ledger @@ -32,6 +32,7 @@ last_updated: 2026-09-11T06:11:49.370Z | 15 | 03 | deviation | packages-user/data-common/src/replay/array.ts | | Repaired replay parameter encoding so diagnostic params remain original and deterministic. | open | | 2026-09-10T09:18:58.279Z | | | 16 | 03 | deviation | packages-user/data-state/test/replayVerifier.ts | | Added a package-local verifier harness shared by Vitest and the Node runner to avoid composite-script import resolution. | open | | 2026-09-10T09:18:58.946Z | | | 17 | 03 | deviation | packages-user/data-state/test/fixtures/closed-loop.ts | | Applied repository Prettier/CRLF formatting to the fixed replay fixture and runner files. | open | | 2026-09-10T09:18:59.589Z | | +| 18 | 03 | deviation | script/check-data-circular.ts | | Plan 03-15 scoped circular gate reports seven pre-existing legacy/render boundary cycles through data-state/src/legacy/move.ts; replay command class changes do not touch those imports. | open | | 2026-09-11T06:27:22.412Z | | ````json [ @@ -238,6 +239,18 @@ last_updated: 2026-09-11T06:11:49.370Z "reason": "", "recorded_at": "2026-09-10T09:18:59.589Z", "resolved_at": null + }, + { + "id": 18, + "kind": "deviation", + "phase": "03", + "file": "script/check-data-circular.ts", + "line": null, + "description": "Plan 03-15 scoped circular gate reports seven pre-existing legacy/render boundary cycles through data-state/src/legacy/move.ts; replay command class changes do not touch those imports.", + "status": "open", + "reason": "", + "recorded_at": "2026-09-11T06:27:22.412Z", + "resolved_at": null } ] ```` diff --git a/.planning/phases/03-data-completion/deferred-items.md b/.planning/phases/03-data-completion/deferred-items.md index d36b450..19314e3 100644 --- a/.planning/phases/03-data-completion/deferred-items.md +++ b/.planning/phases/03-data-completion/deferred-items.md @@ -1,3 +1,4 @@ # Deferred Items — Phase 03 - `pnpm check:type` remains blocked by pre-existing render/legacy diagnostics outside Plan 03-06. The Tile events contract diagnostics from the earlier baseline were resolved by Plan 03-06. +- Plan 03-15's scoped circular gate reports seven pre-existing legacy/render boundary cycles through `data-state/src/legacy/move.ts`; the independent replay command class changes do not touch those imports. diff --git a/packages-user/data-state/src/replay/commands.test.ts b/packages-user/data-state/src/replay/commands.test.ts index effce51..058f4df 100644 --- a/packages-user/data-state/src/replay/commands.test.ts +++ b/packages-user/data-state/src/replay/commands.test.ts @@ -48,6 +48,16 @@ describe('replay commands', () => { expect(items.map(item => item.code)).toEqual(REPLAY_COMMAND_ORDER); expect(items).toHaveLength(8); expect(items.map(item => item.command.execute)).toHaveLength(8); + expect(items.map(item => item.command.constructor.name)).toEqual([ + 'ReplayUpCommand', + 'ReplayRightCommand', + 'ReplayDownCommand', + 'ReplayLeftCommand', + 'ReplayAutoPathfindCommand', + 'ReplayUseItemCommand', + 'ReplayEquipCommand', + 'ReplayUnequipCommand' + ]); }); // 验证顶层注册器按稳定顺序注册并拒绝重复 code @@ -72,18 +82,18 @@ describe('replay commands', () => { it('assembles an independent top-level registry for every CoreState', () => { const first = createCoreState(); const second = createCoreState(); + const firstCommands = REPLAY_COMMAND_ORDER.map( + code => first.replaySystem.getCommand(code)! + ); + const secondCommands = REPLAY_COMMAND_ORDER.map( + code => second.replaySystem.getCommand(code)! + ); expect(first.replaySystem).not.toBe(second.replaySystem); expect(first.pathfinding).not.toBe(second.pathfinding); - expect( - REPLAY_COMMAND_ORDER.map(code => - first.replaySystem.getCommand(code) - ).length - ).toBe(8); - expect( - REPLAY_COMMAND_ORDER.map(code => - second.replaySystem.getCommand(code) - ).length - ).toBe(8); + expect(firstCommands).toHaveLength(8); + expect(secondCommands).toHaveLength(8); + expect(new Set(firstCommands).size).toBe(8); + expect(new Set(secondCommands).size).toBe(8); expect( REPLAY_COMMAND_ORDER.every( code => first.replaySystem.getCommand(code) !== null @@ -95,6 +105,32 @@ describe('replay commands', () => { ) ).toBe(true); expect(first.replaySystem.route).not.toBe(second.replaySystem.route); + for (let index = 0; index < firstCommands.length; index++) { + expect(firstCommands[index]).not.toBe(secondCommands[index]); + } + }); + + // 验证现有 command item 扩展边界可以替换单个实现而不改变注册器 + it('registers an existing custom command item through the current interface', () => { + const state = createCoreState(); + const replay = new ReplaySystem(); + const defaultItems = createReplayCommandItems(state); + const customItem: IReplayCommandItem = { + code: ReplayCommandCode.Up, + command: { + execute: () => Promise.resolve(true) + } + }; + registerReplayCommandItems(replay, [ + customItem, + ...defaultItems.slice(1) + ]); + expect(replay.getCommand(ReplayCommandCode.Up)).toBe( + customItem.command + ); + expect(replay.getCommand(ReplayCommandCode.Right)).toBe( + defaultItems[1].command + ); }); // 验证四向移动不等待 controller.onEnd 即完成 command @@ -390,4 +426,38 @@ describe('replay safety decorators', () => { ); expect(source).not.toContain('shouldReplay'); }); + + // 验证八个 command 类各自拥有 execute 且不存在共享入口或跨类调用 + it('keeps replay command ownership isolated in the command module', () => { + const source = readFileSync( + new URL('./commands.ts', import.meta.url), + 'utf8' + ); + const classes = [ + 'ReplayUpCommand', + 'ReplayRightCommand', + 'ReplayDownCommand', + 'ReplayLeftCommand', + 'ReplayAutoPathfindCommand', + 'ReplayUseItemCommand', + 'ReplayEquipCommand', + 'ReplayUnequipCommand' + ]; + expect(source).not.toContain('ReplayCommandEntrances'); + expect(source).not.toContain('createMoveCommand'); + expect(source).not.toMatch(/\bentries\./); + for (const className of classes) { + const body = source.match( + new RegExp( + `class\\s+${className}\\b[\\s\\S]*?(?=\\r?\\nclass\\s|\\r?\\nfunction\\s|\\r?\\nexport function\\s|\\r?\\n/\\*\\*/)` + ) + )?.[0]; + expect(body).toBeDefined(); + expect(body).toMatch(/execute\s*\(/); + for (const otherClass of classes) { + if (otherClass === className) continue; + expect(body).not.toContain(otherClass); + } + } + }); });