diff --git a/.planning/phases/01-event/01-VERIFICATION.md b/.planning/phases/01-event/01-VERIFICATION.md index 41c61e2..cba8f62 100644 --- a/.planning/phases/01-event/01-VERIFICATION.md +++ b/.planning/phases/01-event/01-VERIFICATION.md @@ -32,6 +32,8 @@ covered_files: - .planning/phases/01-event/01-11-SUMMARY.md - .planning/phases/01-event/01-12-PLAN.md - .planning/phases/01-event/01-12-SUMMARY.md + - .planning/phases/01-event/01-13-PLAN.md + - .planning/phases/01-event/01-13-SUMMARY.md - .planning/phases/01-event/01-CONTEXT.md - .planning/phases/01-event/01-DISCUSSION-LOG.md - .planning/phases/01-event/01-PATTERNS.md @@ -69,7 +71,7 @@ covered_files: - packages-user/data-system/src/index.ts - packages-user/data-system/src/types.ts - packages/common/src/logger.json -covered_digest: "v1:sha256:c66c50e1eb272fd25b750aaca1bc4cecb4fb6c8875e964b74a3bfed5ddf693d3" +covered_digest: "v1:sha256:e4fdc082d2d9a1d4f2815d26a688594bcf08775c8b36944e49be1eea3e95200d" behavior_unverified: 0 overrides_applied: 0 re_verification: diff --git a/.planning/phases/02-pathfinding/02-VERIFICATION.md b/.planning/phases/02-pathfinding/02-VERIFICATION.md index baa383e..7ea4ce7 100644 --- a/.planning/phases/02-pathfinding/02-VERIFICATION.md +++ b/.planning/phases/02-pathfinding/02-VERIFICATION.md @@ -48,7 +48,7 @@ covered_files: - packages-user/data-system/src/path/system.test.ts - packages-user/data-system/src/path/system.ts - packages-user/data-system/src/path/types.ts -covered_digest: "v1:sha256:9a240d6d07ec1d218c07f9e39ad36d4e2cf57344dd575db44c8063fac61f3945" +covered_digest: "v1:sha256:31fed56c7a2c4fc2a89c490c106ac8dbda5857da92531929e3ba3e85570b215c" behavior_unverified: 0 overrides_applied: 0 re_verification: diff --git a/.planning/phases/03-data-completion/03-EVENT-CONTRACT.md b/.planning/phases/03-data-completion/03-EVENT-CONTRACT.md index 31ac39e..6fe6135 100644 --- a/.planning/phases/03-data-completion/03-EVENT-CONTRACT.md +++ b/.planning/phases/03-data-completion/03-EVENT-CONTRACT.md @@ -1,6 +1,6 @@ # Phase 3 Event Built-in Contract -本文件记录八个 event built-ins 的稳定契约。范围只包含批准的八个 built-ins;不加入 +本文件记录七个 event built-ins 的稳定契约。范围只包含批准的七个 built-ins;不加入 完整 legacy 事件目录,不保留未决字段,也不把渲染行为放入数据端函数。 > **实现同步:** 内建函数为类式 `BuiltInFunction` 实现,函数名使用短名;触发来源收集 @@ -41,7 +41,7 @@ - `hit` 使用 `handler.nextLoc`,trigger 为 `OnTouch`。 调用前移动器已将 `state.hero.location` 置为对应位置,因此 `heroLocator` 取自状态而非 -handler 参数。`touchFront` 只负责让勇士向前一步;前方的撞击判定与 `OnTouch` 由移动器 +handler 参数。`stepHero` 只负责让勇士向前一步;前方的撞击判定与 `OnTouch` 由移动器 的 hit 路径产生。 ## Parameter contracts @@ -111,16 +111,6 @@ interface IStepHeroEventParam {} 不读取额外字段,使用勇士当前移动方向执行一次 `forward(1)`。移动器缺失或控制器缺失、 移动已在进行中时安全返回 `void`,启动成功后等待 `onEnd`。 -### `touchFront` — `hero.ts` 的 `EventTouchFront` - -```ts -interface ITouchFrontEventParam {} -``` - -不读取额外字段,让勇士向前一步(`mover.forward()` + `start()` + 等待 `onEnd`)。前方可 -通行则正常前进;不可通行时移动器会走撞击路径并触发前方 `OnTouch`。取不到事件图层或 -移动器、移动已在进行中时安全返回 `void`。 - ### `insertEvents` — `event.ts` 的 `EventInsertEvents` ```ts @@ -146,9 +136,9 @@ type IInsertEventEventParam = Statement[]; ## Registration `data-state/src/event/registrations.ts` 只导出一个函数 -`createEventRegistrations()`,按稳定顺序直接构造八个类的实例并返回 +`createEventRegistrations()`,按稳定顺序直接构造七个类的实例并返回 `BuiltInFunction[]`;不做分类包装,不引入工厂或描述符数组。`CoreState` 在创建 -`GameEventSystem` 时导入该函数并传入注册项集合。除这八项外,本阶段不注册任何 legacy +`GameEventSystem` 时导入该函数并传入注册项集合。除这七项外,本阶段不注册任何 legacy built-in。 稳定顺序与所有权: @@ -158,9 +148,8 @@ built-in。 3. `removeBlock` — `map.ts` 的 `EventRemoveBlock` 4. `moveHero` — `hero.ts` 的 `EventMoveHero` 5. `stepHero` — `hero.ts` 的 `EventStepHero` -6. `touchFront` — `hero.ts` 的 `EventTouchFront` -7. `insertEvents` — `event.ts` 的 `EventInsertEvents` -8. `insertEvent` — `event.ts` 的 `EventInsertEvent` +6. `insertEvents` — `event.ts` 的 `EventInsertEvents` +7. `insertEvent` — `event.ts` 的 `EventInsertEvent` `event/index.ts` 与 `data-state/src/index.ts` 保持 `export`-only,不成为第二装配者; `event/utils.ts` 不参与导出。事件语义、等待语义、`Statement[]` 直接执行、缺失目标安全 diff --git a/.planning/phases/03-data-completion/03-VERIFICATION.md b/.planning/phases/03-data-completion/03-VERIFICATION.md index 542508f..66a87c1 100644 --- a/.planning/phases/03-data-completion/03-VERIFICATION.md +++ b/.planning/phases/03-data-completion/03-VERIFICATION.md @@ -103,7 +103,7 @@ covered_files: - script/check-data-type.ts - script/check-touched-jsdoc.ts - script/test-data-node.ts -covered_digest: "v1:sha256:eaaeb8cdc7d622537838d7ec2474f51d2ca0ec04c1b2606d7e0cb397b4cfd490" +covered_digest: "v1:sha256:f9f65e02f16130c47027d5af2fb93f5cb27e67f28e25534c8d24d4f3f1259ac4" behavior_unverified: 0 overrides_applied: 0 re_verification: diff --git a/packages-user/data-state/src/event/event.test.ts b/packages-user/data-state/src/event/event.test.ts index 4b77d8f..0370c51 100644 --- a/packages-user/data-state/src/event/event.test.ts +++ b/packages-user/data-state/src/event/event.test.ts @@ -183,13 +183,6 @@ describe('event built-ins', () => { expect(fixture.state.hero.location.x).toBe(2); }); - // 验证真实注册项让勇士向前一步,前方撞击判定由移动器负责 - it('steps the hero forward through touchFront', async () => { - const fixture = createFixture(); - await invokeBuiltin(getRegistration('touchFront'), {}, fixture.env); - expect(fixture.state.hero.location.x).toBe(1); - }); - // 验证真实注册项按顺序等待临时事件序列并直接执行语句体 it('awaits id sequences and executes a direct statement body', async () => { const fixture = createFixture(); @@ -227,7 +220,7 @@ describe('event built-ins', () => { expect(fixture.state.eventStore.getEvent('inline-body')).toBeNull(); }); - // 验证默认注册项只包含批准的八个稳定名称 + // 验证默认注册项只包含批准的七个稳定名称 it('registers exactly the approved built-ins in AnonTokyo', () => { const fixture = createFixture(); const names = createEventRegistrations().map(item => item.name); @@ -237,7 +230,6 @@ describe('event built-ins', () => { 'removeBlock', 'moveHero', 'stepHero', - 'touchFront', 'insertEvents', 'insertEvent' ]); @@ -276,9 +268,6 @@ describe('event built-ins', () => { missingEnv ) ).resolves.toBeUndefined(); - await expect( - invokeBuiltin(getRegistration('touchFront'), {}, missingEnv) - ).resolves.toBeUndefined(); await expect( invokeBuiltin(getRegistration('insertEvent'), [], fixture.env) ).resolves.toBeUndefined(); @@ -286,17 +275,16 @@ describe('event built-ins', () => { }); describe('event registration ownership', () => { - // 验证单一注册函数按地图勇士控制三类顺序返回全部八个新鲜实例 - it('returns the eight fresh registrations in stable category order', () => { + // 验证单一注册函数按地图勇士控制三类顺序返回全部七个新鲜实例 + it('returns the seven fresh registrations in stable category order', () => { const registrations = createEventRegistrations(); - expect(registrations).toHaveLength(8); + expect(registrations).toHaveLength(7); expect(registrations.map(item => item.name)).toEqual([ 'setBlock', 'moveBlock', 'removeBlock', 'moveHero', 'stepHero', - 'touchFront', 'insertEvents', 'insertEvent' ]); @@ -348,7 +336,6 @@ describe('event registration ownership', () => { 'EventRemoveBlock', 'EventMoveHero', 'EventStepHero', - 'EventTouchFront', 'EventInsertEvents', 'EventInsertEvent' ]; diff --git a/script/check-touched-jsdoc.ts b/script/check-touched-jsdoc.ts index 287991a..6bbf50a 100644 --- a/script/check-touched-jsdoc.ts +++ b/script/check-touched-jsdoc.ts @@ -1,3 +1,18 @@ +// 门禁:检查「显式传入的文件」中的非构造器声明是否带多行 JSDoc。 +// +// 背景:本脚本由 Phase 03 Plan 03-19 引入,只用于校验本次修正触及的 replay/event +// 文件,不是仓库级 lint,也未接入 CI / pre-commit,仅由 verify 命令按文件名显式调用。 +// +// 检查范围: +// - 只盘点顶层 function 声明与类的方法 / getter / setter。 +// - 不盘点 interface 成员、类型别名、枚举、类属性、对象字面量及箭头函数成员。 +// - 判定只看声明前是否存在多行 /** ... */(开符号单独占行、结束符号单独占行)。 +// +// 重要:本脚本不读取注释内容,也不判断注释是否正确、过时或重复。MISSING 只表示 +// 「缺少多行 JSDoc」这一格式问题,与已有注释的内容无关。修复 MISSING 时不得删除、 +// 改写或降级任何已有注释:应补一段多行 JSDoc;若确属继承 / implements 而来的 API, +// 则依赖 hasBaseMember 的豁免规则(对应 dev.md「继承或 implements 而来的 API 不应 +// 重复添加 jsDoc」)。 import { readFileSync } from 'node:fs'; import { relative, resolve } from 'node:path'; import ts from 'typescript'; @@ -27,6 +42,22 @@ function hasMultilineJsDoc(node: ts.Node, source: ts.SourceFile): boolean { return /\r?\n\s*\*\/$/.test(comment); } +// dev.md:继承或 implements 而来的 API 在实现处不应重复添加 JSDoc, +// 其说明由源头负责;外部库(如 @motajs/anon-tokyo)的源头不在本仓库检查范围内 +function hasBaseMember( + classDecl: ts.ClassDeclaration, + memberName: string, + checker: ts.TypeChecker +): boolean { + for (const clause of classDecl.heritageClauses ?? []) { + for (const typeNode of clause.types) { + const baseType = checker.getTypeAtLocation(typeNode); + if (baseType.getProperty(memberName)) return true; + } + } + return false; +} + function createEntry( file: string, symbol: string, @@ -50,7 +81,8 @@ function createEntry( function collectEntries( file: string, - source: ts.SourceFile + source: ts.SourceFile, + checker: ts.TypeChecker | null ): IInventoryEntry[] { const entries: IInventoryEntry[] = []; for (const statement of source.statements) { @@ -88,14 +120,18 @@ function collectEntries( ts.isGetAccessorDeclaration(member) || ts.isSetAccessorDeclaration(member) ) { + const name = member.name.getText(source); + const inherited = checker + ? hasBaseMember(statement, name, checker) + : false; entries.push( createEntry( file, - `${owner}.${member.name.getText(source)}`, + `${owner}.${name}`, 'method', member, source, - false + inherited ) ); } @@ -104,16 +140,45 @@ function collectEntries( return entries; } -function readEntries(file: string): IInventoryEntry[] { - const text = readFileSync(file, 'utf8'); - const source = ts.createSourceFile( - file, - text, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TS - ); - return collectEntries(toDisplayPath(file), source); +function createCheckedProgram(files: readonly string[]): ts.Program | null { + try { + const configPath = ts.findConfigFile( + process.cwd(), + ts.sys.fileExists, + 'tsconfig.json' + ); + if (!configPath) return null; + const config = ts.readConfigFile(configPath, ts.sys.readFile); + if (config.error) return null; + const parsed = ts.parseJsonConfigFileContent( + config.config, + ts.sys, + process.cwd() + ); + return ts.createProgram( + files.map(file => resolve(file)), + parsed.options + ); + } catch { + return null; + } +} + +function readEntries( + file: string, + program: ts.Program | null, + checker: ts.TypeChecker | null +): IInventoryEntry[] { + const source = + program?.getSourceFile(resolve(file)) ?? + ts.createSourceFile( + file, + readFileSync(file, 'utf8'), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); + return collectEntries(toDisplayPath(file), source, checker); } function report(entries: readonly IInventoryEntry[]): number { @@ -132,7 +197,7 @@ function report(entries: readonly IInventoryEntry[]): number { const constructors = entries.filter(entry => entry.exempt); if (constructors.length > 0) { console.log( - `Constructors explicitly exempt from JSDoc: ${constructors + `Declarations exempt from JSDoc (constructor or inherited): ${constructors .map(entry => entry.symbol) .join(', ')}` ); @@ -150,11 +215,14 @@ function report(entries: readonly IInventoryEntry[]): number { ` ${violation.file}:${violation.line} ${violation.symbol} [${violation.kind}]` ); } + console.error( + ' 注意:MISSING 仅表示缺少多行 JSDoc,与注释内容无关;修复时不得删除或改写已有注释' + ); return 1; } console.log( - 'check-touched-jsdoc passed: every non-constructor declaration has multiline JSDoc' + 'check-touched-jsdoc passed: every non-exempt declaration has multiline JSDoc' ); return 0; } @@ -167,7 +235,14 @@ function main(): void { ); process.exit(1); } - const entries = files.flatMap(readEntries); + const program = createCheckedProgram(files); + const checker = program?.getTypeChecker() ?? null; + if (!checker) { + console.error( + 'check-touched-jsdoc warning: TypeScript program unavailable — inherited members cannot be detected' + ); + } + const entries = files.flatMap(file => readEntries(file, program, checker)); if (report(entries) !== 0) process.exit(1); }