AGENTS.md@src/client · git:20260923.459672e · 2026-09-23 · sha256 8cdd976287c9c731

AGENTS.md@src/client git:20260923.459672eA

Immutable. This exact content never changes and is served at /api/v1/blob/8cdd976287c9c731.

# Client Rules

适用于 `src/client/` 全域。本文件是 Client 半区**跨模块通用规则**的唯一承接点。
测试规则见 `tests/AGENTS.md`;跨层依赖与全仓库规范见根 `AGENTS.md`。

## 本层承担什么

Client 是**单页工作台**,只负责交互与可视化,承担三类职责:

1. **界面投影与交互编排**:把 Host 的事实投影为可读界面,把用户意图翻译成对 Host 的语义化调用;
2. **自有的界面与编辑状态**:只持有「界面如何呈现、用户正在编辑什么」这类状态,不拥有 Host Runtime 的事实;
3. **宿主装配与协调**:插件入口只负责注册、装配、样式与生命周期汇总,**不承载业务语义**。

判断标准:一段代码若回答「界面如何呈现、如何交互」,属 Client;若回答「事实是什么、何时成立、如何持久化」,属 Host。
**不得为了与 Host 保持形式一致而强制建立后端式模块结构**:目录与模块数量服从职责边界,而不是相反。

## 状态所有权

- **单一状态机拥有业务事实**:同一事实只允许一处所有权与一处写入边界;状态变化只经该状态机的语义化 action(reducer 分支)完成;
- **渲染态与业务态分离**:悬停、拖拽中、展开/折叠、临时校验提示等瞬时渲染态归组件本地状态,**不得**写入业务状态;
- **派生数据由选择器计算**,不得存储、双写或与源数据各自维护;选择器必须是纯函数;
- **组件不得直接修改不属于自己的状态**:只允许 dispatch 自己的语义化 action,禁止直接改状态对象字段、禁止跨模块改写他人内部状态;
- 无对应 action 时,**增加明确 action**,而不是在组件内维护第二份副本或绕过状态机直改。

## 数据流与数据访问边界

- **单向数据流**:事实流向固定为「Host → 统一网络边界 → 状态机 → 选择器 → 组件 → 语义化 action → 统一网络边界 → Host」,不得出现反向捷径或旁路;
- **网络访问只经统一网络边界与注入面**:组件与 hook 不得自建第二条数据访问路径(不得直接发起请求、不得绕过注入面自取宿主服务);
- 端点名、协议常量等一律引用共享契约,不得在调用点硬编码字面量;
- **稳定错误码由调用方按语义处理**(如乐观锁冲突按冲突语义刷新/提示),不得只显示通用文案后静默继续;
- 网络边界的失败语义必须显式:区分「业务失败」与「传输/解析失败」,并保留可判定的错误码。

## 副作用与异步生命周期

- 订阅、定时器、监听、Observer、长驻 DOM 与独立渲染根,**必须可追踪责任者与清理方式**;谁创建谁释放,释放路径与创建路径成对出现;
- **卸载后不得写状态**:异步回调进入前先校验所属生命周期仍然有效;
- **异步结果必须防竞态**:按归属校验(结果是否属于当前会话/当前文档)或主动取消(AbortController);两者都没有的异步写入视为缺陷;
- 常驻资源的释放必须**幂等**,且与宿主卸载顺序一致;不得依赖「不会发生」的时序假设;
- 副作用不得藏在名称看似纯计算的函数中。

## 层级边界

- **components(表现与交互)**:负责渲染与用户交互,只消费 props、选择器结果与注入的动作面;不感知网络、持久化与宿主服务细节;
- **hooks(状态组合、生命周期、交互编排)**:负责把状态、派生数据与动作组合成界面可用的输入;一个 hook 只承担一个职责,**不得**把无关的多重职责(如同时管轮询、导入导出与布局)捆在一起;
- **lib(与 UI 生命周期无关的逻辑)**:纯计算与转换、网络访问、持久化;**不得依赖任何 UI 模块**(组件、hook、宿主接入),也不得读取界面状态;被 lib 与组件共用的纯常量应下沉到 lib 或共享契约,而不是让 lib 反向引用组件;
- **依赖方向单向**:components / hooks 可依赖 lib;lib 不得反向依赖,也不得出现循环依赖;
- 纯计算与状态无关的转换优先实现为纯函数:输入即全部事实,不得隐式读取全局或环境。

## 共享契约

- 与 Host 共享的类型与纯常量**一律引用共享契约**(`src/host/shared/`),禁止在 Client 复制形状、枚举、常量或默认值清单;
- 同一语义只允许一处本体:Client 侧只做投影与消费,不得定义第二套映射;
- Client 不得引入 Host 运行时模块(依赖方向约束见根 `AGENTS.md`),只允许引用共享契约中的纯类型与纯常量;
- 修改共享契约必须同步检查 Client 调用方、Host 侧消费者与测试。

## 宿主接入

- 与官方插槽、侧边栏、会话、词典等宿主能力的交互**集中在宿主接入层**;业务组件与 hooks 不得直接触达官方服务对象;
- 官方形状按**最小消费面**收窄声明并做运行时守卫:缺失即降级,不因未知字段抛错,不伪造官方不存在的契约;
- 模块级注册表与 DOM 搬运必须有**明确生命周期与释放路径**(登记、迁移、归还、释放各自有唯一责任者),并在宿主卸载时清空登记;
- 宿主能力不可用时插件其余部分必须照常生效,不得整体失效。

## 文案与样式

- 面向用户的文案**必须走词典**,`zh` / `en` 同步新增,禁止在组件内硬编码用户可见字符串;
- **样式集中在统一样式模块**,以语义化类名与设计 token 表达;**禁止随状态拼接样式**(状态只切换类名/token,不拼接样式值);
- 不得在 Client 内复制设计 token 的具体取值。

## 修改前确认

1. 这个事实的所有者是谁?是否在 Client 内产生了第二份事实源或第二处写入路径?
2. 是否新开了绕过统一网络边界或注入面的数据访问路径?
3. 是否引入无法追踪责任者、无法清理或会在卸载后写入的副作用?异步结果有没有归属校验或取消路径?
4. 这段代码属于 components / hooks / lib / 宿主接入中的哪一层?依赖方向是否仍然单向、无循环?
5. 是否复制了共享契约的形状或常量、绕过了词典、或随状态拼接了样式?

## 核心原则

单一状态机拥有业务事实;界面只投影不拥有 Host 状态;数据只经统一网络边界单向流动;副作用责任者可追踪、卸载即清理、异步防竞态;层次分明、依赖单向、契约共享、文案与样式集中。