


在 TypeScript 单体仓库中维护双向协议 SDK,且不破坏公开 API
在修改以多个包发布、客户端和服务器都能发起请求的严格类型 SDK 时使用。有意识地选择公开导出,在两端添加带能力检查的新操作,记录破坏性变更,并把示例当作测试运行。
要使用这个AI规则
## 先判断改动属于哪一层、是否触及公开 API
本指南适用于发布协议 SDK 的多包 TypeScript 工作区,其中包含共享的内部核心、客户端包、服务端包、独立的 schema 包,以及轻量的框架或运行时适配器包。默认前提是启用严格类型检查、使用 ES 模块,并由单一包管理器驱动覆盖整个工作区的脚本。
有两个特性主导着每一项决策。第一,已发布的接口就是契约:凡是能从包入口点触达的内容都属于公开 API,不可随意更改。第二,该协议是双向的——两端都可以发起请求——因此一侧的改动通常在另一侧带来镜像式的对应义务。
动手编辑之前,先确认改动属于哪一层,以及是否触及已发布的接口。如果触及,就必须按 API 变更来对待,除了风格规则外,还要套用下文的接口与迁移规则。
## 迭代时只跑单个测试,完成前跑全部检查
工作区提供覆盖整个仓库的聚合脚本:安装依赖、构建全部包、运行 lint 与格式检查、自动修复 lint 与格式问题、对所有包做类型检查、运行完整测试套件,以及一个把类型检查与 lint 组合在一起的综合检查脚本。
迭代过程中要收窄反馈循环。用包管理器的过滤参数只运行单个包的测试脚本,传入文件路径以运行单个测试文件,传入名称过滤条件以运行单条测试用例。此外还有用于紧凑迭代的监视模式脚本。
把覆盖整个工作区的运行留到宣布工作完成之前的那一刻,届时综合检查与测试套件都要跑。仅靠类型检查无法证明运行时行为——在没有执行相应测试之前,绝不可声称某项改动可用。
## 遵循仓库的命名与格式规则
- 全程启用严格类型检查;导出函数须标注显式返回类型。
- 类和类型使用 PascalCase;函数和变量使用 camelCase。
- 文件名使用小写并以连字符分隔;测试文件的扩展名中带有 test 后缀。
- 使用 ES 模块导入。扩展名遵循项目的模块解析设置——在打包器风格的解析下,相对导入不带文件扩展名。导入语句要按逻辑分组。
- 两个空格缩进,必须写分号,优先使用单引号。
- 测试放在各个包专用的测试目录下,以便测试运行器的 include 匹配模式能够识别;测试名称要具有描述性。
- 公开 API 要写文档注释;行内注释只留给确实不易理解的逻辑。
当某条规则含义不明确时,就与所在文件的既有写法保持一致。在功能性改动中,不要顺手重新格式化或重构无关代码。
## 从会被编译的示例文件中引入文档示例
写在文档注释里的手工代码片段会悄无声息地腐化,因为没有任何环节会去编译它们。应改为从配套的示例模块中提取示例代码,这些模块会与其余源码一起被编译和类型检查。
配套文件与其对应的模块相邻放置,并采用相互匹配的命名。配套文件内的具名区域通过文档代码块上的 source 属性引用,该属性指向文件与区域。区域名称遵循可预期的规律:导出符号名加变体名,或类名加方法名再加变体名。若省略区域引用,则会纳入整个文件,这一做法适用于任意文件类型。
编辑示例之后要运行片段同步脚本,把当前内容复制进文档注释和 markdown 页面,并将同步后的产物与改动一起提交。
## 公开内容按名称导出,并让包根入口不依赖特定运行时
要把内部汇总模块与经过筛选的公开导出模块区分开:前者导出一切内容、标记为 private,且仅供工作区内的同级包使用;后者对外暴露类型、错误类、常量、类型守卫以及共享基类。包入口点则重新导出这个筛选后的模块,外加自身显式具名的导出。
以下规则没有商量余地:
- 在入口点和筛选后的模块中一律使用显式具名导出。绝不在这些位置使用通配符重新导出。
- 把某个符号加入包入口点,就等于把它变成公开 API。只在确实有意为之时才这样做。
- 内部辅助代码留在内部汇总模块里,绝不加入筛选后的模块,也绝不加入任何入口点。
- 保持每个包的根入口与运行时无关,使浏览器和边缘运行时的打包器都能使用它。凡是其模块依赖图会间接触及平台专有内置能力(例如进程派生或原始套接字)的导出,都必须放在具名的子路径导出之后,并有测试断言根汇总模块不含这些内容。
- 承诺不含校验库类型的已发布接口中,不要混入校验库类型;应从专用的 schema 包发布原始的运行时 schema,并让同级包把该包当作真正的外部依赖来解析,而不是各自携带私有的 schema 副本。
## 以模式层为基础,并让传输层可替换
把 SDK 组织为三层。
1. 类型与 schema 层——与规范对齐的协议消息类型、校验 schema 和常量。这是唯一的事实来源;其他各层都由它派生。
2. 协议层——一个抽象基类,负责消息路由、请求与响应的关联、能力协商以及传输生命周期。两端的实现都继承自它,因此双向行为在结构上天然对称。
3. 高层 API——暴露类型化方法的客户端类与服务端类,外加一个更高层的服务端封装,提供简化的工具、资源与提示注册方式。
传输层是位于统一接口之后的独立可插拔层:推荐用于远程对端的流式 HTTP 传输、为向后兼容而保留的旧版事件流传输,以及用于本地派生进程集成的标准输入输出传输。校验同样秉持可插拔的思路,提供默认校验器以及可兼容受限运行时的备选方案。
框架与运行时适配器包只是轻量的集成层。它们绝不可增加协议功能;任何改变协议行为的内容都属于核心层。
## 理清双方对等端请求的发出与接收流程
出站。高层方法委托给基类的请求方法,后者分配唯一的消息标识符,断言远端对端已声明该方法所需的能力,注册待处理的响应处理器,把序列化后的请求交给传输层,并在匹配的响应到达时等待其完成。
入站。传输层抛出收到的消息,基类路由将其分派为请求、响应或通知。请求处理会按方法名查找对应的处理器,构建上下文对象,让子类为该上下文补充传输相关的细节,调用处理器,并把处理器的结果作为响应经传输层返回。
上下文结构。包含会话标识符;一个请求分组,内含消息标识符、方法名、请求元数据、用于取消的中止信号,以及向对端回送关联请求或通知的辅助方法;还有一个可选的传输分组,在传输层提供时携带已校验的授权信息。服务端上下文还额外提供日志记录、用户输入征询以及模型采样等辅助能力。
能力校验在双向上都会执行,并且在处理器注册时也会执行,因此对端无法为自己从未声明过的内容注册处理器。
## 新增协议操作,并在两端加入能力检查
1. 在类型层定义请求参数 schema 与结果 schema。
2. 在客户端或服务端的能力类型中加入对应的能力标志。
3. 在发起该操作的一端实现发送方法。
4. 在该端的能力检查中加入能力断言。
5. 在对端注册接收处理器。
6. 当该操作值得封装时,在简化的服务端 API 上添加高层封装。
7. 两端都补充测试;若该操作面向用户,还要补充可运行的示例。
第二步和第四步最常被跳过,而跳过它们的代价也最高:缺少能力协商的操作,在面对未实现它的对端时会在运行时失败,而不是在初始化阶段就干净利落地报错。
## 按“改了什么、为何改、如何迁移”记录破坏性变更
每一项破坏性变更都要记入迁移指南,写在它所影响的子系统章节里。要先搜索相关章节并加以扩充,而不是在末尾追加新的独立章节——由一堆平行的一次性条目拼成的指南,恰恰会在有人真正需要它时变得无法阅读。仅限于单个发行说明文件范围内的变更,则写进该文件。
每个条目都要说清三件事:改了什么、为什么改、如何迁移。
机械性的重命名应放进自动迁移工具的映射文件中。要链接到该映射,而不是在正文里复述重命名对照表;重复的表格会与真正执行重命名的工具逐渐脱节。
判断原则:优先采用增量式变更。当破坏无法避免时,在可行的情况下让旧路径以弃用状态再保留一个周期,并把破坏设计成会以清晰的编译期错误暴露出来,而不是变成悄无声息的行为变化。
## 在 CI 中把服务器与客户端配对示例当作测试运行
把示例当作测试,而不是装饰。每个示例都是独立的工作区包,内含配对的服务端与客户端;客户端负责连接、断言其预期结果,并在不匹配时以失败状态退出。有一个运行器会按配置的传输方式与协议版本组合执行每个示例,而该任务是每次变更都要通过的持续集成关卡的一部分。
示例的共用支撑代码集中在一个共享包中,其根导出保持精简——参数解析、断言辅助、同级路径解析。较重的可选依赖,例如演示用的授权提供方或内存事件存储,则放在子路径导出之后,以免无关示例急切地加载它们。示例只导入这些支撑代码,并就地设置传输层,因此每个示例读起来都像真实用法,而不像是对隐藏配置的一层包装。
文档指南页面配有按页组织的片段配套文件,由同一个片段同步步骤负责同步,其中可运行的部分同样会在持续集成中执行。
