diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f7178ce..5df039e 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -210,7 +210,7 @@ Plans: 3. 测试在本地可运行且全部通过 4. 测试由 AI 编写并运行,通过验证后可提交 -**Plans**: 9/9 原计划 executed replanned (D-28;旧 06-01/06-02 执行结果标记 superseded,按同号重跑;数据端切片,非数据 render/legacy 覆盖延后) + gap-fill 06-10..06-15 (D-46;人工评审缺口补测,只补测试不改生产代码;15/15 executed) +**Plans**: 9/9 原计划 executed replanned (D-28;旧 06-01/06-02 执行结果标记 superseded,按同号重跑;数据端切片,非数据 render/legacy 覆盖延后) + gap-fill 06-10..06-15 (D-46;人工评审缺口补测,只补测试不改生产代码;15/15 executed) + perf 06-16 (性能测试补充;只加测试与配置,不改生产代码) Plans: @@ -254,6 +254,10 @@ Plans: - [x] 06-15-PLAN.md — combat interface gap: G-06-01-D (`EnemyContext.deleteAura` normal case — `addAura` applies `atk 2→5` → `deleteAura` same instance → `buildup` expects `2`); blocked `#06-15-1` (same root cause as `#06-01-4`) as correct-expectation `it.skip`, pending user decision +**Wave 6** *(performance-test supplement, user-authorized; appended after 06-15; isolated `test:perf` lane + new `*.perf.ts` files, no production edits, no new dependencies)* + +- [ ] 06-16-PLAN.md — perf supplement: `vitest.perf.config.ts` + `test:perf` script (isolated from `pnpm test:ci`); 18 cases = ② critical calc (`1000/10000/50000`) + ① enemy-context `buildup` (N = `50/200/1000`) + ③ hero attribute recalc (M = `10/100/1000`) + ④ CoreState save/load round trip (`10/100/1000` items × `NoCompression`/`LowCompression`/`HighCompression`); warmup 3 + 20 samples → median/min/p95 via `console.table`, **zero assertions**, results recorded in `06-16-SUMMARY.md` + ### Phase 7: 数据端缺陷修复 **Goal**: 修复 Phase 6 单元测试暴露的数据端疑似缺陷,使正确预期用例转绿,且仅限数据端、不涉及渲染端 diff --git a/.planning/phases/06-unit-tests/06-16-PLAN.md b/.planning/phases/06-unit-tests/06-16-PLAN.md new file mode 100644 index 0000000..a01773f --- /dev/null +++ b/.planning/phases/06-unit-tests/06-16-PLAN.md @@ -0,0 +1,322 @@ +--- +phase: 06-unit-tests +plan: 16 +type: execute +wave: 6 +depends_on: [] +files_modified: + - vitest.perf.config.ts + - package.json + - packages-user/data-system/src/combat/damage.perf.ts + - packages-user/data-system/src/combat/context.perf.ts + - packages-user/data-base/src/hero/attribute.perf.ts + - packages-user/data-state/test/saveables.perf.ts +autonomous: true +requirements: [TEST-01] +estimate: + tokens: 45000 + raw_tokens: 45000 + tasks: 3 + confidence: low +must_haves: + truths: + - "TEST-01 的**性能测试补充**:新增 4 个 `*.perf.ts` 与独立 `test:perf` 通道;`pnpm test:perf` 在本机跑通并打印 `console.table`;既有 `pnpm test:ci` 行为完全不变(D-08 门禁不变)。" + - "收集隔离:`vitest.perf.config.ts` 的 `test.include` 仅为 `['**/*.perf.ts']`;vitest 4.0.18 的默认 include 是 `**/*.{test,spec}.?(c|m)[jt]s?(x)`(实测 `node_modules/vitest/dist/config.cjs` 的 `defaultInclude`),`*.perf.ts` 不匹配,故 `pnpm test:ci` 不收集 perf 文件;`package.json` 的 `test`/`test:ci` 一字不改。" + - "测量口径(用户裁决,4 个文件完全一致):每个 case 先**预热 3 次不计时**,再**采样 20 次取中位数**;`console.table` 列为 `case`、`scale`、`median ms`、`min ms`、`p95 ms`;**不做任何断言**(只记录,耗时永不导致用例失败);计时一律 `globalThis.performance.now()`,不使用 fake timers。" + - "① 怪物上下文构建(`context.perf.ts`):`EnemyContext` + `bindHero` + 注册光环转换器 + 固定 `10` 个全局光环与 `5` 个特殊属性代码,放置 N 个怪 → 单次 `buildup()` 计时;N = `50 / 200 / 1000`。" + - "② 临界计算(`damage.perf.ts`):`DamageContext.calculateCritical(view, 'atk')` 在固定 atk(`0`)与固定伤害区间(`100 → 0`,步长 `10`,临界上界 `10`)上重复计算;次数 = `1000 / 10000 / 50000`。" + - "③ 勇士属性计算(`attribute.perf.ts`):`HeroAttribute` 挂 M 个修饰器分布到 `10` 个数值属性,单次采样重复 `1000` 次「重算 + 读取 10 个最终属性」;M = `10 / 100 / 1000`。`recalculateAttribute` 是 private,故经公开 `markModifierDirty` 触发同一重算路径,读取用 `getFinalAttribute`。" + - "④ 存读档(`saveables.perf.ts`):`CoreState.saveState`/`loadState` 对 **5 个已注册 saveable** 整体往返(`@system/hero`/`@system/flags`/`@system/maps`/`@system/enemy`/`@system/replay`,D-45);装备/物品件数 = `10 / 100 / 1000` × 压缩档 `NoCompression`/`LowCompression`/`HighCompression`(共 9 个 case)。" + - "无生产代码改动、不新增任何依赖、不改 06-01..06-15 计划与既有 `*.test.ts`;全部 fixture/helper **文件内联复用**各文件既有模式,不建跨文件共享 fixture(D-02/D-03)。" + - "结果记录:`pnpm test:perf` 的数值表(18 个 case)写入 `06-16-SUMMARY.md`;**不落盘基线 JSON**、不新增耗时门禁(用户裁决)。" + - "质量门禁(D-44,文件级):改动文件 `eslint --fix` 后 `eslint <改动文件>` 0 错误;4 个 perf 文件在 `pnpm exec vue-tsc --noEmit` 输出中按文件路径过滤 0 类型错误;`pnpm test:ci` 全绿。" + - "阶段化(D-43):本计划按 阶段 1(构件:通道骨架 + ② 临界计算)→ 阶段 2(组合:① 光环流水线 + ③ 修饰器×属性)→ 阶段 3(完整:④ CoreState 整体往返)执行,每阶段跑绿后才进入下一阶段;出现阻断性问题按 D-05/D-07 暂停汇报、等确认,不得跳过或弱化。" + - "执行节奏(D-33):本计划执行前先向用户确认;执行完成后暂停,用中文分条简要汇报耗时结果与异常(如耗时逼近 `testTimeout`)。" + artifacts: + - path: vitest.perf.config.ts + provides: "独立 perf 通道(`test.include` 仅为 `*.perf.ts`、`testTimeout`/`hookTimeout` 30000、与 `vite.config.ts` 一致的 `@motajs/*`/`@user/*` alias);不改动既有 `vite.config.ts`" + - path: package.json + provides: "新增 `test:perf` 脚本(`vitest run --config vitest.perf.config.ts`);`test`/`test:ci` 不变" + - path: packages-user/data-system/src/combat/damage.perf.ts + provides: "② 临界计算耗时表(3 个 case:1000 / 10000 / 50000 次完整临界枚举)" + - path: packages-user/data-system/src/combat/context.perf.ts + provides: "① 怪物上下文构建耗时表(3 个 case:N = 50 / 200 / 1000 的单次 `buildup()`)" + - path: packages-user/data-base/src/hero/attribute.perf.ts + provides: "③ 勇士属性计算耗时表(3 个 case:M = 10 / 100 / 1000 修饰器分布 10 属性,每样本 1000 次重算+读取)" + - path: packages-user/data-state/test/saveables.perf.ts + provides: "④ 存读档耗时表(9 个 case:件数 10/100/1000 × 三档压缩的 `CoreState` 整体往返)" + key_links: + - "alias 是硬前提:`node_modules` 下**没有** `@user/*` 与 `@motajs/*`(实测 `Test-Path node_modules/@user` 为假),bare specifier 只能靠 `resolve.alias` 解析;`tsconfig.json` 的 `paths` 只服务类型检查(`vue-tsc`),不服务 vitest 运行时。故 `vitest.perf.config.ts` 必须镜像 `vite.config.ts` L13–27 的两段 `glob.sync` alias 计算,否则 perf 文件及其传递依赖(`context.ts` → `@user/data-base`)无法解析。" + - "`buildup()` 早退(`context.ts:684`):`if (!this.needUpdate) return;` 且 `if (!this.bindedHero) { logger.warn(110); return; }`。因此①的每个样本必须先触发公开的 `needUpdate = true` —— 用 `context.bindHero(hero)`(`context.ts:193`,最省的公开触发器;fixture 不 attach `damageSystem`/`mapDamage`,故无额外副作用)。" + - "`calculateCritical`(`damage.ts:134`)先 `heroStatus.getModifiableClone()`,再对 `findNextCritical`(`damage.ts:206`)做二分(`hero.set` + `calculator.calculate`,`precision` 默认 12)。克隆语义保证**重复枚举不污染夹具**,每个样本结构一致。" + - "`HeroAttribute.recalculateAttribute` 是 private(`attribute.ts:80`),公开等价入口是 `markModifierDirty(modifier)`(`attribute.ts:235` → 对归属属性调 `recalculateAttribute`);`getFinalAttribute` 是公开只读入口。" + - "`HeroEquipsStore.add(item)`(`equipStore.ts:184`)依赖 `state.tileStore.num(item)` 与 `state.itemStore.getData(num)`,未注册则返回 `-1`;故必须先 `tileStore.addTile({...})` + `itemStore.addItem(raw)`(镜像 `equipStore.test.ts:100-111`)。`MapState.loadState` 在 Low/High 压缩且 `!compared` 时走 `logger.error(55)` 并直接返回(`mapState.ts:409`),故④的夹具要镜像 `saveablesRoundTrip.test.ts` 的 `maps.compareWith(...)` + `enemyManager.compareWith(...)` 基线。" + - "`tsconfig.json` 的 include 含 `packages-user/**/*.ts`,故 4 个 perf 文件纳入 `vue-tsc --noEmit` 的文件级门禁(D-44);`vitest.perf.config.ts` 不在任何 tsconfig project(`tsconfig.node.json` 只含 `./vite.config.ts`),其门禁 = `eslint` 0 错误 + 配置能被 vitest 实际加载(`pnpm test:perf` 成功即证明),本计划**不改 tsconfig**。" + prohibitions: + - "不创建或修改任何生产/核心源码(仅新增 `vitest.perf.config.ts`、4 个 `*.perf.ts`,并只改 `package.json` 的 scripts 一行)" + - "不安装任何依赖(vitest 4.0.18 已在 devDependencies),不安装覆盖率/基准工具(D-09)" + - "不改动 `package.json` 的 `test` / `test:ci` 脚本,不改动 `vite.config.ts`" + - "不新增或改动任何 warn/error 码断言,不新增 warn/error 码" + - "不在 perf 文件内写任何 `expect(...)`(不做耗时断言;用户裁决)" + - "不使用 fake timers、不引入异步等待(计时用真实 `globalThis.performance.now()`)" + - "不建立跨文件共享 fixture/factory/measure helper(D-02/D-03);每个 perf 文件内联自己的最小测量助手" + - "不修改 06-01..06-15 的 PLAN/SUMMARY、既有 `*.test.ts` 与 `06-COVERAGE-MAP.md`" + - "不落盘基线 JSON;不把耗时写入任何门禁脚本" +--- + + +按用户裁决为 Phase 6 追加**性能测试补充**:新增一条与 `pnpm test:ci` 完全隔离的 perf 测量通道(`vitest.perf.config.ts` + `test:perf` 脚本),并为数据端四个热点路径各建一个 `*.perf.ts` 测量文件(`context.perf.ts` / `damage.perf.ts` / `attribute.perf.ts` / `saveables.perf.ts`)。**只新增测试与配置,不改任何生产代码、不引入新依赖**。 + +四个测量项与固定三档规模(用户裁决): + +1. **怪物上下文构建**(`data-system/src/combat/context.perf.ts`):`EnemyContext` + `bindHero` + 注册光环转换器 + 固定 `10` 个全局光环、`5` 个特殊属性代码,放置 N 个怪 → 测**单次 `buildup()`** 耗时;N = `50 / 200 / 1000`(3 个 case)。 +2. **临界计算**(`data-system/src/combat/damage.perf.ts`):`DamageContext.calculateCritical(view, 'atk')` 在固定 atk(`0`)与固定伤害区间(`100 → 0`,步长 `10`)上**完整枚举**重复计算;次数 = `1000 / 10000 / 50000`(3 个 case)。 +3. **勇士属性计算**(`data-base/src/hero/attribute.perf.ts`):`HeroAttribute` 挂 M 个修饰器分布到 `10` 个数值属性,单次采样重复 `1000` 次「`markModifierDirty` 重算 + 读取 10 个 `getFinalAttribute`」;M = `10 / 100 / 1000`(3 个 case)。 +4. **存读档**(`data-state/test/saveables.perf.ts`):`CoreState.saveState`/`loadState` 对 5 个已注册 saveable 整体往返;装备/物品件数 = `10 / 100 / 1000` × 压缩档 `NoCompression`/`LowCompression`/`HighCompression`(9 个 case)。 + +合计 **18 个 case**,每个 case 预热 3 次(不计时)+ 采样 20 次取中位数,输出 `console.table`(列 `case`、`scale`、`median ms`、`min ms`、`p95 ms`);**不做任何断言**——耗时只记录,绝不让用例因慢而失败。 + +Purpose: Phase 6 的行为单测只证明「对不对」,不回答「随规模增长会不会塌」。这四条路径(光环全量重建、临界点二分枚举、修饰器重算、整体存读档)是数据端在真实魔塔规模下最可能劣化的位置,而它们此前**没有任何耗时数据**。本计划用最小的、与既有门禁完全隔离的通道把这四个数据点固定下来,供后续回归对比;同时因为 `include` 与默认 include 不重叠,`pnpm test:ci` 的语义、用时与结果一字不变。 + +Output: `vitest.perf.config.ts`、`package.json` 的 `test:perf` 脚本、4 个 `*.perf.ts`(18 个 case 的耗时表),以及承载数值表的 `06-16-SUMMARY.md`。 + +阶段结构(D-43):阶段 1 构件(通道骨架 + ② 临界计算,最小单元)→ 阶段 2 组合(① 光环流水线 + ③ 修饰器×属性矩阵)→ 阶段 3 完整(④ CoreState 整体存读档,含三档压缩)。每阶段跑绿后才进入下一阶段。 + + + +@C:/Users/book/.config/opencode/gsd-core/workflows/execute-plan.md +@C:/Users/book/.config/opencode/gsd-core/templates/summary.md + + + +@.planning/phases/06-unit-tests/06-CONTEXT.md +@.planning/codebase/TESTING.md +@.planning/phases/06-unit-tests/06-PATTERNS.md +@dev.md +@vite.config.ts +@package.json +@tsconfig.json +@packages-user/data-system/src/combat/damage.ts +@packages-user/data-system/src/combat/context.ts +@packages-user/data-base/src/hero/attribute.ts +@packages-user/data-base/src/hero/equipStore.ts +@packages-user/data-state/src/core.ts +@packages-user/data-state/test/saveablesRoundTrip.test.ts + + + +- 只新增 `vitest.perf.config.ts`、4 个 `*.perf.ts`,并只在 `package.json` 的 `scripts` 里新增一行 `test:perf`;**绝不修改任何生产/核心源码**(`packages/`、`packages-user/*/src` 下的非 `*.perf.ts` 文件)。 +- **不改** `package.json` 的 `test` / `test:ci`,**不改** `vite.config.ts`,**不改** `tsconfig*.json`。 +- 新文件一律 `CRLF` 换行、4 空格缩进、单引号、行宽 80(`dev.md` + `.prettierrc`)。 +- 每个新的顶层结构(`describe` / 测量助手 / fixture 工厂)前写中文单行注释说明用途;每个 `it(...)` 前写中文单行注释说明覆盖内容(`dev.md` 注释规范)。 +- **不使用 `import type`、不写连续 `as`**(`dev.md`);能不用 `as` 就不用,fixture 的类型缺口按各文件既有测试文件的现有写法处理。 +- 每个 perf 文件**内联自己的**最小测量助手与 fixture(D-02/D-03),不新建跨文件共享文件、不引用其他 perf 文件的 helper。 +- **零断言(用户裁决)**:perf 文件不得出现 `expect` 调用;耗时只进表格与 SUMMARY。用例内不得因为耗时抛出或跳过。 +- 每个 case 单独一个 `it`(一个规模/压缩档一个 `it`),避免某个慢 case 拖垮同文件其他 case 的判定与超时归属;`afterAll` 里统一 `console.table(records)`。 +- 测量助手参数固定:`WARMUP_RUNS = 3`(不计时)、`SAMPLE_RUNS = 20`、中位数取下中位数(`samples[10]`)、`p95` 取升序第 `19` 个(`samples[Math.ceil(20 * 0.95) - 1]`)、`min` 取 `samples[0]`;三个数值统一 `Number(v.toFixed(3))` 以避免浮点噪声。 +- 未被使用的 `for...of` 迭代变量用 `_` 前缀(`@typescript-eslint/no-unused-vars` 的 `varsIgnorePattern: '^_'`),否者会报 eslint 错误。 +- 计时只使用 `globalThis.performance.now()`;不使用 fake timers、`process.hrtime` 等其它计时源。 +- 每个 perf 文件按对应既有测试文件的 harness 形态起步(`context.perf.ts`/`damage.perf.ts` 用 `vi.hoisted` stub + `beforeAll` 动态 `import`;`attribute.perf.ts`/`saveables.perf.ts` 用 `vi.hoisted` stub + 静态 `import`),保持与既有文件同构。 +- 各 fixture 必须**无告警噪声**:① 的 10 个全局光环用互不相同的优先级、转换器按代码 1:1 命中(避免 `97`)、所有光环 `couldApplySpecial: false`(避免 `98/99/100`);④ 先建 `maps.compareWith(...)` / `enemyManager.compareWith(...)` 基线(避免 `logger.error(55)`)。运行输出里出现 `[WARNING Code` / `[ERROR Code` 即说明 fixture 有问题——**调整 fixture,不要放宽**。 +- **执行协议(D-33):执行本计划前必须先向用户确认;执行完成后暂停,用中文分条简要汇报本计划结果;详细结果写入共用的 `06-TEST-FINDINGS.md`(无新增缺陷时注明「无新增条目」)。** +- **阶段门禁(D-43):三个阶段按 构件 → 组合 → 完整 顺序执行,每阶段跑绿后才进入下一阶段;若某阶段发现阻断性问题(例如某 case 因 `testTimeout` 失败、或 perf 文件被 `pnpm test:ci` 收集)使后续阶段无法运行,暂停并按 D-05/D-07 向用户汇报、等确认,不得跳过或弱化。** +- **质量门禁(D-44,每个阶段执行后、提交前必过)**:对改动文件运行 `eslint --fix`,并确保 `eslint <改动文件>` **0 错误**;对**本计划改动的 perf/config/script 文件**运行 `pnpm exec vue-tsc --noEmit`,按输出文件路径过滤确保 0 类型错误(`vitest.perf.config.ts` 不在任何 tsconfig project,故以 eslint + 实际加载成功为准;全局既有无关错误,如 `client-modules`/`legacy-plugin-data`,不在本门禁范围);`pnpm test:ci` **全绿**。任一不通过则**不得提交**。 +- **不得静默改规模或改超时**:若 ② 的 `50000` 或 ① 的 `1000` 用例实测逼近/超过 `testTimeout`(30000ms,与 `vite.config.ts` 一致,用户裁决),不得私自调小规模或调大 timeout —— 按 D-07 暂停汇报,由用户裁决。 + + + + + + 阶段 1(构件级):新建 perf 通道(`vitest.perf.config.ts` + `test:perf`)并落地 ② 临界计算测量 + vitest.perf.config.ts, package.json, packages-user/data-system/src/combat/damage.perf.ts + + vite.config.ts(L13–27 两段 `glob.sync` alias 计算与 `path.resolve(__dirname, srcPath)`;L46–49 `test.testTimeout`/`hookTimeout` = 30000 —— perf config 必须镜像 alias 与超时) + package.json(L6–26 `scripts`:`test`/`test:ci` 现状;本任务只在其间新增一行 `test:perf`) + packages-user/data-system/src/combat/damage.test.ts(L18–42 `vi.hoisted` stub;L95–125 `FakeCalculator`(`damage = Math.max(0, 100 - atk * 10)`、`getCriticalLimit() => 10`);L150–216 `createFixture`/`createHandler` —— perf fixture 必须与其同构) + packages-user/data-system/src/combat/damage.ts(L134–194 `calculateCritical`(`heroStatus.getModifiableClone()`、`precision` 默认 12、`maxIterations = clamp(precision, 4, 64)`)、L206–242 `findNextCritical`(二分 + `hero.set` + `calculator.calculate`)) + packages-user/data-system/src/combat/types.ts(L438 `calculateCritical` 的接口签名;`IDamageCalculator`/`CriticalableHeroStatus` 定义) + .planning/phases/06-unit-tests/06-CONTEXT.md(D-02/D-03/D-05/D-07/D-43/D-44) + dev.md(模块/命名/注释/类型规范、CRLF、4 空格、单引号) + + + 本任务分三块,全部属于「只加测试与配置」: + + **① `vitest.perf.config.ts`(仓库根,新建)**:用 `import { defineConfig } from 'vitest/config';` 导出 `defineConfig({ resolve: { alias: [...aliases, ...aliasesUser] }, test: { include: ['**/*.perf.ts'], testTimeout: 30000, hookTimeout: 30000 } })`。 + - `aliases`/`aliasesUser` **逐字镜像** `vite.config.ts` L13–27:`glob.sync('packages/*/src')` → `{ find: '@motajs/', replacement: path.resolve(__dirname, srcPath) }`;`glob.sync('packages-user/*/src')` → `{ find: '@user/', ... }`;`__dirname` 与 `glob.sync` 的用法保持一致。**这是硬前提**:仓库 `node_modules` 下没有 `@user/*`/`@motajs/*`,运行时只能靠 alias 解析(`tsconfig.json` 的 `paths` 只服务类型检查)。 + - 不加 vue 插件、不动 `test.exclude`(保留 vitest 默认排除)、不加 `environment`(默认即 `node`,数据端 DOM-free)。 + - 不修改 `vite.config.ts`(既有 `pnpm test:ci` 通道必须一字不变)。 + + **② `package.json`**:在 `"test:ci": "vitest run",` 之后新增一行 `"test:perf": "vitest run --config vitest.perf.config.ts",`;保持 4 空格缩进、键顺序紧贴 `test:ci`、**不动** `test`/`test:ci`、不动其它字段。 + + **③ `packages-user/data-system/src/combat/damage.perf.ts`(新建,② 临界计算)**: + - 文件首行中文注释说明用途(性能测量、只记录不断言)。 + - `vi.hoisted(() => { ... })` 镜像 `damage.test.ts` L18–42:`vi.stubGlobal('main', { replayChecking: true })`、`vi.stubGlobal('location', { origin: 'http://localhost' })`、`Map.prototype.getOrInsert`/`getOrInsertComputed` 补丁。 + - 文件内联常量:`WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`、`CRITICAL_SCALES = [1000, 10000, 50000] as const`。 + - 文件内联测量助手(不跨文件共享,D-03):`interface PerfRecord { case: string; scale: string; 'median ms': number; 'min ms': number; 'p95 ms': number }`;模块级 `const records: PerfRecord[] = []`;`function measureCase(caseName: string, scale: string, run: () => void): PerfRecord` —— 先 `for (let i = 0; i < WARMUP_RUNS; i++) run();`(不计时),再 `for (let i = 0; i < SAMPLE_RUNS; i++)` 用 `const start = globalThis.performance.now(); run(); samples.push(globalThis.performance.now() - start);` 采样;`samples.sort((a, b) => a - b)` 后取 `median = samples[SAMPLE_RUNS / 2]`(下中位数)、`min = samples[0]`、`p95 = samples[Math.ceil(SAMPLE_RUNS * 0.95) - 1]`,三者 `Number(v.toFixed(3))`;把 record `records.push(...)` 后 `return`。 + - 文件内联 fixture(镜像 `damage.test.ts` 的 `createFixture`,最小合成、D-02):合成 `TestEnemyAttr`/`TestHeroAttr`;`class PerfCalculator` 实现 `IDamageCalculator`(`calculate(handler)` 返回 `{ damage: Math.max(0, 100 - handler.hero.getBaseAttribute('atk') * 10), turn: 2 }`;`getCriticalLimit()` 返回 `10`);假 `IEnemyContext`(`state`/`indexer = new MapLocIndexer()` 且 `setWidth(4)`/`getBindedHero()`/`getEnemyLocatorByView`/`getViewByComputed`);真实 `HeroAttribute`(`hp: 100, atk: 0, def: 0`);`Enemy`(`hp: 30, atk: 8, def: 2`)与其 `clone()`;`view`;`locator = { x: 2, y: 3 }`;`new DamageContext(context, calculator, hero)`。既有的动态 import harness 可用 `beforeAll` 缓存模块(与 `damage.test.ts` 同构);若直接静态 import 在本文件同样可行,则按 `attribute.test.ts` 风格静态 import,二者择一但必须与该文件既有 harness 保持一致。 + - 规模常量与 case 一一对应(`describe` 内 3 个 `it`,每个 `it` 前一行中文注释说明覆盖内容与规模):每个 `it` 内 `measureCase('临界计算', String(count), () => { for (let i = 0; i < count; i++) { for (const _ of damageContext.calculateCritical(view, 'atk')) { /* 完整消费生成器 */ } } })`。`_` 前缀用于通过 `@typescript-eslint/no-unused-vars`(`varsIgnorePattern: '^_'`)。样本必须**完整消费生成器**,否则测不到临界枚举的真实开销。 + - `afterAll(() => { console.table(records); })` 打印整表;全文件**不得出现 `expect`**。 + - 若 `50000` 用例实测逼近或超过 `testTimeout`(30000ms),**不得**调小规模或调大 timeout —— 按 D-07 暂停汇报、等用户裁决。 + + + pnpm test:perf(须成功加载配置并打印 3 行 ② 的 case 表,列为 case/scale/median ms/min ms/p95 ms,且无断言失败);pnpm exec vitest list --filesOnly(默认配置;输出不得含 `.perf.ts`,即 `pnpm test:ci` 仍不收集 perf 文件) + 仓库根未生成 `vitest.perf.config.ts`;或该 config 的 `test.include` 不是 `['**/*.perf.ts']`;或未镜像 `@motajs/*`/`@user/*` alias 导致解析失败(`Failed to resolve import`);或 `package.json` 的 `test`/`test:ci` 被改动;或 `pnpm test:perf` 未收集到 `damage.perf.ts`;或表格缺 `median ms`/`min ms`/`p95 ms` 任一列;或文件内出现 `expect(`;或 `pnpm exec vitest list --filesOnly` 输出中出现 `.perf.ts`(说明 perf 文件可能被 `test:ci` 收集);或本任务改动了任何生产源码或 `vite.config.ts` + + + - 新增 `vitest.perf.config.ts`:`test.include = ['**/*.perf.ts']`、`testTimeout = 30000`、`hookTimeout = 30000`、alias 与 `vite.config.ts` 一致;`vite.config.ts` 未改动。 + - `package.json` 仅新增 `"test:perf": "vitest run --config vitest.perf.config.ts"` 一行,`test`/`test:ci` 逐字未变。 + - 新增 `packages-user/data-system/src/combat/damage.perf.ts`:3 个 case(`1000`/`10000`/`50000`),每个 case 预热 3 + 采样 20、取中位数;`afterAll` 打印 `console.table`;全文件 0 个 `expect`。 + - `pnpm test:perf` 收集到 1 个 perf 文件并打印 3 行表格(含 `case`/`scale`/`median ms`/`min ms`/`p95 ms`)。 + - `pnpm exec vitest list --filesOnly`(默认 `vite.config.ts`)输出不含 `.perf.ts`;`pnpm test:ci` 仍然全绿且收集文件数与改动前一致。 + - D-44 门禁通过:`eslint --fix` 后改动文件 `eslint` 0 错误(注意 `console.table` 只触发 `no-console` 的 `warn`,不是 error);`damage.perf.ts` 在 `pnpm exec vue-tsc --noEmit` 输出中按路径过滤 0 类型错误;`pnpm test:ci` 全绿。 + - 除 `vitest.perf.config.ts`、`package.json`、`damage.perf.ts` 外无任何文件改动(`git status` 复核)。 + + 阶段 1 的 `pnpm test:perf`(3 行表)与「`test:ci` 不收集 perf」两项都成立后才进入阶段 2。若配置加载失败、或被 `test:ci` 收集、或某 case 超时,暂停并按 D-07 向用户汇报、等确认。 + + + + 阶段 2(组合级):新增 ① 怪物上下文构建与 ③ 勇士属性计算两个 perf 文件 + packages-user/data-system/src/combat/context.perf.ts, packages-user/data-base/src/hero/attribute.perf.ts + + packages-user/data-system/src/combat/context.test.ts(L23–47 `vi.hoisted` stub;L96–166 内联 `FakeSpecial`/`createSpecial`/`createEnemy`/`createContextFixture`;L178–280 `FakeAura`/`FakeAuraOptions`/`FakeConverter` —— perf fixture 必须与其同构) + packages-user/data-system/src/combat/context.ts(L105–111 `resize`(清空怪物 + `needUpdate = true`)、L193–198 `bindHero`(`needUpdate = true`)、L342–350 `addAura`、L564–620 `buildupSpecials`(`globalAuraList` → `sortedAura`、按优先级 while 循环)、L626–642 `buildupBase`(按优先级对范围内每个怪 `aura.apply`)、L684–720 `buildup`(`!needUpdate` 早退、`!bindedHero` → warn 110、阶段顺序)) + packages-user/data-system/src/combat/context.ts(L433–503 `processSpecialModifier` 的 98/99/100 触发条件 —— 决定 perf fixture 为何必须 `couldApplySpecial: false`) + packages-user/data-base/src/hero/attribute.test.ts(L7–31 `vi.hoisted` stub + `afterAll(vi.unstubAllGlobals)`;L37–92 内联 `TestHero`/`TestModifier`/`createAttribute`;L133–145 `addModifier` 排序语义;L205–218 `markModifierDirty` 语义 —— perf fixture 必须与其同构) + packages-user/data-base/src/hero/attribute.ts(L10–46 `BaseHeroModifier`;L80–102 private `recalculateAttribute`;L127–129 `getFinalAttribute`;L180–202 `addModifier`;L231–239 `markDirty`/`markModifierDirty`) + .planning/phases/06-unit-tests/06-CONTEXT.md(D-02/D-03/D-05/D-07/D-43/D-44) + dev.md(注释/类型规范、CRLF、4 空格、单引号、`_` 前缀约定) + + + 两个文件都遵守与阶段 1 相同的测量协议(`WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`、下中位数、`p95 = samples[18]`、`min = samples[0]`、`toFixed(3)`、`afterAll` 打印 `console.table(records)`、文件内联测量助手、**零 `expect`**);同一文件内每个 case 单独一个 `it`。 + + **① `packages-user/data-system/src/combat/context.perf.ts`(怪物上下文构建)**: + - `vi.hoisted` stub + `beforeAll` 动态 import harness 镜像 `context.test.ts`(`main`/`location`/`Map` 补丁)。 + - 内联最小 fixture(D-02/D-03):合成 `TestEnemyAttr`/`TestHeroAttr`;`createSpecial(code)`;`createEnemy(id, attrs)`(基础 `hp: 10, atk: 2, def: 0`);内联 `PerfAura implements IAuraView`(`priority`/`range`/`couldApplyBase`/`couldApplySpecial = false`/`getRangeParam()`/`apply(handler)` 里 `handler.enemy.addAttribute('atk', 1)`/`applySpecial()` 返回 `null`);内联 `PerfConverter implements IAuraConverter`,**按特殊属性代码 1:1 命中**且对每个代码**缓存同一个光环实例**(一个 `Map`),`convert()` 返回缓存实例 —— 这样光环拓扑固定为 `10 + 5 = 15` 个,单次 `buildup()` 的耗时随 N 稳定线性增长,且**不会**因 N 变大触发 O(N²) 让 `testTimeout` 失效;转换器与光环的 `import.meta.env.DEV` 告警路径必须不触发(见下)。 + - 夹具构建(每 case 一次,**不参与计时**):`new EnemyContext({} as IStateBase)` → `resize(side, side)`(`side = Math.ceil(Math.sqrt(N))`,保证 N 个坐标在界内)→ `bindHero(heroAttr)`(`new HeroAttribute({ hp: 100, atk: 0, def: 0 })`)→ `registerAuraConverter(converter)` → `addAura` × 10 个全局 `PerfAura`(`FullRange`,**优先级互不相同**,建议 `1..10`,`couldApplySpecial: false`)→ `setEnemyAt({ x: i % side, y: Math.floor(i / side) }, createEnemy('perf-' + i))` × N,且每个怪 `addSpecial(createSpecial(code))`(代码取 `20..24` 共 5 个,10 个全局光环的代码集合与这 5 个**互不相同**)。 + - 无告警约束(必须成立,否则 fixture 有问题,需调整而不是放宽):每个特殊属性代码只被一个转换器命中(避免 `97`);所有光环 `couldApplySpecial = false` 使 `processAuraSpecial` 不执行(避免 `98/99/100`);10 个全局光环优先级互不相同。 + - 3 个 case:N = `50 / 200 / 1000`,每个 `it` 内 `measureCase('怪物上下文构建', String(n), () => { context.bindHero(heroAttr); context.buildup(); })`。**必须每个样本先 `bindHero`**:`buildup()` 在 `needUpdate === false` 时直接返回,`bindHero` 是公开且最省的 `needUpdate = true` 触发器(该 fixture 不 attach `damageSystem`/`mapDamage`,故无额外副作用)。 + - 若 `1000` 用例实测逼近/超过 `testTimeout`(30000ms),不得调小规模或调大 timeout —— 按 D-07 暂停汇报。 + + **③ `packages-user/data-base/src/hero/attribute.perf.ts`(勇士属性计算)**: + - `vi.hoisted` stub + `afterAll(() => { vi.unstubAllGlobals(); })` + **静态 import**,镜像 `attribute.test.ts`(`import { BaseHeroModifier, HeroAttribute } from './attribute';`,`import { type IHeroModifier } from './types';`)。 + - 内联 fixture:合成 `PerfHeroAttr`(**恰好 10 个数值字段**,如 `hp/atk/def/mdef/money/exp/point/score/mana/speed`,全为 `number`);`const ATTR_NAMES: readonly (keyof PerfHeroAttr)[]` 列出这 10 个名字(顺序固定);`class PerfModifier extends BaseHeroModifier`(`readonly type = '@perf/value'`、`readonly priority = 0`、`modify(value) { return value + this.value; }`、`clone()` 返回新实例)。 + - 夹具构建(每 case 一次,不参与计时):`new HeroAttribute({ ...10 个属性各给一个固定基础值 })`;`for (let i = 0; i < m; i++) attribute.addModifier(ATTR_NAMES[i % 10], new PerfModifier(i), ...)`,把 M 个修饰器**均匀分布到 10 个属性**(每属性 M/10 个,M 均为 10 的倍数),并把每个修饰器收进 `const modifiers: PerfModifier[]` 供采样时轮转使用;每个修饰器都是新实例(避免 `108` 告警)。 + - 3 个 case:M = `10 / 100 / 1000`,每个 `it` 内 `measureCase('勇士属性计算', String(m), () => { for (let i = 0; i < 1000; i++) { attribute.markModifierDirty(modifiers[i % modifiers.length]); for (const name of ATTR_NAMES) attribute.getFinalAttribute(name); } })`。`recalculateAttribute` 为 private,故用公开 `markModifierDirty` 触发同一重算路径(`dev.md` 类型规范下 `markModifierDirty(modifier)` 的实参类型与 `attribute.test.ts:L216` 的写法一致,可编译)。 + - 每个 `it` 前一行中文注释写清覆盖内容与规模;不得写 `expect`。 + + + pnpm test:perf(共 9 行 case:② 3 行 + ① 3 行 + ③ 3 行;无断言失败、无 `[WARNING Code` / `[ERROR Code` 噪声);pnpm exec vitest list --filesOnly(默认配置;输出仍不含任何 `.perf.ts`);pnpm test:ci(全绿,用于确认既有行为单测不受影响) + 两个文件未生成,或文件名不是 `*.perf.ts`;或 `pnpm test:perf` 未收集到二者;或运行时出现 `[WARNING Code 97/98/99/100/110]` 之一(说明光环 fixture 触发了告警路径,必须调整 fixture 而非忽略);或出现 `[ERROR Code`;或 ① 的转换器为每个(怪物, 特殊属性)都新建光环实例导致 N=1000 用例超时;或 ③ 使用了不可公开调用的 private 重算入口、或没走 `markModifierDirty`;或任一文件出现 `expect(`;或生产源码被改动 + + + - 新增 `packages-user/data-system/src/combat/context.perf.ts`:3 个 case(N = 50/200/1000),单样本 = 一次全量 `buildup()`(每样本先 `bindHero`);固定 10 个全局光环 + 5 个特殊属性代码;输出 3 行表格。 + - 新增 `packages-user/data-base/src/hero/attribute.perf.ts`:3 个 case(M = 10/100/1000),单样本 = 1000 次「`markModifierDirty` + 读 10 个 `getFinalAttribute`」;修饰器分布到 10 个属性;输出 3 行表格。 + - 两个文件均为「预热 3 + 采样 20 取中位数」,`afterAll` 打印 `console.table`,0 个 `expect`,测量助手文件内联(无跨文件共享)。 + - `pnpm test:perf` 共输出 9 行 case,且运行日志无 warn/error code 噪声。 + - `pnpm exec vitest list --filesOnly`(默认配置)输出仍不含 `.perf.ts`;`pnpm test:ci` 全绿且收集文件数不变。 + - D-44 门禁通过:改动文件 `eslint` 0 错误;两个 perf 文件在 `pnpm exec vue-tsc --noEmit` 输出中按路径过滤 0 类型错误。 + + ① 与 ③ 各 3 行表格打印成功、无告警噪声、`test:ci` 不受影响后才进入阶段 3。若某 case 超时或出现告警码,暂停并按 D-07 汇报、等确认(不得调规模/超时、不得忽略告警)。 + + + + 阶段 3(完整级):新增 ④ 存读档 perf(5 个 saveable 整体往返 × 3 档压缩 × 3 档件数)并汇总数值表 + packages-user/data-state/test/saveables.perf.ts + + packages-user/data-state/test/saveablesRoundTrip.test.ts(L20–53 `vi.hoisted` stub 与 5 个 saveable id;L63–111 `createEnemy`/`seedReplay`;L113–151 `seedState`(`maps.createMap` + `maps.compareWith` 基线 + `enemyManager.compareWith` 基线);L220–237 三档压缩整体往返写法 —— perf 夹具必须与其同构) + packages-user/data-state/src/core.ts(L94–130 字段与 saveables;L248–252 五个 `addSaveableContent`;L516–545 `saveState`/`loadState`) + packages-user/data-base/src/hero/equipStore.ts(L184–203 `add(item: number | string): number` 依赖 `state.tileStore.num` + `state.itemStore.getData`) + packages-user/data-base/src/hero/items.ts(L74–109 `itemCount`/`addItem`(`Equipment` → `equipment.add`;其它分类 → 分类 Map 计数)) + packages-user/data-base/src/hero/equipStore.test.ts(L75–111 `createItem`/`registerItem`(`tileStore.addTile({ num, id, events: {}, type: TileType.Item, pass: { onlyEvents: false, inPass: 15, outPass: 15 }, eventPass: true })` + `itemStore.addItem(item)`)—— 合成道具注册的权威写法) + packages-user/data-common/src/store/types.ts(L141–209 `ItemCategory` / `IItemRawData`(`effect` 与 `equip` 都是**必填**字段)) + packages-user/data-common/src/save/types.ts(L3–9 `SaveCompression` 三档) + packages-user/data-base/src/map/mapState.ts(L400–425 Low/High 压缩要求 `compared` 基线,否则 `logger.error(55)`) + .planning/phases/06-unit-tests/06-CONTEXT.md(D-02/D-03/D-05/D-07/D-32/D-43/D-44/D-45) + + + 新建 `packages-user/data-state/test/saveables.perf.ts`,协议与阶段 1/2 完全一致(`WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`、下中位数、`p95 = samples[18]`、`min = samples[0]`、`toFixed(3)`、`afterAll` 打印 `console.table(records)`、文件内联测量助手、**零 `expect`**);每个「件数 × 压缩档」组合单独一个 `it`。 + - 头部:`vi.hoisted` stub 镜像 `saveablesRoundTrip.test.ts` L20–44(`main`/`location`/`Map` 补丁);静态 import `createCoreState` from `../src/core`,`SaveCompression`/`ItemCategory`/`TileType`/`type IHeroAttr`/`type IItemRawData` 等 from `@user/data-common`,`Enemy`/`ValueModifier` 等 from `@user/data-base`(按需,保持最小)。 + - 常量:`WARMUP_RUNS = 3`、`SAMPLE_RUNS = 20`、`ITEM_SCALES = [10, 100, 1000] as const`、`COMPRESSIONS = [SaveCompression.NoCompression, SaveCompression.LowCompression, SaveCompression.HighCompression] as const`(共 9 个 case)。 + - 内联合成道具工厂(镜像 `equipStore.test.ts` 的 `createItem`/`registerItem`,D-02):`createItemRaw(num, id, category)` 返回完整 `IItemRawData`(**`effect` 与 `equip` 两个必填字段都要给**,`equip` 内 `slots: [0]`、`animate: 'sword'`、`value`/`percentage` 用 `new Map()` 或固定小 `Map`、`loadEvent`/`unloadEvent` 为 `null`);`registerItem(state, item)` 先 `state.tileStore.addTile({ num: item.num, id: item.id, events: {}, type: TileType.Item, pass: { onlyEvents: false, inPass: 15, outPass: 15 }, eventPass: true })` 再 `state.itemStore.addItem(item)`(顺序不可颠倒:`HeroEquipsStore.add` 依赖 `itemStore.getData`)。 + - 件数夹具(每 case 一次,**不参与计时**;用 `createCoreState()` 新建干净状态):先镜像 `saveablesRoundTrip.test.ts` 的最小基线 —— `state.maps.createMap('F1', 2, 2)` + `map.addLayer()` + `state.maps.compareWith(new Map([['F1', new Map([[0, new Uint32Array(4)]])]]))`、`state.enemyManager.compareWith(new Map([[1, createEnemy()]]))`(避免 Low/High 压缩走 `mapState.ts:409` 的 `logger.error(55)`);然后按件数 K 注册并写入 —— `for (let i = 0; i < K; i++) { registerItem(state, createItemRaw(1000 + i, 'perf-equip-' + i, ItemCategory.Equipment)); state.hero.items.equipment.add(1000 + i); }` 与 `for (let i = 0; i < K; i++) { registerItem(state, createItemRaw(2000 + i, 'perf-item-' + i, ItemCategory.Consumable)); state.hero.items.addItem(2000 + i, 1); }` —— 使 `@system/hero` 的存档负载随 K 线性增长(K 件装备实例 + K 条消耗品条目)。 + - 单样本 = 一次整体往返:`const snapshot = state.saveState(compression); state.loadState(snapshot, compression);`;9 个 `it` 前各写一行中文注释(覆盖的件数与压缩档)。 + - 约束:不得写 `expect`;不得断言恢复后的数值(那是 06-13 行为测试的职责,本文件只测耗时);不得 `logger.catch` 包住耗时循环(会把捕获开销算进测量);夹具注册与基线构建必须在预热循环之前完成。 + - 若某 case 实测逼近/超过 `testTimeout`(30000ms),不得调小件数或调大 timeout —— 按 D-07 暂停汇报。 + + + pnpm test:perf(共 18 行 case:② 3 + ① 3 + ③ 3 + ④ 9;无断言失败、无 `[WARNING Code` / `[ERROR Code`);pnpm exec vitest list --filesOnly(默认配置;输出不含 `.perf.ts`);pnpm test:ci(全绿) + `saveables.perf.ts` 未生成或被 `pnpm test:perf` 遗漏;或 ④ 只覆盖部分压缩档/件数(不是 9 个 case);或出现 `[ERROR Code 55]`(说明缺少 `maps.compareWith` 基线);或出现其它 warn/error 噪声(说明道具注册顺序或分类不对);或 `state.hero.items.equipment.add` 返回 `-1`(未先注册图块/道具定义);或文件内出现 `expect(`;或 ④ 的 case 超时;或生产源码被改动 + + + - 新增 `packages-user/data-state/test/saveables.perf.ts`:9 个 case = 件数 `10/100/1000` × 压缩档 `NoCompression/LowCompression/HighCompression`,单样本 = `CoreState.saveState` + `loadState` 整体往返(5 个 saveable,D-45)。 + - 夹具只使用合成道具定义与合成地图/怪物基线(D-02);道具注册严格 `addTile` → `itemStore.addItem` → `items.equipment.add` / `items.addItem`;运行日志无 `[ERROR Code 55]` 等噪声。 + - `pnpm test:perf` 一次运行输出 **18 行** case 表(`case`/`scale`/`median ms`/`min ms`/`p95 ms`),0 个 `expect`。 + - `pnpm exec vitest list --filesOnly`(默认配置)输出不含任何 `.perf.ts`;`pnpm test:ci` 全绿且收集文件数与基线一致(既有行为测试零影响)。 + - D-44 门禁通过:改动文件 `eslint` 0 错误;`saveables.perf.ts` 在 `pnpm exec vue-tsc --noEmit` 输出中按路径过滤 0 类型错误。 + - 18 行数值表收集齐全,可直接粘进 `06-16-SUMMARY.md`(不落盘基线 JSON)。 + + 四个 perf 文件全部落地、`pnpm test:perf` 一次运行输出 18 行 case 表、`pnpm test:ci` 全绿且不收集 perf 文件;D-44 三项门禁通过。随后暂停,用中文分条汇报耗时结果(含是否有 case 逼近 `testTimeout`),并把数值表写入 `06-16-SUMMARY.md`。 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| none | 仅新增仓库内 Node Vitest 性能测量文件与一个本地配置/脚本;无运行时输入面、无网络、无 DOM、无文件写入(结果只进 `console.table` 与 SUMMARY)。 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-06-16-01 | Tampering | `vitest.perf.config.ts` 误把 perf 文件并入默认收集通道 | medium | mitigate | `test.include` 限定 `['**/*.perf.ts']`;`test`/`test:ci` 脚本一字不改;每个阶段的 `` 都跑 `pnpm exec vitest list --filesOnly` 确认默认配置输出不含 `.perf.ts`,并复核 `pnpm test:ci` 收集文件数不变。 | +| T-06-16-02 | Tampering | 新 config 绕过既有 alias,导致解析到错误模块或静默失败 | medium | mitigate | 逐字镜像 `vite.config.ts` 的 `glob.sync` alias(`@motajs/*`/`@user/*`);`pnpm test:perf` 必须实际收集并执行 4 个文件,任何 `Failed to resolve import` 即失败。 | +| T-06-16-03 | Repudiation | 用「跑绿」冒充性能数据(例如空转循环、未消费生成器、未真正重建光环) | medium | mitigate | ② 必须完整消费 `calculateCritical` 生成器;① 每样本前 `bindHero` 保证 `buildup()` 不早退(`needUpdate`);④ 每样本真正调用 `saveState`+`loadState`;禁止任何 `expect`,禁止 `it.skip`。 | +| T-06-16-04 | Denial of service | 最大规模 case 触发 `testTimeout`(30000ms),把计划判据变成随机失败 | medium | mitigate | 每个 case 独立 `it`(超时归因清晰);① 的转换器按代码缓存光环实例把拓扑固定在 `10+5`,避免 O(N²);③ 每样本固定 1000 次重算;若某 case 逼近/超时,按 D-07 暂停汇报,**不得**静默调规模或调大 timeout。 | +| T-06-16-05 | Information disclosure | 夹具引入真实游戏数据或产物 | low | accept | 全部为文件内联合成 fixture(合成道具/怪物/地图基线,D-02),不读取真实游戏数据、不落盘基线 JSON。 | +| T-06-16-06 | Tampering | 为让 perf 跑绿而修改生产源码或既有测试 | medium | mitigate | 明确禁止改动 `packages*/**`(非 `*.perf.ts`)、`vite.config.ts`、`tsconfig*.json`、既有 `*.test.ts` 与 06-01..06-15 产物;`git status` + D-44 门禁在每阶段提交前复核。 | +| T-06-16-SC | Tampering | 包安装(npm/pnpm) | high | mitigate | 明确不引入任何新依赖(vitest 4.0.18 已在 devDependencies);若确需安装,停止并先走包合法性 checkpoint。 | + + + +- `pnpm test:perf` 一次运行收集 4 个 `*.perf.ts`、打印 18 行 `console.table`(列 `case`/`scale`/`median ms`/`min ms`/`p95 ms`)并成功退出;输出中不出现 `[WARNING Code` / `[ERROR Code`。 +- `pnpm exec vitest list --filesOnly`(默认 `vite.config.ts` 配置)输出**不含**任何 `.perf.ts`;`pnpm test:ci` 全绿且收集文件数与基线一致(既有行为测试零影响)。 +- 仓库根存在 `vitest.perf.config.ts`(`include = ['**/*.perf.ts']`、`testTimeout`/`hookTimeout` = 30000、alias 与 `vite.config.ts` 一致);`package.json` 仅新增 `test:perf`,`test`/`test:ci` 逐字未变。 +- 4 个 perf 文件全部为「预热 3 + 采样 20 + 中位数/最小/p95」,测量助手与 fixture 文件内联,**零 `expect`**,未使用 fake timers。 +- 三档固定规模齐全:① `50/200/1000`;② `1000/10000/50000`;③ `10/100/1000`;④ `10/100/1000` × `NoCompression/LowCompression/HighCompression`。 +- `git status` 仅显示 `vitest.perf.config.ts`、`package.json`、4 个 `*.perf.ts`(以及 SUMMARY/ROADMAP 等规划产物)的变更;无生产源码改动、无新依赖、无基线 JSON 落盘。 +- D-44 文件级门禁:改动文件 `eslint --fix` 后 `eslint <改动文件>` 0 错误;4 个 perf 文件在 `pnpm exec vue-tsc --noEmit` 输出中按路径过滤 0 类型错误;`pnpm test:ci` 全绿。 +- **执行协议**:执行前必须先向用户确认;执行后暂停并用中文分条汇报(含是否有 case 逼近 `testTimeout`)。 + + + +- 用户批准的四项测量全部落地且口径一致:18 个 case,均为预热 3 次不计时 + 采样 20 次取中位数,输出 `console.table`(`case`/`scale`/`median ms`/`min ms`/`p95 ms`),**零断言**。 +- 新增独立通道:`vitest.perf.config.ts` + `test:perf` 脚本;`pnpm test:ci` 的语义、收集文件数与结果**完全不变**(`*.perf.ts` 不被默认 include 匹配)。 +- 规模固定:① `50/200/1000`;② `1000/10000/50000`;③ `10/100/1000`;④ `10/100/1000` × 三档压缩。 +- 无生产代码改动、不新增依赖、不改 `vite.config.ts`/`tsconfig*.json`/既有 `*.test.ts` 与 06-01..06-15 产物。 +- `06-16-SUMMARY.md` 内含 18 行数值表(来自真实 `pnpm test:perf` 运行)与测量参数说明;**不落盘基线 JSON**。 +- D-44 文件级三项门禁(eslint 0 错误 / 文件级 vue-tsc 0 类型错误 / `pnpm test:ci` 全绿)在每个阶段提交前通过。 + + + +Perf test files (created): +- `packages-user/data-system/src/combat/damage.perf.ts` — ② 临界计算(3 个 case:1000/10000/50000 次完整临界枚举;固定 atk 0、固定伤害区间 100→0) +- `packages-user/data-system/src/combat/context.perf.ts` — ① 怪物上下文构建(3 个 case:N = 50/200/1000 的单次 `buildup()`;固定 10 全局光环 + 5 特殊属性代码) +- `packages-user/data-base/src/hero/attribute.perf.ts` — ③ 勇士属性计算(3 个 case:M = 10/100/1000 修饰器分布 10 属性;每样本 1000 次 `markModifierDirty` + 读 10 个 `getFinalAttribute`) +- `packages-user/data-state/test/saveables.perf.ts` — ④ 存读档(9 个 case:件数 10/100/1000 × 三档压缩的 `CoreState` 5-saveable 整体往返) + +Config / script (created / modified): +- `vitest.perf.config.ts`(新建)— 独立 perf 通道:`include: ['**/*.perf.ts']`、`testTimeout`/`hookTimeout` 30000、与 `vite.config.ts` 一致的 `@motajs/*`/`@user/*` alias +- `package.json`(改 1 行)— 新增 `test:perf`;`test`/`test:ci` 不变 + +Planning artifacts: +- `06-16-SUMMARY.md` — 18 行耗时数值表(`case`/`scale`/`median ms`/`min ms`/`p95 ms`)+ 测量参数(预热 3、采样 20、中位数/p95 口径、运行环境)+ 无断言说明 +- `.planning/ROADMAP.md` — Phase 6 的 Wave 6 小节与 Plans 计数登记「性能测试补充(06-16)」 + + + +完成时创建 `.planning/phases/06-unit-tests/06-16-SUMMARY.md`,其中必须包含:真实 `pnpm test:perf` 运行输出的 **18 行数值表**(`case`/`scale`/`median ms`/`min ms`/`p95 ms`,以 markdown 表格呈现)、测量参数说明(预热 3 次不计时、采样 20 次取中位数、`min`/`p95` 口径、计时源 `globalThis.performance.now()`、运行环境 Node 版本/平台)、`pnpm test:ci` 收集文件数不变与全绿结论、以及「本计划不落盘基线 JSON、不做任何耗时断言」的说明。若某 case 逼近 `testTimeout` 或出现告警/错误码噪声,按 D-05 追加 `06-TEST-FINDINGS.md` 的 `#06-16-N` 并在 SUMMARY 中显式标注;无异常时注明「无新增条目」。执行完成后暂停并用中文分条汇报。 + +