fix(event): 采纳七个内建并修正 touched-jsdoc 门禁

- event.test.ts 与 03-EVENT-CONTRACT.md 同步为七个内建(移除 touchFront/EventTouchFront)

- check-touched-jsdoc 按 dev.md 豁免继承/implements 成员,补充防误删说明

- 刷新 Phase 01-03 验证指纹,三个阶段恢复 passed
This commit is contained in:
unanmed 2026-09-13 23:38:46 +08:00
parent a55c2e32a2
commit c80516deb7
6 changed files with 106 additions and 53 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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[]` 直接执行、缺失目标安全

View File

@ -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:

View File

@ -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'
];

View File

@ -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);
}