AGENTS.md@src/client · git:20260924.2e6ce57 · 2026-09-24 · sha256 c808b4aaec2095e6
AGENTS.md@src/client git:20260924.2e6ce57A
Immutable. This exact content never changes and is served at /api/v1/blob/c808b4aaec2095e6.
# Client Rules 适用于 `src/client/` 全域。本文件是 Client 半区**跨模块通用规则**的唯一承接点。 测试规则见 `tests/AGENTS.md`;跨层依赖与全仓库规范见根 `AGENTS.md`。 ## 本层承担什么 Client 是**单页工作台**,只负责交互与可视化,承担三类职责: 1. **界面投影与交互编排**:把 Host 的事实投影为可读界面,把用户意图翻译成对 Host 的语义化调用; 2. **自有的界面与编辑状态**:只持有「界面如何呈现、用户正在编辑什么」这类状态,不拥有 Host Runtime 的事实; 3. **宿主装配与协调**:插件入口只负责注册、装配、样式与生命周期汇总,**不承载业务语义**。 判断标准:一段代码若回答「界面如何呈现、如何交互」,属 Client;若回答「事实是什么、何时成立、如何持久化」,属 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 的具体取值。 - 以上规则由 `tests/contract/` 下的客户端门禁机械锁定。 ## 修改前确认 1. 这个事实的所有者是谁?是否在 Client 内产生了第二份事实源或第二处写入路径? 2. 是否新开了绕过统一网络边界或注入面的数据访问路径? 3. 是否引入无法追踪责任者、无法清理或会在卸载后写入的副作用?异步结果有没有归属校验或取消路径? 4. 这段代码属于 components / hooks / lib / 宿主接入中的哪一层?依赖方向是否仍然单向、无循环? 5. 是否复制了共享契约的形状或常量、绕过了词典、或随状态拼接了样式? ## 核心原则 单一状态机拥有业务事实;界面只投影不拥有 Host 状态;数据只经统一网络边界单向流动;副作用责任者可追踪、卸载即清理、异步防竞态;层次分明、依赖单向、契约共享、文案与样式集中。