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:
AncTe committed 2026-09-13 23:38:46 +08:00
1 parent a55c2e32a2
commit c80516deb7
6 files changed
+106 -53

No files matched your search

+91 -16
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);
}