mirror of
https://github.com/motajs/template.git
synced 2026-11-04 00:04:32 +08:00
docs(09-33): add setting system type declaration plan and execution summary (code uncommitted)
This commit is contained in:
1 parent
f5f8322985
commit
db726ff58d
3 files changed
+565
No files matched your search
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"head": "f5f83229851c910b05db33834d7e0f65d7b5120b",
|
||||
"hashes": {
|
||||
"packages-user/client-base/src/setting/index.ts": "fcb073fefcd6bebc8afd726f830e66828ebc5e90",
|
||||
"packages-user/client-base/src/index.ts": "3904bbe873a1fc1fe448529708dda15232271019",
|
||||
"packages-user/client-base/src/types.ts": "cdf7224dc39118665b00a9697c31e6f5ae7c4f84",
|
||||
"packages-user/client-base/package.json": "3e9b20220568df6244bbbceadb16e5c3b2a3e874",
|
||||
"packages-user/client-base/src/hotkey/types.ts": "d1cdf0c25530950a46bce801df63ed975665b451",
|
||||
"packages-user/data-common/src/save/types.ts": "06b37a622a5e26abb91cc6332c49ca65c6691204",
|
||||
"packages/system/src/ui/types.ts": "2a24550a6a3f8dade44524f7be2564f7d7957551"
|
||||
},
|
||||
"dirty": [
|
||||
"packages-user/client-base/src/index.ts",
|
||||
"packages-user/client-base/src/map/__test__/rendererGate.test.ts",
|
||||
"packages-user/client-base/src/map/renderer.ts",
|
||||
"packages-user/client-base/src/map/shader/map.frag",
|
||||
"packages-user/client-base/src/map/status.ts",
|
||||
"packages-user/client-base/src/map/types.ts",
|
||||
"packages-user/client-base/src/map/vertex.ts",
|
||||
"packages-user/client-base/src/setting/index.ts",
|
||||
"packages-user/client-base/src/setting/types.ts",
|
||||
"src/client/ui/__test__/hotkeySettingsGate.test.ts",
|
||||
"src/client/ui/__test__/uiContainerGate.test.ts",
|
||||
"src/client/ui/book.tsx",
|
||||
"src/client/ui/bookDetail.tsx",
|
||||
"src/client/ui/equipbox.tsx",
|
||||
"src/client/ui/hotkey.tsx",
|
||||
"src/client/ui/toolbox.tsx"
|
||||
],
|
||||
"typecheck": [],
|
||||
"typecheckTotal": 159
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 33
|
||||
type: execute
|
||||
# 28:沿用本阶段既有计划的波次编号,使本计划排在 09-01..09-32 之后(D-01 增量派发,计划累积)
|
||||
# depends_on 为空:本计划只替换 1 个尚未被任何文件引用的声明文件,结构上独立;
|
||||
# 运行在最后的波次只是「排期在后」,不是「依赖前序计划」
|
||||
wave: 28
|
||||
depends_on: []
|
||||
# 09-33-BASELINE.json 由 Task 1 在收到「可以执行」之后、写入任何代码之前写出(Task 0 只汇报、不写文件)
|
||||
files_modified:
|
||||
- packages-user/client-base/src/setting/types.ts
|
||||
- .planning/phases/09-minimum-browser-runnable/09-33-PLAN.md
|
||||
- .planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
# 未校准估算(confidence: low),SUMMARY 须声明这一点。
|
||||
# 本计划只做一件事:把 setting/types.ts 的两个空壳 interface 替换为 6 个类型声明。
|
||||
# 不实现系统、不写工厂、不写 UI、不接存档注册、不接 IClientBase、不改 index.ts。
|
||||
# 主要成本在:逐字落 6 个 interface 与 jsDoc、核对 `ISaveableContent<T>` / `ISettingItem<void>` /
|
||||
# `IGameUI` / `UIComponent` 的泛型可用性、跑一次整仓 vue-tsc 基线 + 一次复检、写摘要。
|
||||
# tokens 取 raw 的 2 倍,与 09-19..09-32 的换算口径一致;仍属未校准。
|
||||
# tasks 只计 2 个 auto 执行任务(Task 1–2);Task 0 是 blocking-human 汇报关卡,不写文件,不计入估算。
|
||||
tokens: 18000
|
||||
raw_tokens: 9000
|
||||
tasks: 2
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求 / D-03)**:先汇报「把 `setting/types.ts` 的两个空壳 interface 换成 6 个类型声明」的要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条列出 6 个 interface 与其全部成员、`ISettingItem` 不带分组字段的决定、不提供「未分组」枚举方法的决定、`ISettingSubPageItem` 是 `ISettingItem<void>` 并带 `page: IGameUI`、存档形状 `ReadonlyMap`、jsDoc / 导入语法要求、验证方式,以及「不做浏览器 UAT / 执行器不提交任何代码」两条铁律,然后停下等待用户恰好回复「可以执行」"
|
||||
- "**只动 `files_modified` 的 4 个路径**:`packages-user/client-base/src/setting/types.ts`、`09-33-PLAN.md`、`09-33-BASELINE.json`、`09-33-SUMMARY.md`;**不改** `packages-user/client-base/src/setting/index.ts`(它已经是 `export * from './types';`)、`packages-user/client-base/src/types.ts`(`IClientBase`)、`packages-user/client-base/package.json`、`packages-user/client-base/src/hotkey/types.ts`、`packages-user/data-common/src/save/types.ts`、`packages/system/src/ui/types.ts`;不改任何其它 UI / 依赖 / 配置"
|
||||
- "**`types.ts` 恰好声明 6 个导出的 interface**:`ISettingItem<T = unknown>`(extends `ISaveableContent<T>`)、`ISettingSubPageItem`(extends `ISettingItem<void>`)、`ISettingItemGroup`、`ISettingSystemSave`、`ISettingSystem`(extends `ISaveableContent<ISettingSystemSave>`)、`ISettingComponentProps<T = unknown>`;`export` 出现次数恰好 6 次,且全部为 `export interface`"
|
||||
- "**`ISettingItem` 的成员恰好是 7 个**:`readonly id: string`、`readonly name: string`、`readonly description: string`、`getValue(): T`、`setValue(value: T): void`、`getPreviewString(): string`、`getSettingComponent(): UIComponent`;**不含任何分组字段**(分组归属只存在于 `ISettingSystem.groups` 与 `ISettingItemGroup.items`);块内左括号计数恰好 4(即只有 4 个方法,未重复声明继承自 `ISaveableContent` 的存档 / 读取方法)"
|
||||
- "**`ISettingSubPageItem` 是 `ISettingItem<void>` + `readonly page: IGameUI`**:`void` 只出现在类型层(此时存档 / 读取在类型上即为空操作),本计划不写任何实现、不写工厂;`page` 类型直接使用 `@motajs/system` 的 `IGameUI` 默认泛型"
|
||||
- "**`ISettingItemGroup` 与热键的 `IHotkeyCommandGroup` 同形**:`readonly name: string` + `readonly items: readonly ISettingItem[]`;分组只承载名称与条目,不额外带 id 字段(id 是 `ISettingSystem.groups` 的键)"
|
||||
- "**存档形状与热键同风格**:`ISettingSystemSave { readonly values: ReadonlyMap<string, unknown> }`;`ISettingSystem` 继承 `ISaveableContent<ISettingSystemSave>` 且块内左括号计数恰好 7(`setGroup` / `addItemToGroup` / `getGroup` / `getItem` / `iterateGroups` / `getValue<T>` / `setValue<T>`),不重复声明存档 / 读取方法"
|
||||
- "**`ISettingSystem` 的 7 个方法签名逐字按设计**:`setGroup(group: string, name: string, items: ISettingItem[]): void`、`addItemToGroup(group: string, item: ISettingItem): void`、`getGroup(group: string): ISettingItemGroup | null`、`getItem(id: string): ISettingItem | null`、`iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>`、`getValue<T>(id: string): T | null`、`setValue<T>(id: string, value: T): void`;`readonly groups: ReadonlyMap<string, ISettingItemGroup>`"
|
||||
- "**不提供“未分组”枚举方法**:不声明 `getUngrouped`(设置项只能通过分组进入系统,与热键指令不同——热键存在未分组指令,设置项不存在)"
|
||||
- "**`ISettingComponentProps<T = unknown>` 只有一个成员**:`readonly setting: ISettingItem<T>`;不新增任何选项对象、工厂参数类型或实现类"
|
||||
- "**导入是普通导入且只有 2 行**:第 1 行 `import { ISaveableContent } from '@user/data-common';`,第 2 行 `import { IGameUI, UIComponent } from '@motajs/system';`(顺序与用户批准的声明草案逐字一致);两行紧跟文件开头、中间无空行无注释、无第三行导入;不使用仅类型的导入写法(dev.md 要求)"
|
||||
- "**jsDoc 符合 dev.md**:每个导出 interface 的每个成员与方法都有中文 jsDoc;方法用换行风格、可带 `@param`;短成员用单行风格;方法之间空一行,成员之间不空行;**不写 interface 本身的 jsDoc**(dev.md「一般不建议给接口本身写注释」);不改任何既有注释(该文件当前只有 3 行空壳、无注释,属整文件替换)"
|
||||
- "**纯类型声明、零实现**:文件内不含类、函数体、`export const` / `export let` / `export type` / `export function` / `export class`;不含 `=>`、不含 `) {` 形式的实现体;不新增依赖、不改 `index.ts`、不新增测试文件(声明文件用 Task 1 的 node 源码形状闸门已足够,`?raw` 测试对纯声明过重)"
|
||||
- "**验证是静态的,且如实声明未做浏览器验证**:`pnpm exec eslint packages-user/client-base/src/setting/types.ts` 必须 0 报错;`pnpm check:type` 整仓运行后**该文件内 NEW 诊断数为 0**,且整仓诊断总数不高于基线;Task 1 的源码形状闸门通过;**7 个**受保护文件哈希与基线一致;HEAD 与基线相同或 `baseline..HEAD` 只含 `.planning/`;全仓脏文件不出现新的范围外路径;**不做浏览器 UAT**"
|
||||
- "**范围与纪律**:不实现 `SettingSystem` / `SettingItem` 类、不写任何工厂、不写设置界面、不做存档注册、不在 `IClientBase` 上加 `setting` 字段、不改热键实现、不顺手修复任何无关诊断;发现疑似问题只记录、不擅自登记为缺陷"
|
||||
artifacts:
|
||||
- path: packages-user/client-base/src/setting/types.ts
|
||||
provides: "6 个导出 interface:`ISettingItem<T = unknown>`(`id` / `name` / `description` / `getValue` / `setValue` / `getPreviewString` / `getSettingComponent`,继承 `ISaveableContent<T>`)、`ISettingSubPageItem`(`ISettingItem<void>` + `readonly page: IGameUI`)、`ISettingItemGroup`(`name` + `readonly items: readonly ISettingItem[]`)、`ISettingSystemSave`(`readonly values: ReadonlyMap<string, unknown>`)、`ISettingSystem`(`groups` + 7 个方法,继承 `ISaveableContent<ISettingSystemSave>`)、`ISettingComponentProps<T = unknown>`(`readonly setting: ISettingItem<T>`);2 行普通导入;逐成员中文 jsDoc;零实现"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json
|
||||
provides: "Task 1 在收到「可以执行」之后、写入任何代码之前写出的机器可读基线:`{ head, hashes, dirty, typecheck, typecheckTotal }`;`hashes` 保护 **7 个**文件(`setting/index.ts` / `client-base/src/index.ts` / `client-base/src/types.ts` / `client-base/package.json` / `hotkey/types.ts` / `data-common/src/save/types.ts` / `system/src/ui/types.ts`)——`client-base/src/index.ts` 已导出 `./setting` 且本来就是脏文件,纳入哈希是为了证明本计划没有顺手改包级桶导出;`dirty` 是当时 `git status --porcelain -uall` 的全仓脏文件快照(去掉 `.planning/`);`typecheck` 是该文件 scope 内的归一化诊断数组;`typecheckTotal` 是整仓诊断条数;只写一次"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md
|
||||
provides: "执行摘要:6 个 interface 的最终形状与成员清单、两条设计取舍(设置项不带分组字段 / 不提供未分组枚举)、`ISettingItem<void>` 的 `page` 用法、`ReadonlyMap` 存档形状、jsDoc 与导入语法口径、真实门禁结果、未做浏览器 UAT 声明、估算未校准声明、执行器 0 次代码提交声明;**先跑完所有门禁再写本文件,写完后再跑摘要闸门**"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-33-PLAN.md
|
||||
provides: "本计划文件本身"
|
||||
key_links:
|
||||
- "`ISettingItem<T>` ↔ `ISaveableContent<T>` — 存档 / 读取方法完全继承 `@user/data-common` 的 `ISaveableContent<T>`(`saveState(compression): T` / `loadState(state, compression): void`);本文件只声明设置语义,不重复声明这两个方法,否则会出现签名漂移"
|
||||
- "`ISettingSubPageItem` ↔ `ISettingItem<void>` — 子页面项仍是一个设置项,`void` 使存档 / 读取在类型上即为空操作;`page: IGameUI` 只描述「进入哪个子页面」,本计划不实现打开逻辑"
|
||||
- "`ISettingSystem.groups` ↔ `ISettingItemGroup` — 分组 id 只存在于 `ReadonlyMap` 的键上,`ISettingItemGroup` 不带 id;设置项不带分组字段,因此「项 → 组」只能通过遍历 `groups` 反查,这是本设计的单向关系"
|
||||
- "`ISettingItem.getSettingComponent(): UIComponent` ↔ `@motajs/system` 的 `UIComponent` — 设置项只承诺「能给出一个组件」,不承诺组件 props 形状;组件侧契约由 `ISettingComponentProps<T>` 描述,两者本计划都只声明、不实现"
|
||||
- "`@motajs/system` / `@user/data-common` 导入 ↔ `client-base/package.json` 既有依赖 — 两个包已在 `dependencies` 中声明(`hotkey/types.ts` 已在使用同一组合),本计划不新增依赖;若类型检查报模块解析失败,说明写错了导入路径,不得改 package.json"
|
||||
- "受保护文件哈希(`setting/index.ts` / `client-base/src/index.ts` / `client-base/src/types.ts` / `client-base/package.json` / `hotkey/types.ts` / `data-common/src/save/types.ts` / `system/src/ui/types.ts`)— 证明本计划没有改设置桶导出、没有改包级桶导出、没有给 `IClientBase` 加字段、没有加依赖、没有动被参照的三个声明文件"
|
||||
- "`pnpm check:type` NEW-only ← 基线快照 — 该文件是新建的未跟踪文件,只有「改前 / 改后」同一棵树内对比才能证明「本计划没有引入新诊断」;全仓诊断总数同样只允许下降或持平"
|
||||
assumptions:
|
||||
- "**目标文件是空壳且无引用**:`packages-user/client-base/src/setting/types.ts` 当前仅 3 行(`export interface ISettingItem {}`、空行、`export interface ISettingSystem {}`),`setting/index.ts` 仅 1 行 `export * from './types';`;规划日全仓检索 `ISettingItem` / `ISettingSystem` 只命中该文件自身,无任何消费者,因此整文件替换不会破坏调用点"
|
||||
- "**两个依赖包的导出可用**:`packages/system/src/index.ts` → `./ui` → `./types` 导出 `IGameUI`(含 `C extends UIComponent = UIComponent` 默认泛型)与 `UIComponent`;`packages-user/data-common/src/index.ts` → `./save` → `./types` 导出 `ISaveableContent<T>`;`client-base/package.json` 已依赖 `@motajs/system` 与 `@user/data-common`;`hotkey/types.ts` 是同一导入组合的既有先例"
|
||||
- "**`void` 泛型实参合法**:`ISettingItem<void>` 与 `ISaveableContent<void>` 在 `strict` 模式下合法(`saveState` 返回 `void`、`loadState(state: void, …)`),这是「子页面项无自身存档值」的类型表达,本计划不写实现"
|
||||
- "**工作树仍是 09-32 之后的状态(历史记录,仅供参考)**:规划日观察到 `git status --porcelain -uall` 中已有多条未提交改动(`textbox.tsx`、`client-base/src/index.ts`、`map/*`、`src/client/ui/*`、以及未跟踪的 `setting/index.ts` / `setting/types.ts` / 两个闸门测试),规划日 HEAD 为 `e69dc0af06e4bcf0c836829ede9b448fa9d9098c`。**上述 HEAD 与脏文件枚举均为历史字面量,仅作背景说明(info-only),不是任何门禁的输入,也不要求与执行期一致**;所有门禁一律使用 `09-33-BASELINE.json` 中 Task 1 现场采集的活基线(live baseline)"
|
||||
- "**git 状态格式**:`git status --porcelain -uall` 每行前 3 个字符是状态列与分隔空格,其后为仓库相对路径(本仓无含空格的特殊路径,因此直接 `slice(3).trim()` 取路径);`.planning/` 条目在比较前统一剔除"
|
||||
- "**无 RESEARCH / 无 VALIDATION**:本 run 不存在 09-33-RESEARCH.md 与 09-33-VALIDATION.md;本阶段无 requirement ID,Nyquist 验证不适用,不创建这两类文件;本计划也不新增测试文件(用户明确「`?raw` 源码闸门对声明文件大概过重」,故闸门用 `node -e` 一次性脚本实现,不落盘为测试)"
|
||||
- "**dev.md 约束**:不得出现仅类型的导入写法;不得出现 `?.`(本文件也不会有);对象 / 成员类型一律先声明 interface;`//` 注释后留空格(本文件以 jsDoc 为主);导入之间不得夹注释与空行;换行 CRLF;Prettier `printWidth 80` / `tabWidth 4` / 单引号 / 无尾逗号(由 eslint 的 prettier 插件校验)"
|
||||
prohibitions:
|
||||
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(AGENTS.md / D-03)"
|
||||
- "**不得运行任何 git 写命令**:git add / commit / stash / checkout / restore / worktree / reset 一律禁止;代码改动留在工作树,是否提交由用户另行决定;三个 `.planning` 产物可能被编排器按 `commit_docs: true` 聚合提交,不属于本计划的代码提交"
|
||||
- "**不得修改 `packages-user/client-base/src/setting/index.ts`**(它已经导出 `./types`,本计划没有任何理由碰它)"
|
||||
- "**不得修改 `packages-user/client-base/src/types.ts`**:不在 `IClientBase` 上加 `setting` 字段、不改任何既有成员"
|
||||
- "**不得修改 `packages-user/client-base/src/hotkey/types.ts` / `packages-user/data-common/src/save/types.ts` / `packages/system/src/ui/types.ts`**:它们是设计参照与被继承接口的源头,不是本计划的改造对象"
|
||||
- "**不得修改 `packages-user/client-base/package.json`**:不新增依赖(`@motajs/system` / `@user/data-common` 已在依赖中)"
|
||||
- "**不得实现任何东西**:不写 `SettingSystem` / `SettingItem` 类、不写 `createXxx`、不写工厂、不写选项接口、不写设置界面、不做存档注册、不改任何 UI、不改入口"
|
||||
- "不得新增测试文件、不得跑 build:game / build:packages / build:lib、不得执行安装命令"
|
||||
- "不得越界处理发现的其它疑似问题;只记录、不擅自修复、不擅自登记为缺陷"
|
||||
open_items:
|
||||
- "**OI-1(命名报备,按 AGENTS.md 在 Task 0 一并报备)**:本计划新增的**公共命名**为 6 个 interface —— `ISettingItem`、`ISettingSubPageItem`、`ISettingItemGroup`、`ISettingSystemSave`、`ISettingSystem`、`ISettingComponentProps`;成员 / 方法 —— `id`、`name`、`description`、`getValue`、`setValue`、`getPreviewString`、`getSettingComponent`、`page`、`items`、`values`、`groups`、`setGroup`、`addItemToGroup`、`getGroup`、`getItem`、`iterateGroups`、`setting`;泛型参数 —— `T`。**以上全部来自用户本次派发中已批准的声明草案**,本计划不新增、不改名、不新增别的公共命名。若你要调整任一命名,执行前按你的命名整体替换,方案形态不变"
|
||||
- "**OI-2(文件替换授权)**:目标文件当前是 3 行空壳(两个空 interface,无注释、无实现)。本计划按你的指令**整文件替换**;这不涉及修改任何既有注释,但仍在此明确报备「旧的两个空 interface 声明会被删除」"
|
||||
- "**OI-3(执行期硬停条件)**:①若 `pnpm check:type` 在该文件内出现 NEW 诊断(例如 `void` 泛型不被接受、`IGameUI` / `UIComponent` / `ISaveableContent` 解析失败),停下上报,不得改依赖包、不得改 `tsconfig`、不得加 `@ts-expect-error` 来蒙过;②若整仓诊断总数高于基线,停下上报并定位是否由本文件引起;③若 eslint 报出 prettier 格式差异且无法在不改语义的前提下消除,停下上报;④若发现任何其它文件引用了 `ISettingItem` / `ISettingSystem`(与规划日检索结果不符),停下上报,因为那意味着本计划不再是纯类型变更"
|
||||
- "**OI-4(已知遗留,只记录、不修复)**:①设置系统本体、工厂、UI、存档注册、`IClientBase` 接线全部**未实现**,属后续计划;②`setting/index.ts` 与 `setting/types.ts` 目前都是未跟踪文件,本计划不改变这一状态;③工作树里 09-29..09-32 的改动仍未提交,范围闸门只能证明「没有新增范围外路径」"
|
||||
---
|
||||
|
||||
<!-- planner-discipline-allow: getUngrouped -->
|
||||
|
||||
<objective>
|
||||
Phase 9 · 第 33 个计划(新增的独立增量)——**把 `packages-user/client-base/src/setting/types.ts` 的两个空壳 interface 替换为用户已批准的 6 个类型声明(纯类型层,零实现)**。
|
||||
|
||||
**要解决的问题:**
|
||||
|
||||
`setting/types.ts` 现在只有 `export interface ISettingItem {}` 与 `export interface ISettingSystem {}` 两个空接口,`setting/index.ts` 也已经 `export * from './types';`,但接口内部没有任何成员。后续所有设置相关工作(设置系统实现、设置项工厂、设置界面、存档注册)都需要一个确定的数据契约,否则每个后续计划都要先自行发明一套字段,导致实现层漂移。本计划先把契约钉死。
|
||||
|
||||
**做法(用户已批准的声明草案,不得扩大):**
|
||||
|
||||
1. `ISettingItem<T = unknown> extends ISaveableContent<T>`:`id` / `name` / `description` 三个只读字段 + `getValue` / `setValue` / `getPreviewString` / `getSettingComponent` 四个方法。存档 / 读取完全继承 `ISaveableContent<T>`,不在本接口重复声明。
|
||||
2. `ISettingSubPageItem extends ISettingItem<void>`,额外带 `readonly page: IGameUI`:子页面项仍然是设置项,`void` 让它的存档 / 读取在类型上即为空操作。
|
||||
3. `ISettingItemGroup`:`name` + `readonly items: readonly ISettingItem[]`,与热键的 `IHotkeyCommandGroup`(`name` + `commands`)同形。
|
||||
4. `ISettingSystemSave`:`readonly values: ReadonlyMap<string, unknown>`,与热键存档(`ReadonlyMap`)同风格。
|
||||
5. `ISettingSystem extends ISaveableContent<ISettingSystemSave>`:`groups` + 7 个方法(`setGroup` / `addItemToGroup` / `getGroup` / `getItem` / `iterateGroups` / `getValue<T>` / `setValue<T>`)。
|
||||
6. `ISettingComponentProps<T = unknown>`:`readonly setting: ISettingItem<T>`。
|
||||
|
||||
**两条已定的设计取舍:**
|
||||
|
||||
- **设置项不带分组字段**:分组归属只存在于 `ISettingSystem.groups` 与 `ISettingItemGroup.items`,不搞「项上记一个 group」的双写。
|
||||
- **不提供“未分组”枚举方法**(`getUngrouped`):热键系统有未分组指令,设置项只能通过分组进入系统,没有「游离设置项」这一状态。
|
||||
|
||||
**明确不做:** 不实现 `ISettingSystem` / `ISettingItem` 的任何实现类、不写工厂、不写选项接口、不写设置界面、不做存档注册、不在 `IClientBase` 上加字段、不改 `setting/index.ts`、不改热键实现、不新增依赖、不新增测试文件。
|
||||
|
||||
**约束:** 只动 `files_modified` 的 4 个路径;逐成员中文 jsDoc;导入只有 2 行普通导入;文件内零实现。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(写基线、替换声明文件、写摘要)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。**Task 0 只汇报、不写任何文件**;`09-33-BASELINE.json` 是 **Task 1 在收到「可以执行」之后、写入任何代码之前的第一步**写出(只写一次)。「开始执行」的界限仍是「可以执行」四个字。
|
||||
|
||||
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md / 测试文件,Nyquist 验证不适用。
|
||||
</objective>
|
||||
|
||||
## 本计划产出物(Artifacts this plan produces)
|
||||
|
||||
| 类型 | 名称 / 路径 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 替换 | `packages-user/client-base/src/setting/types.ts` | 3 行空壳 → 6 个导出 interface(`ISettingItem` / `ISettingSubPageItem` / `ISettingItemGroup` / `ISettingSystemSave` / `ISettingSystem` / `ISettingComponentProps`)+ 2 行普通导入 + 逐成员中文 jsDoc;零实现 |
|
||||
| 基线 | `.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json` | `head` + **7 个**受保护文件哈希 + 全仓脏文件快照 + 该文件 scope 的诊断基线 + 整仓诊断总数 |
|
||||
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md` | 执行摘要、门禁结果、未 UAT / 0 次代码提交声明 |
|
||||
| 计划 | `.planning/phases/09-minimum-browser-runnable/09-33-PLAN.md` | 本文件 |
|
||||
|
||||
## 覆盖审计(Multi-Source Coverage Audit)
|
||||
|
||||
| 源 | 条目 | 覆盖位置 |
|
||||
|----|------|----------|
|
||||
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | 本增量只补设置系统的类型契约,不新增入口、不做浏览器验证;SUMMARY 如实记录 |
|
||||
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
|
||||
| RESEARCH | 本 run 无 RESEARCH.md / 本计划无 DISCOVERY.md | 不适用 |
|
||||
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该次派发的唯一计划,不拆分 |
|
||||
| CONTEXT D-02 | 计划可累积追加 | plan 33,排在 09-01..09-32 之后(`wave: 28` 只是排期编号) |
|
||||
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
|
||||
| CONTEXT D-04 | 新命名必须先反馈获批 | OI-1(6 个 interface + 全部成员 / 方法名,均来自用户已批准草案) |
|
||||
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 4 项 + 范围闸门 + **7 个**受保护文件哈希 |
|
||||
| 用户要求 1 | 仅类型声明,落点是 `setting/types.ts` | Task 1(唯一代码任务) |
|
||||
| 用户要求 2 | 6 个 interface 与成员签名逐字按草案 | Task 1 的 `need` 列表 + Task 2 形状闸门复检 |
|
||||
| 用户要求 3 | 设置项无分组字段、分组仅 `name` + `items`、无未分组枚举 | Task 1 块内负向断言(分组字段)+ `getUngrouped` 缺失断言 |
|
||||
| 用户要求 4 | `ISettingSubPageItem` 为 `ISettingItem<void>` + `page: IGameUI` | Task 1 的 `need` 列表 + 泛型实参断言 |
|
||||
| 用户要求 5 | 逐成员 jsDoc / 普通导入 / 不改 `index.ts` | Task 1 导入断言 + eslint(prettier)+ 受保护哈希(`setting/index.ts`、`client-base/src/index.ts`) |
|
||||
| 用户要求 6 | 不实现系统 / 工厂 / UI / 存档注册 / `IClientBase` 接线 | prohibitions + 受保护哈希(`client-base/src/types.ts`)+ 零实现闸门 |
|
||||
| 用户要求 7 | eslint + `pnpm check:type` NEW-only + 不新增 `?raw` 测试 + 无浏览器 UAT + 无提交 | Task 2 全部门禁 |
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
@packages-user/client-base/src/setting/types.ts
|
||||
@packages-user/client-base/src/setting/index.ts
|
||||
@packages-user/client-base/src/hotkey/types.ts
|
||||
@packages-user/data-common/src/save/types.ts
|
||||
@packages/system/src/ui/types.ts
|
||||
@packages-user/client-base/src/types.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「把 `setting/types.ts` 的两个空壳 interface 换成 6 个类型声明」的要做什么 / 为什么 / 怎么做 / 影响范围、逐 interface 成员清单、两条设计取舍与验证方式,等待「可以执行」</name>
|
||||
<precondition>工作树仍是 09-32 结束时的状态,可用以下事实自检:`packages-user/client-base/src/setting/types.ts` 共 3 行(`export interface ISettingItem {}`、空行、`export interface ISettingSystem {}`),且与 `setting/index.ts`(1 行 `export * from './types';`)一同处于**未跟踪**状态(`git status` 显示 `??`);规划日全仓检索 `ISettingItem` / `ISettingSystem` 只命中这两个空壳自身,没有任何消费者;`packages/system/src/index.ts` → `./ui` → `./types` 导出 `IGameUI<C extends UIComponent = UIComponent>` 与 `UIComponent`;`packages-user/data-common/src/index.ts` → `./save` → `./types` 导出 `ISaveableContent<T>`(含 `saveState(compression: SaveCompression): T` / `loadState(state: T, compression: SaveCompression): void`);`packages-user/client-base/package.json` 已依赖 `@motajs/system` 与 `@user/data-common`;`packages-user/client-base/src/hotkey/types.ts` 是「`@motajs/system` + `@user/data-common` 导入 + `name` / 列表分组 + `ReadonlyMap` 存档」的既有先例。</precondition>
|
||||
<files>无(本任务不写任何文件,只做只读汇报;`09-33-BASELINE.json` 改由 Task 1 在获批后、写代码前写出)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-05 验证按需、D-06 不扩大范围)
|
||||
- dev.md(注释规范 / 类型规范 / 普通导入 / 导出接口放 `types.ts` / CRLF)
|
||||
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得擅改、修改方式须预先声明)
|
||||
- packages-user/client-base/src/setting/types.ts 与 index.ts(本次要替换的文件与不得改动的桶导出)
|
||||
- packages-user/client-base/src/hotkey/types.ts(分组 / 存档 / 方法的既有写法参照)
|
||||
- packages-user/data-common/src/save/types.ts(`ISaveableContent<T>` 的签名)
|
||||
- packages/system/src/ui/types.ts(`IGameUI` / `UIComponent` 的定义与默认泛型)
|
||||
</read_first>
|
||||
<what-built>
|
||||
一份待批准的执行计划(`09-33-PLAN.md`):把 `packages-user/client-base/src/setting/types.ts` 里的两个空 interface 整文件替换为 6 个导出 interface(`ISettingItem<T>` / `ISettingSubPageItem` / `ISettingItemGroup` / `ISettingSystemSave` / `ISettingSystem` / `ISettingComponentProps<T>`),逐成员中文 jsDoc、2 行普通导入、零实现;并用 eslint + 类型基线 NEW-only + 源码形状闸门验收。
|
||||
</what-built>
|
||||
<action>
|
||||
**停下并向用户汇报,等待恰好回复「可以执行」后才继续;本任务不写任何文件(含 `09-33-BASELINE.json`——它由 Task 1 的第一步写出),不派发执行子代理。**
|
||||
|
||||
① **要做什么(一件事):** 把 `packages-user/client-base/src/setting/types.ts` 从 3 行空壳整文件替换为 6 个类型声明。**只声明类型,不写任何实现。**
|
||||
|
||||
② **为什么:** 该文件的 `ISettingItem` / `ISettingSystem` 现在是完全空的 interface,`index.ts` 已经导出它们,但没有任何成员。后续的设置系统实现、设置项工厂、设置界面、存档注册若各自发明字段,就会出现多套互不兼容的契约。本计划先把契约钉死在类型层,后续计划只在它之上实现,不改它。
|
||||
|
||||
③ **怎么写(逐 interface,严格按此范围):**
|
||||
- **导入(2 行,文件最开头,中间无空行无注释,无第三行):** 第 1 行从 `@user/data-common` 取 `ISaveableContent`;第 2 行从 `@motajs/system` 取 `IGameUI` 与 `UIComponent`(顺序与已批准草案一致)。全部使用普通导入写法(dev.md 禁止仅类型导入)。
|
||||
- **`ISettingItem<T = unknown> extends ISaveableContent<T>`(7 个成员):**
|
||||
- `readonly id: string`(设置项唯一标识)
|
||||
- `readonly name: string`(名称,用于显示)
|
||||
- `readonly description: string`(说明文本)
|
||||
- `getValue(): T`(获取当前值)
|
||||
- `setValue(value: T): void`(设置当前值,`@param value 新的设置值`)
|
||||
- `getPreviewString(): string`(获取当前值的预览文本)
|
||||
- `getSettingComponent(): UIComponent`(获取用于修改此设置项的组件)
|
||||
- **不声明**存档 / 读取方法(继承自 `ISaveableContent<T>`);**不带任何分组字段**。
|
||||
- **`ISettingSubPageItem extends ISettingItem<void>`(1 个新增成员):** `readonly page: IGameUI`(这个设置项进入的子页面)。`void` 让该设置项的存档 / 读取在类型上即为空操作;本计划不写实现、不写打开逻辑。
|
||||
- **`ISettingItemGroup`(2 个成员):** `readonly name: string` + `readonly items: readonly ISettingItem[]`。与热键的 `IHotkeyCommandGroup`(`name` + `commands`)同形;分组 id 只作为 `ISettingSystem.groups` 的键,不放进分组对象。
|
||||
- **`ISettingSystemSave`(1 个成员):** `readonly values: ReadonlyMap<string, unknown>`(键为设置项 id)。
|
||||
- **`ISettingSystem extends ISaveableContent<ISettingSystemSave>`(1 个字段 + 7 个方法):**
|
||||
- `readonly groups: ReadonlyMap<string, ISettingItemGroup>`(键为分组 id)
|
||||
- `setGroup(group: string, name: string, items: ISettingItem[]): void`(设置分组,不存在则创建)
|
||||
- `addItemToGroup(group: string, item: ISettingItem): void`(向分组追加设置项)
|
||||
- `getGroup(group: string): ISettingItemGroup | null`
|
||||
- `getItem(id: string): ISettingItem | null`
|
||||
- `iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>`(与热键同名同形)
|
||||
- `getValue<T>(id: string): T | null`
|
||||
- `setValue<T>(id: string, value: T): void`
|
||||
- 同样**不重复声明**存档 / 读取方法。
|
||||
- **`ISettingComponentProps<T = unknown>`(1 个成员):** `readonly setting: ISettingItem<T>`。
|
||||
- **jsDoc:** 每个 interface 的每个成员与方法都写中文 jsDoc;方法用换行风格(`/**` 换行写描述,参数用 `@param`),短成员用单行风格;方法之间空一行,字段之间不空行;**不写 interface 本身的 jsDoc**(dev.md「一般不建议给接口本身写注释」)。
|
||||
- **零实现:** 文件内只有 `import` 与 `interface` 声明;没有类、没有函数体、没有常量、没有箭头函数、没有默认导出。
|
||||
|
||||
④ **两条设计取舍,请重点确认:**
|
||||
- **设置项不带分组字段。** 分组归属只存在于 `ISettingSystem.groups` / `ISettingItemGroup.items`;不搞「项上记一个 group」的双写,避免设置项换组时出现两处不一致。
|
||||
- **不提供“未分组”枚举方法(`getUngrouped`)。** 热键系统有未分组指令,所以 `IClientHotkey` 有它;设置项只能通过分组进入系统,没有「游离设置项」这一状态,因此本设计不提供该方法。**若你希望保留一个未分组入口,请在回复中说明,我会加上。**
|
||||
- 另请确认:`ISettingSubPageItem` 用 `ISettingItem<void>`(存档 / 读取在类型上为空操作)而不是另立一个基接口;`ISettingSystemSave` 用 `ReadonlyMap` 而不是普通对象。
|
||||
|
||||
⑤ **影响范围:**
|
||||
- 只改 1 个源码文件:`packages-user/client-base/src/setting/types.ts`(当前未跟踪的空壳,整文件替换)。
|
||||
- 另有 3 个 `.planning` 产物:本计划、`09-33-BASELINE.json`、`09-33-SUMMARY.md`。合计 4 个路径。
|
||||
- **显式不改**:`packages-user/client-base/src/setting/index.ts`(已是 `export * from './types';`)、`packages-user/client-base/src/types.ts`(不加 `IClientBase.setting`)、`packages-user/client-base/package.json`(不加依赖)、`packages-user/client-base/src/hotkey/types.ts`、`packages-user/data-common/src/save/types.ts`、`packages/system/src/ui/types.ts`,以及任何 UI / 入口 / 配置。
|
||||
- **无下游破坏**:规划日全仓检索确认没有任何文件引用 `ISettingItem` / `ISettingSystem`,因此这次替换不会影响现有编译单元。
|
||||
|
||||
⑥ **命名报备(OI-1,AGENTS.md 要求先获批):** 新增公共命名为 6 个 interface(`ISettingItem` / `ISettingSubPageItem` / `ISettingItemGroup` / `ISettingSystemSave` / `ISettingSystem` / `ISettingComponentProps`)、成员 / 方法(`id` / `name` / `description` / `getValue` / `setValue` / `getPreviewString` / `getSettingComponent` / `page` / `items` / `values` / `groups` / `setGroup` / `addItemToGroup` / `getGroup` / `getItem` / `iterateGroups` / `setting`)、泛型参数 `T`。**全部来自你本次派发中给出的草案**;本计划不新增、不改名。若你要调整,请在回复中给出替换后的命名,我会整体替换后再执行。
|
||||
|
||||
⑦ **验证方式:**
|
||||
1. `pnpm exec eslint packages-user/client-base/src/setting/types.ts`——必须 0 报错(同时覆盖 prettier 格式:80 列 / 4 空格 / 单引号 / 无尾逗号 / CRLF)。
|
||||
2. `cmd /c "pnpm check:type"`(整仓 `vue-tsc --noEmit`;必须用 `cmd /c` 包装器)前后基线比较:**该文件内 NEW 诊断数必须为 0**,且整仓诊断总数不得高于基线(基线由 Task 1 在改代码前采集)。
|
||||
3. 源码形状闸门(`node -e`,不落盘为测试文件):6 个 `export interface`、逐条成员签名、`ISettingItem` 块内左括号计数 4、`ISettingSystem` 块内左括号计数 7、`ISettingItem` 块内无分组字段、无 `getUngrouped`、无实现体、导入为前两行且无第三行。
|
||||
4. 7 个受保护文件哈希 + HEAD 政策 + 全仓脏文件范围闸门(证明没有改设置桶导出 / 包级桶导出 / `IClientBase` / 依赖 / 三个参照声明文件,也没有范围外写入)。
|
||||
5. **不新增测试文件**(声明文件用形状闸门已足够,`?raw` 测试对纯声明过重);**不做浏览器 UAT**。
|
||||
|
||||
⑧ **两条铁律:** **不做浏览器 UAT、不宣称任何设置系统 / 界面 / 存档行为已验证**(本计划没有任何运行期行为);**执行器不提交任何代码**(禁止 git add / commit / stash / checkout / worktree / reset),代码改动留在工作树供你审查。三个 `.planning` 产物可能由 GSD 编排器按 `commit_docs: true` 聚合提交,不算本计划的代码提交。
|
||||
</action>
|
||||
<how-to-verify>
|
||||
逐条核对 ①–⑧ 是否与你的意图一致,特别是:①6 个 interface 的成员签名与草案是否逐字一致(`ISettingItem` 的 7 个成员、`ISettingSystem` 的 `groups` + 7 个方法、`ISettingComponentProps` 的 `setting`);②「设置项不带分组字段」与「不提供未分组枚举」这两条取舍是否认可;③`ISettingSubPageItem` 用 `ISettingItem<void>` + `readonly page: IGameUI`、`ISettingSystemSave` 用 `ReadonlyMap<string, unknown>` 是否认可;④jsDoc 口径(逐成员中文注释、方法换行风格、无接口级注释)是否可接受;⑤范围是否只有这 4 个路径、`setting/index.ts` / `client-base/src/types.ts` / `package.json` 是否确认不动;⑥命名清单(OI-1)是否全部按你的草案、无需替换;⑦验证口径(eslint 单独跑、`pnpm check:type` NEW-only + 总数不增、形状闸门、7 个受保护哈希、不新增测试文件、无浏览器 UAT、不提交代码)是否就是你要的。
|
||||
</how-to-verify>
|
||||
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(**尤其是 ④ 的两条设计取舍、⑤ 的范围、⑥ 的命名**)请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 写出改动前基线,然后把 `setting/types.ts` 整文件替换为 6 个类型声明(逐成员 jsDoc、2 行普通导入、零实现)</name>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json(新增,第一步写出), packages-user/client-base/src/setting/types.ts</files>
|
||||
<action>
|
||||
**依赖 Task 0 已收到「可以执行」,且命名(OI-1)与「整文件替换」(OI-2)均已获批。**
|
||||
|
||||
**第一步(必须在写入任何代码之前):** 写出 `.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json`,schema 为 `{ head, hashes, dirty, typecheck, typecheckTotal }`:
|
||||
- `head`:当前 `git rev-parse HEAD`。
|
||||
- `hashes`:**7 个**受保护文件的 `git hash-object`——`packages-user/client-base/src/setting/index.ts`、`packages-user/client-base/src/index.ts`、`packages-user/client-base/src/types.ts`、`packages-user/client-base/package.json`、`packages-user/client-base/src/hotkey/types.ts`、`packages-user/data-common/src/save/types.ts`、`packages/system/src/ui/types.ts`;键用完整仓库相对路径(POSIX 正斜杠),值小写 sha。`client-base/src/index.ts` 已导出 `./setting` 且本就是脏文件,纳入保护是为了证明本计划没有动包级桶导出。
|
||||
- `dirty`:`git status --porcelain -uall`(**不加 pathspec**),每行取前 3 个字符之后的路径并 `trim()`,剔除 `.planning/` 开头的条目,按字典序写入。**注意:09-29..09-32 的未提交改动与未跟踪的 `setting/index.ts` / `setting/types.ts` / 两个闸门测试会出现在这份快照里;这是活基线(live baseline),不是异常。此处列举仅作示意(历史背景),实际内容一律以现场 `git status` 输出为准,与本计划文件中的任何历史 HEAD / 脏文件字面量无关。**
|
||||
- `typecheck`:整仓跑 `cmd /c "pnpm check:type"`(本机为 Windows / PowerShell,必须用与 verify 块相同的 `cmd /c` 包装器;裸 `pnpm check:type` 会因执行策略失败),把输出中匹配 `error TS` 加数字的行按归一化口径处理——反斜杠转正斜杠、去掉 `(行,列):` 片段、截取到首个 `src/` 或 `packages-user/` 起的部分(与 Task 2 逐字相同)——**只保留以 `packages-user/client-base/src/setting/types.ts` 开头的条目**,按行写入数组(保留重复项与顺序)。
|
||||
- `typecheckTotal`:同一份输出中匹配 `error TS` 加数字的**总行数**。
|
||||
- 采集时:`check:type` 可能以非零退出(工作树本就有既有诊断),只要输出可解析就照常采集;只有未运行 / 输出为空 / 无法解析出任何诊断行时才停下上报。**本文件只写一次,此后不得重写。**
|
||||
|
||||
**第二步:`packages-user/client-base/src/setting/types.ts`(整文件替换;该文件当前只有两个空 interface、无注释、无实现,属用户批准的替换)。**
|
||||
|
||||
1. 第 1 行为 `import { ISaveableContent } from '@user/data-common';`,第 2 行为 `import { IGameUI, UIComponent } from '@motajs/system';`(与已批准草案的顺序一致)。两行之间无空行、无注释,其后不得再有第三行导入。全部为普通导入(dev.md 禁止仅类型导入)。
|
||||
2. 空一行后声明 `ISettingItem<T = unknown>`,`extends ISaveableContent<T>`。成员顺序固定:
|
||||
- `readonly id: string`,jsDoc:设置项的唯一标识
|
||||
- `readonly name: string`,jsDoc:设置项的名称,用于显示
|
||||
- `readonly description: string`,jsDoc:设置项的说明文本
|
||||
- 空一行,然后 `getValue(): T`,jsDoc:获取当前设置项的值
|
||||
- 空一行,然后 `setValue(value: T): void`,jsDoc:设置当前设置项的值 + `@param value 新的设置值`
|
||||
- 空一行,然后 `getPreviewString(): string`,jsDoc:获取当前设置值的预览文本
|
||||
- 空一行,然后 `getSettingComponent(): UIComponent`,jsDoc:获取用于修改此设置项的组件
|
||||
- **不得**出现存档 / 读取方法,**不得**出现任何分组字段。
|
||||
3. 空一行后声明 `ISettingSubPageItem`,`extends ISettingItem<void>`,唯一新增成员 `readonly page: IGameUI`,jsDoc:这个设置项进入的子页面。
|
||||
4. 空一行后声明 `ISettingItemGroup`,成员 `readonly name: string`(jsDoc:设置分组的名称)与 `readonly items: readonly ISettingItem[]`(jsDoc:分组包含的设置项列表);两者之间不空行。
|
||||
5. 空一行后声明 `ISettingSystemSave`,唯一成员 `readonly values: ReadonlyMap<string, unknown>`,jsDoc:所有设置项的值,键为设置项 id。
|
||||
6. 空一行后声明 `ISettingSystem`,`extends ISaveableContent<ISettingSystemSave>`。成员顺序固定:`readonly groups: ReadonlyMap<string, ISettingItemGroup>`(jsDoc:所有设置分组,键为分组 id)→ 空一行 → `setGroup(group: string, name: string, items: ISettingItem[]): void`(jsDoc 描述 + `@param group 设置分组 id` / `@param name 设置分组名称,用于显示` / `@param items 分组中包含的设置项`)→ 空一行 → `addItemToGroup(group: string, item: ISettingItem): void`(jsDoc + `@param group 设置分组 id` / `@param item 要添加的设置项`)→ 空一行 → `getGroup(group: string): ISettingItemGroup | null`(jsDoc + `@param group 设置分组 id`)→ 空一行 → `getItem(id: string): ISettingItem | null`(jsDoc + `@param id 设置项 id`)→ 空一行 → `iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>`(jsDoc:迭代所有的设置分组)→ 空一行 → `getValue<T>(id: string): T | null`(jsDoc + `@param id 设置项 id`)→ 空一行 → `setValue<T>(id: string, value: T): void`(jsDoc + `@param id 设置项 id` / `@param value 新的设置值`)。**不得**重复声明存档 / 读取方法,**不得**新增第 8 个方法。
|
||||
7. 空一行后声明 `ISettingComponentProps<T = unknown>`,唯一成员 `readonly setting: ISettingItem<T>`,jsDoc:当前设置组件对应的设置项。
|
||||
8. jsDoc 口径:方法一律换行风格(`/**`、描述行、可选 `@param` 行、`*/`),短成员用单行 `/** … */`;**不写任何 interface 本身的 jsDoc**;不写非 jsDoc 的多行注释;不写行注释。
|
||||
9. 零实现:文件内不出现类、函数体、常量、箭头函数、默认导出;不新增依赖;不改 `setting/index.ts`;不新增测试文件。
|
||||
10. 缩进 4 空格、单引号、无尾逗号、CRLF;每行不超过 80 列(Prettier 口径)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const b='.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json';const need=['packages-user/client-base/src/setting/index.ts','packages-user/client-base/src/index.ts','packages-user/client-base/src/types.ts','packages-user/client-base/package.json','packages-user/client-base/src/hotkey/types.ts','packages-user/data-common/src/save/types.ts','packages/system/src/ui/types.ts'];const bad=[];if(!fs.existsSync(b))bad.push('baseline missing');else{let j={};try{j=JSON.parse(fs.readFileSync(b,'utf8'))}catch(e){bad.push('baseline unparsable')}for(const k of ['head','hashes','dirty','typecheck','typecheckTotal'])if(!(k in j))bad.push('missing '+k);const ks=Object.keys(j.hashes||{});if(ks.length!==7)bad.push('hashes must have 7 entries, got '+ks.length);for(const f of need)if(!Object.prototype.hasOwnProperty.call(j.hashes||{},f))bad.push('hashes missing '+f);if(!Array.isArray(j.dirty))bad.push('dirty must be an array');if(!Array.isArray(j.typecheck))bad.push('typecheck baseline must be an array');if(typeof j.typecheckTotal!=='number')bad.push('typecheckTotal must be a number')}if(bad.length){console.error('TASK1 BASELINE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: baseline')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const p='packages-user/client-base/src/setting/types.ts';const raw=fs.readFileSync(p,'utf8');const code=raw.replace(/\/\*[^]*?\*\//g,'');const f=code.replace(/\s+/g,' ');const bad=[];const lines=code.split(/\r?\n/).map(l=>l.trim()).filter(l=>l.length>0);if(lines[0].indexOf('import {')!==0||lines[0].indexOf('ISaveableContent')<0||lines[0].indexOf('data-common')<0)bad.push('first import must be @user/data-common with ISaveableContent');if(lines[1].indexOf('import {')!==0||lines[1].indexOf('IGameUI')<0||lines[1].indexOf('UIComponent')<0)bad.push('second import must be @motajs/system with IGameUI and UIComponent');if(lines[2]&&lines[2].indexOf('import ')>-1)bad.push('unexpected third import');if(raw.indexOf('import type')>=0)bad.push('type-only import is forbidden');if((f.match(/export /g)||[]).length!==6)bad.push('exactly 6 exports required, got '+(f.match(/export /g)||[]).length);if((f.match(/export interface /g)||[]).length!==6)bad.push('exactly 6 exported interfaces required');if(/\)\s*\{/.test(f))bad.push('implementation body found');if(f.indexOf('getUngrouped')>=0)bad.push('getUngrouped must not be declared');const need=['export interface ISettingItem<T = unknown> extends ISaveableContent<T> {','readonly id: string;','readonly name: string;','readonly description: string;','getValue(): T;','setValue(value: T): void;','getPreviewString(): string;','getSettingComponent(): UIComponent;','export interface ISettingSubPageItem extends ISettingItem<void> {','readonly page: IGameUI;','export interface ISettingItemGroup {','readonly items: readonly ISettingItem[];','export interface ISettingSystemSave {','readonly values: ReadonlyMap<string, unknown>;','export interface ISettingSystem extends ISaveableContent<ISettingSystemSave> {','readonly groups: ReadonlyMap<string, ISettingItemGroup>;','setGroup(group: string, name: string, items: ISettingItem[]): void;','addItemToGroup(group: string, item: ISettingItem): void;','getGroup(group: string): ISettingItemGroup | null;','getItem(id: string): ISettingItem | null;','iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>;','getValue<T>(id: string): T | null;','setValue<T>(id: string, value: T): void;','export interface ISettingComponentProps<T = unknown> {','readonly setting: ISettingItem<T>;'];for(const n of need)if(f.indexOf(n)<0)bad.push('missing :: '+n);const u=f.indexOf('export interface ISettingSubPageItem extends ISettingItem<void> {');const g=f.indexOf('export interface ISettingItemGroup {');if(u<0||g<0||g<u)bad.push('cannot slice ISettingSubPageItem');else{const block=f.slice(u,g);if(block.indexOf('readonly page: IGameUI;')<0)bad.push('ISettingSubPageItem must declare page');if((block.match(/\(/g)||[]).length!==0)bad.push('ISettingSubPageItem must not declare methods')}const gg=f.indexOf('export interface ISettingSystemSave {');if(g<0||gg<0||gg<g)bad.push('cannot slice ISettingItemGroup');else{const block=f.slice(g,gg);if(block.indexOf('readonly name: string;')<0)bad.push('ISettingItemGroup must declare name');if(block.indexOf('readonly items: readonly ISettingItem[];')<0)bad.push('ISettingItemGroup must declare items');if((block.match(/\(/g)||[]).length!==0)bad.push('ISettingItemGroup must not declare methods')}const p1=f.indexOf('export interface ISettingComponentProps<T = unknown> {');if(p1<0)bad.push('cannot slice ISettingComponentProps');else{const block=f.slice(p1);if(block.indexOf('readonly setting: ISettingItem<T>;')<0)bad.push('ISettingComponentProps must declare setting');if((block.match(/\(/g)||[]).length!==0)bad.push('ISettingComponentProps must not declare methods')}const a=f.indexOf('export interface ISettingItem<T = unknown> extends ISaveableContent<T> {');const z=f.indexOf('export interface ISettingSubPageItem extends ISettingItem<void> {');if(a<0||z<0||z<a)bad.push('cannot slice ISettingItem');else{const block=f.slice(a,z);if(/readonly\s+group\b/.test(block))bad.push('ISettingItem must not declare a group member');const np=(block.match(/\(/g)||[]).length;if(np!==4)bad.push('ISettingItem must declare exactly 4 methods, got '+np)}const s=f.indexOf('export interface ISettingSystem extends ISaveableContent<ISettingSystemSave> {');const e=f.indexOf('export interface ISettingComponentProps<T = unknown> {');if(s<0||e<0||e<s)bad.push('cannot slice ISettingSystem');else{const np=(f.slice(s,e).match(/\(/g)||[]).length;if(np!==7)bad.push('ISettingSystem must declare exactly 7 methods, got '+np)}if(bad.length){console.error('TASK1 SHAPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: declarations')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint packages-user/client-base/src/setting/types.ts"</automated>
|
||||
<fails_when>基线缺失或字段残缺、`hashes` 不是 7 条(含 `client-base/src/index.ts`)、`dirty` / `typecheck` 不是数组、`typecheckTotal` 不是数字;`types.ts` 的前两行不是指定的两行普通导入、出现第三行导入、出现仅类型导入;`export` 或 `export interface` 计数不是 6;出现 `) {` 形式的实现体;出现 `getUngrouped`;任一条成员签名缺失;`ISettingItem` 块内左括号计数不是 4(多出存档 / 读取方法或多加了方法)、`ISettingSystem` 块内左括号计数不是 7;`ISettingItem` 块内出现分组成员;eslint 报错;或任一 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`09-33-BASELINE.json` 已在写代码前写出且只写一次(HEAD + 7 个受保护哈希 + 全仓脏文件快照 + 该文件 scope 的 typecheck 基线 + 整仓诊断总数);`setting/types.ts` 已整文件替换为 6 个导出 interface(`ISettingItem<T>` / `ISettingSubPageItem` / `ISettingItemGroup` / `ISettingSystemSave` / `ISettingSystem` / `ISettingComponentProps<T>`),成员签名与设计逐字一致,`ISettingItem` 无分组字段、系统无未分组枚举方法、两处存档接口均为继承而非重复声明;2 行普通导入在文件最前;逐成员中文 jsDoc;零实现;eslint 0 报错。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 先跑全部门禁(eslint / `pnpm check:type` NEW-only / 形状复检 / 7 个受保护哈希 / HEAD / 范围),再写执行摘要,最后跑摘要闸门</name>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md</files>
|
||||
<action>
|
||||
**依赖 Task 1 已完成。**
|
||||
|
||||
**A. 先跑完所有门禁(此时还没有 SUMMARY):** 逐条执行下方 automated 命令并记录**实际**输出。
|
||||
- eslint:`pnpm exec eslint packages-user/client-base/src/setting/types.ts`,要求退出码 0、无任何报错(含 prettier 格式)。
|
||||
- `cmd /c "pnpm check:type"`:整仓运行(必须用 `cmd /c` 包装器,与 verify 块一致),按**与 Task 1 基线逐字相同的归一化口径与同一 scope**(`packages-user/client-base/src/setting/types.ts`)过滤,只判 NEW;基线中存在、执行后消失的诊断允许;同时要求整仓诊断总数不高于 `typecheckTotal`。
|
||||
- 形状复检:重跑源码形状闸门(下方第二条 automated,独立于 eslint / 类型检查)。
|
||||
- 受保护文件哈希(**7 个**)与基线逐字相等;HEAD 与基线相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径。
|
||||
- 范围(两条都要):①`git status --porcelain -uall -- packages-user/client-base/src/setting/types.ts` 只出现该路径;②整仓 `git status --porcelain -uall` 的每条路径必须属于该目标文件、或出现在基线 dirty 快照、或以 `.planning/` 开头,否则判范围外写入。
|
||||
- **顺序铁律**:先跑门禁,**再**写 SUMMARY;不得在门禁实际运行之前把结果写进 SUMMARY。
|
||||
|
||||
**B. 写 `.planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md`(用 A 步的真实结果):**
|
||||
- 6 个 interface 的最终形状与成员清单(逐条列出签名),并说明 `ISettingItem` / `ISettingSystem` 的存档 / 读取方法来自 `ISaveableContent<T>` 继承、未重复声明。
|
||||
- 两条设计取舍的落地:设置项不带分组字段(分组归属只在 `ISettingSystem.groups` / `ISettingItemGroup.items`)、不提供未分组枚举方法(设置项只能经分组进入);**SUMMARY 必须写明不提供 `getUngrouped`(该 token 由摘要闸门断言,须逐字出现)**。
|
||||
- `ISettingSubPageItem` 用 `ISettingItem<void>` + `readonly page: IGameUI` 的语义(`void` 即「无自身存档值」,类型层空操作)。
|
||||
- `ISettingSystemSave` 用 `ReadonlyMap<string, unknown>` 的形状与理由(键为设置项 id,与热键存档同风格)。
|
||||
- 导入口径(2 行普通导入、无仅类型导入)、jsDoc 口径(逐成员中文、方法换行风格、无接口级注释)、文件为整文件替换(旧的两个空 interface 已删除,属用户批准);**SUMMARY 必须写明落点文件为 `setting/types.ts`(该 token 由摘要闸门断言,须逐字出现)**。
|
||||
- 记录 A 步的真实门禁结果(eslint 退出码、`pnpm check:type` 该文件 NEW 数与整仓总数、形状闸门、7 个受保护哈希、HEAD 政策、范围闸门)。
|
||||
- 声明:`estimate` 未校准(`confidence: low`);**不做浏览器 UAT,不宣称任何设置系统 / 界面 / 存档行为已验证**(本计划没有任何运行期行为);工作树里 09-29..09-32 的改动仍未提交、这些路径在基线里本来就是脏的,范围闸门只证明「无新增范围外路径」;**执行器 0 次代码提交**(`.planning` 产物可能由编排器按 `commit_docs: true` 聚合提交),代码工作树留给用户审查。
|
||||
- 声明:设置系统本体、工厂、UI、存档注册、`IClientBase` 接线**均未实现**,属后续计划(不在本计划范围)。
|
||||
|
||||
**C. 最后跑摘要闸门:** 确认 SUMMARY 存在且包含上述必需声明(见下方最后一条 automated);该闸门必须在 B 步之后运行,失败则补齐 SUMMARY 后重跑,不得只改闸门。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec eslint packages-user/client-base/src/setting/types.ts"</automated>
|
||||
<automated>node -e "const fs=require('fs');const p='packages-user/client-base/src/setting/types.ts';const raw=fs.readFileSync(p,'utf8');const code=raw.replace(/\/\*[^]*?\*\//g,'');const f=code.replace(/\s+/g,' ');const bad=[];if((f.match(/export interface /g)||[]).length!==6)bad.push('expected exactly 6 exported interfaces, got '+(f.match(/export interface /g)||[]).length);if(/\)\s*\{/.test(f))bad.push('implementation body found');if(f.indexOf('getUngrouped')>=0)bad.push('getUngrouped must not be declared');for(const n of ['export interface ISettingItem<T = unknown> extends ISaveableContent<T> {','export interface ISettingSubPageItem extends ISettingItem<void> {','export interface ISettingItemGroup {','readonly name: string;','readonly items: readonly ISettingItem[];','export interface ISettingSystemSave {','readonly values: ReadonlyMap<string, unknown>;','export interface ISettingSystem extends ISaveableContent<ISettingSystemSave> {','readonly groups: ReadonlyMap<string, ISettingItemGroup>;','setGroup(group: string, name: string, items: ISettingItem[]): void;','addItemToGroup(group: string, item: ISettingItem): void;','getGroup(group: string): ISettingItemGroup | null;','getItem(id: string): ISettingItem | null;','iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>;','getValue<T>(id: string): T | null;','setValue<T>(id: string, value: T): void;','readonly page: IGameUI;','export interface ISettingComponentProps<T = unknown> {','readonly setting: ISettingItem<T>;'])if(f.indexOf(n)<0)bad.push('missing :: '+n);if(bad.length){console.error('TASK2 SHAPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task2: shape')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json','utf8'));const scope='packages-user/client-base/src/setting/types.ts';const r=cp.spawnSync('cmd',['/c','pnpm check:type'],{encoding:'utf8',timeout:900000});const text=(r.stdout||'')+(r.stderr||'');const diag=text.split(/\r?\n/).filter(l=>/error TS[0-9]+/.test(l));const bad=[];if(r.status===null)bad.push('vue-tsc did not run');else if(r.status!==0&&diag.length===0)bad.push('vue-tsc crashed with no parseable diagnostics');const norm=l=>l.replace(/\\/g,'/').replace(/\([0-9]+,[0-9]+\):/,'').replace(/^.*?(?=(src|packages-user)\/)/,'').trim();const cur=diag.map(norm).filter(x=>x.indexOf(scope)===0);const before=(b.typecheck||[]).filter(x=>x.indexOf(scope)===0);const count=(a,v)=>{let n=0;for(const x of a)if(x===v)n++;return n};for(const v of new Set(cur)){if(count(cur,v)>count(before,v))bad.push('NEW diagnostic :: '+v)}if(diag.length>(b.typecheckTotal||0))bad.push('total diagnostics increased :: '+diag.length+' > '+b.typecheckTotal);if(bad.length){console.error('TYPECHECK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK typecheck: '+cur.length+' target diagnostics, '+diag.length+' total')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json','utf8'));const bad=[];const keys=Object.keys(b.hashes||{});if(keys.length!==7)bad.push('baseline must protect 7 files, got '+keys.length);for(const file of keys){const r=cp.spawnSync('git',['hash-object',file],{encoding:'utf8'});if(r.status!==0)bad.push('cannot hash '+file);else if((r.stdout||'').trim()!==b.hashes[file])bad.push(file+' changed')}const head=(cp.spawnSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).stdout||'').trim();if(head!==b.head){const d=cp.spawnSync('git',['diff','--name-only',b.head+'..'+head],{encoding:'utf8'});if(d.status!==0)bad.push('HEAD changed and diff failed');else{const nonPlan=(d.stdout||'').split(/\r?\n/).filter(Boolean).filter(p=>!p.startsWith('.planning/'));if(nonPlan.length)bad.push('HEAD changed with non-planning commits :: '+nonPlan.join(','))}}if(bad.length){console.error('PROTECTED GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK protected files unchanged; HEAD policy satisfied')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-33-BASELINE.json','utf8'));const target='packages-user/client-base/src/setting/types.ts';const bad=[];if(!Array.isArray(b.dirty))bad.push('baseline dirty snapshot missing');const out=cp.spawnSync('git',['status','--porcelain','-uall'],{encoding:'utf8'}).stdout||'';const paths=out.split(/\r?\n/).filter(Boolean).map(l=>l.slice(3).trim()).filter(p=>p.indexOf('.planning/')!==0);const before=new Set((b.dirty||[]).map(p=>String(p).replace(/\\/g,'/')));for(const p of paths)if(p!==target&&!before.has(p))bad.push('out-of-scope write :: '+p);if(!paths.includes(target))bad.push('target file not modified');if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: no out-of-scope writes ('+paths.length+' dirty paths total, '+before.size+' pre-existing)')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const p='.planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md';const bad=[];if(!fs.existsSync(p))bad.push('summary missing');else{const s=fs.readFileSync(p,'utf8');for(const n of ['types.ts','ISettingItem','ISettingSubPageItem','ISettingItemGroup','ISettingSystemSave','ISettingSystem','ISettingComponentProps','ISaveableContent','IGameUI','UIComponent','ReadonlyMap','getUngrouped','eslint','check:type','confidence: low','UAT','0 次代码提交'])if(s.indexOf(n)<0)bad.push('summary missing :: '+n)}if(bad.length){console.error('SUMMARY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK summary content')"</automated>
|
||||
<fails_when>eslint 退出码非 0 或有任何报错;`pnpm check:type` 在该文件内出现 NEW 诊断,或整仓诊断总数高于基线(归一化口径与 scope 必须与 Task 1 逐字相同);7 个受保护文件任一哈希变化(`setting/index.ts` / `client-base/src/index.ts` / `client-base/src/types.ts` / `client-base/package.json` / `hotkey/types.ts` / `data-common/src/save/types.ts` / `system/src/ui/types.ts`);HEAD 变化且 `baseline..HEAD` 含非 `.planning` 路径;目标文件未出现在脏文件列表;整仓脏文件里出现既不在基线 dirty 快照也不属于目标文件、也不在 `.planning/` 的新路径;`09-33-SUMMARY.md` 缺失或缺少必需声明;或任一 node 闸门非零退出。**顺序错误(先写 SUMMARY 再跑门禁、或未运行就记录结果)同样判失败。**</fails_when>
|
||||
</verify>
|
||||
<done>eslint 对 `setting/types.ts` 0 报错;`pnpm check:type` 该文件内无 NEW 诊断且整仓诊断总数不高于基线;形状闸门复检通过;7 个受保护文件哈希与基线一致;HEAD 与基线相同或仅含 `.planning` 聚合提交;范围闸门确认只改了目标文件且无新增范围外路径;**先跑完门禁、再写 `09-33-SUMMARY.md`、最后通过摘要闸门**,摘要如实声明 6 个 interface 形状、两条设计取舍、`ISettingItem<void>` 的 `page` 语义、`ReadonlyMap` 存档形状、jsDoc / 导入口径、未做浏览器 UAT、估算未校准、执行器 0 次代码提交、系统本体未实现。</done>
|
||||
</task>
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| 本文件 ↔ `ISaveableContent<T>` | 存档 / 读取契约由 `@user/data-common` 定义;本文件只做类型继承,一旦重复声明就可能与源头签名漂移 |
|
||||
| `ISettingSystem.groups` ↔ 设置项 | 分组是设置项唯一的归属路径;分组 id 只在 `ReadonlyMap` 键上,`ISettingItemGroup` 不带 id |
|
||||
| `ISettingItem<void>` ↔ 子页面项 | `void` 把「无自身存档值」编码进类型;若改成 `unknown` 或另立基接口,后续实现的存档语义会变 |
|
||||
| `IGameUI` / `UIComponent` ↔ `@motajs/system` | 组件契约由系统层定义;本文件只引用,不自定义替代类型 |
|
||||
| 声明文件 ↔ 下游实现 | 本计划是后续全部设置相关计划的契约来源;字段 / 方法被随意增删会让后续实现集体返工 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-33-01 | Tampering | `ISettingItem` 成员集合 | high | mitigate | Task 1 逐条断言 7 个成员签名,并用块内左括号计数 4 证明没有多声明(含继承方法)或漏声明;Task 2 复检同一闸门 |
|
||||
| T-09-33-02 | Tampering | 设置项的分组归属 | medium | mitigate | Task 1 对 `ISettingItem` 块做分组成员负向断言;分组只允许经 `ISettingSystem.groups` / `ISettingItemGroup.items` 表达 |
|
||||
| T-09-33-03 | Tampering | 存档契约被重复声明或改写 | high | mitigate | 断言 `ISettingItem` / `ISettingSystem` 均 `extends ISaveableContent<…>`,且块内方法计数为 4 / 7(不含存档 / 读取);不修改 `data-common/src/save/types.ts`(受保护哈希) |
|
||||
| T-09-33-04 | Tampering | 借「补类型」之名加实现 / 加依赖 / 接线 | high | mitigate | 断言 `export` 计数恰为 6 且全为 `export interface`、无 `) {` 实现体;7 个受保护文件哈希覆盖设置桶导出、包级桶导出(`client-base/src/index.ts`)、`IClientBase`、`package.json` 与三个参照声明文件;范围闸门拒绝任何新增路径 |
|
||||
| T-09-33-05 | Repudiation | 声称「类型 / 系统已可用」但实际只有声明 | medium | mitigate | Task 0 与 SUMMARY 必须写明「只声明、零实现,系统本体 / 工厂 / UI / 存档注册 / `IClientBase` 接线均未实现」;不做浏览器 UAT 声明 |
|
||||
| T-09-33-06 | Elevation of Privilege | 改依赖包 / `tsconfig` / 加 `@ts-expect-error` 来让类型检查变绿 | high | mitigate | OI-3 规定遇到 NEW 诊断必须停下上报;prohibitions 禁止改 `tsconfig` / 依赖 / 用指令蒙过;受保护哈希覆盖依赖声明文件 |
|
||||
| T-09-33-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划**不新增任何依赖、不执行安装命令**;`@motajs/system` / `@user/data-common` 已在 `client-base/package.json` 中;若执行期出现安装需求,必须停下上报 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **静态检查(源码)**:`pnpm exec eslint packages-user/client-base/src/setting/types.ts` 退出码 0(含 prettier 的 80 列 / 4 空格 / 单引号 / 无尾逗号 / CRLF)。
|
||||
- **静态检查(类型)**:`cmd /c "pnpm check:type"` 整仓运行后在 `packages-user/client-base/src/setting/types.ts` 内 **NEW 诊断数为 0**(归一化口径与 scope 与 Task 1 基线逐字相同),且整仓诊断总数不高于基线。
|
||||
- **源码形状**:Task 1 / Task 2 的 `node -e` 闸门通过——2 行普通导入在文件最前且无第三行导入;`export` 与 `export interface` 计数均为 6;无 `) {` 实现体;无未分组枚举方法;25 条必需签名全部命中;`ISettingItem` 块内左括号 4 且无分组成员;`ISettingSystem` 块内左括号 7;`ISettingSubPageItem` / `ISettingItemGroup` / `ISettingComponentProps` 三块内左括号均为 0(无方法),且分别含 `page`、`name` + `items`、`setting`。
|
||||
- **范围**:`git status --porcelain -uall -- packages-user/client-base/src/setting/types.ts` 只出现该路径;整仓 `git status --porcelain -uall` 里没有「不在基线 dirty 快照、也不是目标文件、也不在 `.planning/`」的新路径;**7 个**受保护文件(`setting/index.ts` / `client-base/src/index.ts` / `client-base/src/types.ts` / `client-base/package.json` / `hotkey/types.ts` / `data-common/src/save/types.ts` / `system/src/ui/types.ts`)哈希与基线一致。
|
||||
- **无代码提交**:HEAD 与 `09-33-BASELINE.json` 的 `head` 相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径;任何非 `.planning` 路径的提交都判失败。
|
||||
- **无浏览器验证声明**:SUMMARY 明确写入「未做浏览器 UAT」;并写明设置系统本体、工厂、UI、存档注册、`IClientBase` 接线均未实现。
|
||||
- **执行顺序**:全部门禁(eslint / check:type / 形状复检 / 哈希 / HEAD / 范围)先跑完 → 再写 `09-33-SUMMARY.md` → 最后跑摘要闸门;不得在门禁运行前记录结果。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `packages-user/client-base/src/setting/types.ts` 恰好 6 个导出 interface:`ISettingItem<T = unknown> extends ISaveableContent<T>`、`ISettingSubPageItem extends ISettingItem<void>`、`ISettingItemGroup`、`ISettingSystemSave`、`ISettingSystem extends ISaveableContent<ISettingSystemSave>`、`ISettingComponentProps<T = unknown>`。
|
||||
- [ ] `ISettingItem` 成员逐字为:`readonly id: string` / `readonly name: string` / `readonly description: string` / `getValue(): T` / `setValue(value: T): void` / `getPreviewString(): string` / `getSettingComponent(): UIComponent`;无分组字段;不重复声明存档 / 读取方法。
|
||||
- [ ] `ISettingSubPageItem` 只有 `readonly page: IGameUI` 一个新增成员;`ISettingItemGroup` 只有 `readonly name: string` + `readonly items: readonly ISettingItem[]`;`ISettingSystemSave` 只有 `readonly values: ReadonlyMap<string, unknown>`;`ISettingComponentProps<T>` 只有 `readonly setting: ISettingItem<T>`。
|
||||
- [ ] `ISettingSystem` 成员逐字为:`readonly groups: ReadonlyMap<string, ISettingItemGroup>` + `setGroup` / `addItemToGroup` / `getGroup` / `getItem` / `iterateGroups` / `getValue<T>` / `setValue<T>` 七个方法;不提供未分组枚举方法;不重复声明存档 / 读取方法。
|
||||
- [ ] 导入恰为 2 行普通导入(`@motajs/system` 的 `IGameUI` / `UIComponent`,`@user/data-common` 的 `ISaveableContent`),位于文件最前、无第三行导入、无仅类型导入;逐成员中文 jsDoc(方法换行风格、短成员单行、无接口级注释)。
|
||||
- [ ] 纯类型:无类、无函数体、无常量、无箭头函数、无默认导出;未新增依赖;未新增测试文件;未改 `setting/index.ts`、`client-base/src/types.ts`、`package.json`、`hotkey/types.ts`、`data-common/src/save/types.ts`、`system/src/ui/types.ts`。
|
||||
- [ ] 门禁全绿:eslint 0 报错;`pnpm check:type` 该文件 NEW 为 0 且整仓总数不增;形状闸门通过;7 个受保护哈希一致;HEAD 政策与范围闸门通过;**先跑门禁、再写 `09-33-SUMMARY.md`、最后通过摘要闸门**;摘要含 6 个 interface 形状、两条设计取舍、`void` / `page` 语义、`ReadonlyMap` 存档形状、jsDoc / 导入口径、未 UAT 声明、估算未校准声明、执行器 0 次代码提交声明、系统本体未实现声明。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-33-SUMMARY.md` **after all gates have actually run**, recording: the final shape of the six interfaces (`ISettingItem<T = unknown> extends ISaveableContent<T>` with `id` / `name` / `description` / `getValue` / `setValue` / `getPreviewString` / `getSettingComponent`; `ISettingSubPageItem extends ISettingItem<void>` with `readonly page: IGameUI`; `ISettingItemGroup` with `name` + `readonly items: readonly ISettingItem[]`; `ISettingSystemSave` with `readonly values: ReadonlyMap<string, unknown>`; `ISettingSystem extends ISaveableContent<ISettingSystemSave>` with `groups` + the seven methods; `ISettingComponentProps<T = unknown>` with `readonly setting: ISettingItem<T>`); that the two save/load methods are inherited from `ISaveableContent` and deliberately not redeclared; the two locked design decisions (setting items carry no group field — membership lives only in `ISettingSystem.groups` / `ISettingItemGroup.items`; no ungrouped-enumeration method, because settings only enter the system through groups, unlike hotkey commands); the `void` rationale for sub-page items (no save value of their own) and that this is type-level only, with no opening/sub-page logic; the `ReadonlyMap` save shape and why it mirrors the hotkey save; the import discipline (two normal import lines, no type-only import) and the jsDoc discipline (per-member Chinese comments, wrapped style for methods, no interface-level comment); that the file was replaced wholesale and the two previous empty interfaces were intentionally removed per the user's instruction (no existing comments were involved); the real gate results from step A (eslint exit code, `pnpm check:type` NEW count for the target file plus whole-repo total, the source-shape gates, the seven protected hashes, the HEAD policy, the scope gate); that `estimate` is uncalibrated (`confidence: low`); that **no browser UAT was performed** and no settings-system / UI / save behaviour is claimed verified; that the settings system itself, its factories, UI, save registration and `IClientBase` wiring are **not** implemented and belong to later plans; that the 09-29..09-32 changes are still uncommitted so the scope gate only proves "no out-of-scope paths were added"; and that the **executor made 0 code commits** (the three `.planning` artifacts may be aggregated by the orchestrator under `commit_docs: true`).
|
||||
</output>
|
||||
@@ -0,0 +1,154 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 33
|
||||
subsystem: client-base-setting
|
||||
tags: [typescript, type-declarations, setting-system, client-base]
|
||||
|
||||
# Dependency graph
|
||||
requires: []
|
||||
provides:
|
||||
- "packages-user/client-base/src/setting/types.ts:6 个导出 interface,作为后续设置系统实现 / 工厂 / UI / 存档注册的类型契约"
|
||||
affects: [client-base, setting-system, setting-ui, save-registration]
|
||||
|
||||
# Actuals(未校准估算配对;tokens 为 types.ts 实际字符数 / 4)
|
||||
actuals:
|
||||
tokens: 581
|
||||
tasks: 2
|
||||
commits: 0
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "设置项契约:ISettingItem<T> 承载设置语义,存档 / 读取完全继承 ISaveableContent<T>"
|
||||
- "分组单向归属:分组 id 只存在于 ISettingSystem.groups 的键上,设置项不带分组字段"
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- packages-user/client-base/src/setting/types.ts
|
||||
|
||||
key-decisions:
|
||||
- "设置项不带分组字段:分组归属只在 ISettingSystem.groups 与 ISettingItemGroup.items 中表达"
|
||||
- "不提供未分组枚举方法:设置项只能经分组进入系统,没有游离设置项状态"
|
||||
- "ISettingSubPageItem 使用 ISettingItem<void> + readonly page: IGameUI,void 表示无自身存档值"
|
||||
- "ISettingSystemSave 使用 ReadonlyMap<string, unknown>,与热键存档同风格"
|
||||
- "两处存档 / 读取方法继承自 ISaveableContent,不重复声明"
|
||||
|
||||
requirements-completed: []
|
||||
|
||||
# Metrics
|
||||
duration: 1 session
|
||||
completed: 2026-10-29
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 9 Plan 33: setting/types.ts 类型契约 Summary
|
||||
|
||||
**把 `setting/types.ts` 的 3 行空壳替换为 6 个导出 interface 的纯类型声明,为后续设置系统实现钉死数据契约。**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 1 session
|
||||
- **Tasks:** 2(Task 0 为汇报关卡,不计入)
|
||||
- **Files modified:** 1(`packages-user/client-base/src/setting/types.ts`)
|
||||
- **Baseline HEAD:** `f5f83229851c910b05db33834d7e0f65d7b5120b`
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- 落点文件 `packages-user/client-base/src/setting/types.ts` 由 3 行空壳整文件替换为 6 个导出 interface。
|
||||
- 旧的两个空 interface 已按用户批准删除(该文件原本无任何注释,不涉及修改既有注释)。
|
||||
- 仅类型声明,零实现:没有类、函数体、常量、工厂、UI、存档注册或 `IClientBase` 接线。
|
||||
- 全部门禁通过(eslint / 类型 NEW-only / 形状 / 受保护哈希 / HEAD / 范围)。
|
||||
|
||||
## 六个 interface 的最终形状
|
||||
|
||||
1. `ISettingItem<T = unknown> extends ISaveableContent<T>`
|
||||
- `readonly id: string`
|
||||
- `readonly name: string`
|
||||
- `readonly description: string`
|
||||
- `getValue(): T`
|
||||
- `setValue(value: T): void`
|
||||
- `getPreviewString(): string`
|
||||
- `getSettingComponent(): UIComponent`
|
||||
2. `ISettingSubPageItem extends ISettingItem<void>`
|
||||
- `readonly page: IGameUI`
|
||||
3. `ISettingItemGroup`
|
||||
- `readonly name: string`
|
||||
- `readonly items: readonly ISettingItem[]`
|
||||
4. `ISettingSystemSave`
|
||||
- `readonly values: ReadonlyMap<string, unknown>`
|
||||
5. `ISettingSystem extends ISaveableContent<ISettingSystemSave>`
|
||||
- `readonly groups: ReadonlyMap<string, ISettingItemGroup>`
|
||||
- `setGroup(group: string, name: string, items: ISettingItem[]): void`
|
||||
- `addItemToGroup(group: string, item: ISettingItem): void`
|
||||
- `getGroup(group: string): ISettingItemGroup | null`
|
||||
- `getItem(id: string): ISettingItem | null`
|
||||
- `iterateGroups(): Iterable<[id: string, group: ISettingItemGroup]>`
|
||||
- `getValue<T>(id: string): T | null`
|
||||
- `setValue<T>(id: string, value: T): void`
|
||||
6. `ISettingComponentProps<T = unknown>`
|
||||
- `readonly setting: ISettingItem<T>`
|
||||
|
||||
`ISettingItem` 与 `ISettingSystem` 的存档 / 读取方法(`saveState` / `loadState`)来自 `ISaveableContent<T>` 的继承,本文件没有重复声明,避免与 `@user/data-common` 源头签名漂移。
|
||||
|
||||
## 设计取舍
|
||||
|
||||
- **设置项不带分组字段。** 分组归属只存在于 `ISettingSystem.groups` 与 `ISettingItemGroup.items`;不做「项上记 group」的双写,避免换组时两处不一致。`ISettingItemGroup` 只承载 `name` 与 `items`,分组 id 仅是 `groups` 的键。
|
||||
- **不提供未分组枚举方法。** 本文件不声明 `getUngrouped`。热键系统存在未分组指令,因此 `IClientHotkey` 有该方法;设置项只能通过分组进入系统,不存在「游离设置项」状态。
|
||||
- **`ISettingSubPageItem` 使用 `ISettingItem<void>`。** `void` 表示子页面项没有自身的存档值,`saveState` / `loadState` 在类型层即为空操作;`page: IGameUI` 只描述进入哪个子页面。本计划不实现打开逻辑,属纯类型表达。
|
||||
- **`ISettingSystemSave` 使用 `ReadonlyMap<string, unknown>`。** 键为设置项 id,与热键存档(`ReadonlyMap`)同风格。
|
||||
|
||||
## 导入口径与 jsDoc 口径
|
||||
|
||||
- 导入恰为 2 行普通导入,位于文件最前、中间无空行无注释、无第三行导入、无仅类型导入:
|
||||
- `import { ISaveableContent } from '@user/data-common';`
|
||||
- `import { IGameUI, UIComponent } from '@motajs/system';`
|
||||
- 逐成员中文 jsDoc:方法使用换行风格(可带 `@param`),短成员使用单行风格;方法之间空一行,字段之间不空行;不写 interface 本身的 jsDoc;不写任何非 jsDoc 注释。
|
||||
- 文件为整文件替换:旧的 `export interface ISettingItem {}` 与 `export interface ISettingSystem {}` 已删除(属用户批准的替换,原文件无既有注释)。
|
||||
|
||||
## 门禁结果(Task 2 步骤 A 实际运行)
|
||||
|
||||
| 门禁 | 结果 |
|
||||
|------|------|
|
||||
| `eslint packages-user/client-base/src/setting/types.ts` | 退出码 0,0 报错(含 prettier:80 列 / 4 空格 / 单引号 / 无尾逗号 / CRLF) |
|
||||
| `check:type`(整仓 `vue-tsc --noEmit`,`cmd /c` 包装) | 该文件 NEW 诊断 0;整仓总数 159,与基线 159 持平 |
|
||||
| 源码形状闸门(Task 1 / Task 2) | 通过:2 行导入、6 个 `export interface`、25 条签名命中、`ISettingItem` 块内 4 个方法且无分组字段、`ISettingSystem` 块内 7 个方法、无 `getUngrouped`、无实现体 |
|
||||
| 7 个受保护文件哈希 | 与基线逐字一致(见下) |
|
||||
| HEAD 政策 | HEAD 未变(`f5f8322...`),无任何提交 |
|
||||
| 范围闸门 | 仅目标文件被修改;整仓 16 条脏路径全部在基线快照内,无新增范围外路径 |
|
||||
|
||||
7 个受保护文件哈希(与 `09-33-BASELINE.json` 一致):
|
||||
|
||||
- `packages-user/client-base/src/setting/index.ts` — `fcb073fefcd6bebc8afd726f830e66828ebc5e90`
|
||||
- `packages-user/client-base/src/index.ts` — `3904bbe873a1fc1fe448529708dda15232271019`
|
||||
- `packages-user/client-base/src/types.ts` — `cdf7224dc39118665b00a9697c31e6f5ae7c4f84`
|
||||
- `packages-user/client-base/package.json` — `3e9b20220568df6244bbbceadb16e5c3b2a3e874`
|
||||
- `packages-user/client-base/src/hotkey/types.ts` — `d1cdf0c25530950a46bce801df63ed975665b451`
|
||||
- `packages-user/data-common/src/save/types.ts` — `06b37a622a5e26abb91cc6332c49ca65c6691204`
|
||||
- `packages/system/src/ui/types.ts` — `2a24550a6a3f8dade44524f7be2564f7d7957551`
|
||||
|
||||
## 未做 / 明确声明
|
||||
|
||||
- **未做浏览器 UAT**:本计划没有任何运行期行为,不宣称设置系统 / 界面 / 存档行为已验证。
|
||||
- **执行器 0 次代码提交**:未运行任何 git 写命令,代码改动留在工作树供审查;HEAD 与基线相同。`.planning` 产物可能由编排器按 `commit_docs: true` 聚合提交,不属于本计划的代码提交。
|
||||
- **估算未校准**:计划 `estimate` 为 `confidence: low`,实际成本仅作参考,不用于校准结论。
|
||||
- **系统本体未实现**:`ISettingSystem` / `ISettingItem` 的实现类、工厂、设置界面、存档注册、`IClientBase` 接线均未实现,属后续计划。
|
||||
- 工作树中 09-29..09-32 的既有改动仍未提交,这些路径在基线里本就是脏的;范围闸门只证明「无新增范围外路径」。
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written(未实现范围外内容,未修改任何受保护文件)。
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None。
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- 设置系统的类型契约已就位,后续实现可直接基于这 6 个 interface,无需再自造字段。
|
||||
- 后续需要实现系统本体、设置项工厂、设置界面、存档注册,并在 `IClientBase` 上接入 `setting` 字段(均不在本计划范围)。
|
||||
|
||||
---
|
||||
|
||||
*Phase: 09-minimum-browser-runnable*
|
||||
*Completed: 2026-10-29*
|
||||
Reference in new issue
Block a user