搜索/修改横跨两种运行时的插件式数据管道,不把它改坏

修改横跨两种运行时的插件式数据管道,不把它改坏

在横跨两种运行时的插件式后端中让编码智能体修改代码时使用。把版本号集中在一个文件里,把测试过滤器用在正确的任务上,遵守插件、并发和服务分层的规则,并确认每个新测试在没有对应改动时会失败。

要使用这个AI规则
## 把版本号集中在一个文件里,并从同一个根目录构建 让版本号和工具链坐标保持权威唯一,并始终从同一个位置构建。 - 把每一个版本号和工具链坐标都存放在一个权威文件中;所有使用方(构建脚本、辅助工具、测试、包描述文件)都从这里读取,你也只需要更新这一个文件。 - 当某个值被有意硬编码或在多处重复,例如运行时兼容性路径片段,不要把它当成bug去动态计算;而是在权威位置找到当前值并与之保持一致。 - 始终从同一个固定根目录运行所有构建和测试命令,这样相对路径的连接方式每次都能一致地解析。 - 优先使用由引导脚本(bootstrap)提供的、纳入版本管理的固定工具链,而不是全局安装的语言或版本管理器,这样每个环境的构建方式都一样。 ## 把测试过滤器用在真正拥有这些测试的任务上 在运行筛选后的测试子集之前,先弄清楚测试任务之间是如何连接的。 - 一个总控测试任务往往本身不包含任何测试类,只是把工作委派给子任务,因此对它应用名称过滤器会悄无声息地匹配不到任何内容,看起来却像是通过了。应该把过滤器应用到真正拥有这些测试的具体任务上。 - 测试套件的拆分是有意为之的:有些测试会被移出它看似应属的套件,例如需要启动第二个运行时的测试,会被放进提供该运行时的套件里。如果过滤器什么都找不到,目标很可能就在另一个套件中。 - 要清楚单元测试、集成测试和验收测试各自存放在哪里,这样才能调用正确的入口。 ## 重新组织代码,让新情况看起来像一开始就规划好的 修改代码时,不要只是在已有内容后面简单追加。要评估在你的改动之后,周围的代码是否依然合理。如果某个抽象最初是为一种情况设计的,而你现在要加入第二种情况,就应该重新组织结构,使最终结果读起来就像两种情况从一开始就被同时考虑过一样。让每个不同的概念都拥有自己命名清晰的独立单元,再在更高层的代码中组合这些单元,使意图无需阅读每处实现细节就能一目了然。 ## 基于框架的基类和注册表来编写插件 在框架自身的扩展点之上构建扩展,而不是绕开它们。 - 继承特定类型的基类,并重写其生命周期钩子:一个setup或register钩子,再加上该类型的入口方法。 - 通过框架提供的声明式选项DSL来声明配置,而不是直接读取原始参数,这样校验、类型转换和文档生成都会自动处理。 - 在类级别设置并发模式:当多个工作线程可能同时调用该实例时用shared,当调用必须串行、同一时刻只能有一个线程运行时用single。 - 所有插件查找都要经过中央注册表,新的内置插件在那里注册,外部插件则以打包单元的形式被发现。 ## 把指标等通用处理放进包裹插件的委托器 把横切关注点放在一个包装层里,并从框架获取协作对象。 - 让每个插件都运行在一个委托器(delegator)之后,由它添加诸如指标采集、线程安全等横切关注点;在修改行为时,要判断这段逻辑该放在委托器里还是插件本身。 - 让指标遵循命名空间层级结构,通过命名空间调用来创建子命名空间。 - 提供一个空对象(null-object)指标实现,它实现相同的接口但会丢弃数据,用于指标被禁用或处于测试环境的情况。 - 永远不要直接实例化指标对象,而是从框架接收它们。诸如日志和配置这类标准mixin会自动包含进来,而更重的能力则保持按需启用(opt-in)。 ## 不让工作循环变慢,并按正确顺序启动和停止 保护好热路径,并遵守启动与停止的顺序。 - 把工作线程循环当作热路径:它拉取批次数据、送入已编译的管道运行、并发出完成信号。不要在其中加入阻塞调用或大量内存分配。 - 遵循严格的生命周期顺序:先注册输出和过滤器,然后启动工作线程,最后启动输入;关闭时则相反,先停止输入,工作线程排空剩余数据,再关闭过滤器和输出。违反这个顺序会导致数据丢失或死锁。 - 对跨线程共享的状态使用原子类型,而不是普通的布尔字段,因为跨线程的内存可见性在其他情况下无法得到保证。 ## 把管道配置编译成代码,并能导出来调试 有些引擎会通过一条分阶段的流水线,在运行时把声明式配置编译成可执行的类:先用语法规则把配置文本解析成语法树;再把这棵树降级为中间表示,即一个由节点和条件分支组成的有向无环图;然后为每个节点生成源代码,并在专用命名空间内于运行时编译成类;最后把编译好的单元接入执行循环。条件分支会被编译成谓词函数,接入分支节点。要提供一个调试开关,可以把生成的源代码转储到目录中,方便你检查实际运行的内容。 ## 在整个代码库中统一错误、空值和日志的写法 在整个代码库中保持一致的结构、错误处理、空值处理和日志约定。 - 按功能领域而不是按架构层来组织模块,把每个新类型放到它所属的领域中。 - 保持命名一致:为extension、delegator、abstract等类型预留固定的后缀和前缀;除非有意允许子类化,否则将类标记为final。 - 优先使用非受检的运行时异常;只在无法避免时才使用受检异常,并且始终附带描述性信息和保留完整的原因链(cause chain)。 - 对于空值,用显式的非空校验来验证构造函数参数,为有意允许为空的契约加上注解,并且只在查找可能失败的场景使用可选类型(optional),而不是把它当作通用的空值替代品。 - 每个类声明一个静态final的logger,并谨慎选择日志级别:细粒度诊断信息用低级别,可恢复的问题记为警告(warning),未捕获的失败记为错误(error)。 - 优先使用不可变的final字段,共享缓存使用并发安全的map类型,并在类型是共享的地方注明线程安全性。 ## API 响应统一走一个辅助函数,业务逻辑放进命令类 对服务进行分层,让路由、响应和业务逻辑各自拥有唯一归属。 - 让各个API模块继承自一个共享基类,该基类扩展自web框架自身的基类,并通过构造函数接收公共依赖。 - 在一个中央命名空间映射表中声明各路径前缀对应的端点挂载点;新增命名空间时也在这里添加,而不是临时随意路由。 - 所有响应都通过统一的响应辅助方法发出,由它为返回内容包装默认元数据;不要手写原始响应。 - 用一小组带类型的错误子类(如not-found、bad-request、timeout)来表示错误;在处理函数中抛出这些错误,交由基类统一格式化成一致的响应外层结构。 - 把业务逻辑放在专门的命令类中,而不是路由处理函数里;处理函数只负责构建命令,并把其结果传给响应辅助方法。 ## 连接两种运行时的地方,要测试经过两边的完整往返 当代码需要桥接两种运行时,例如由宿主虚拟机驱动第二种语言时,要在框架指定的注册点注册被桥接的类,并遵循那里已有的模式。在使用点(而不是构造时)获取线程本地的上下文对象,因为一个被保存下来的上下文如果在另一个线程中被复用会失败。要测试贯穿两层的完整往返流程,而不是只测试单侧,这样才能捕捉到单层单元测试会漏掉的类型转换和注册错误。加载第二种运行时的测试必须让其模块查找路径指向纳入版本管理的位置;那个路径是有意为之的,编辑这些测试时不要改动它。 ## 用真实管道的辅助工具测试,并清理留下的状态 用专门构建的辅助工具来验证真实行为,并且不留下任何残留状态。 - 优先使用框架提供的管道辅助工具来启动一个真实的管道并捕获结果,而不是手工构造管道对象。 - 为了隔离测试,可以在spec文件内部定义临时的插件类,并将其注册到真实的注册表中。 - 用共享示例(shared examples)来验证接口契约,用共享上下文(shared context)来处理通用初始化;用fixture+服务的模式驱动集成测试,从fixture文件启动。 - 确认已有的救援(rescue)或捕获(catch)代码块确实覆盖了新增加的代码路径。 - 在teardown中清理全局或虚拟机级别的状态:你设置的每一个属性都需要一个对应的清理动作,以免状态泄漏到其他测试中。 - 优先使用继承自真实基类的、具体且专门构建的替身对象,而不是通用mock;把mock保留给基础设施边界使用,如果某个stub只是为了搭建环境,就让真实方法照常运行。 ## 确认每个新测试在没有对应改动时会失败 确保每个测试确实依赖于它所守护的那个改动。 - 验证每个新测试在没有对应改动的情况下会失败;一个无论有没有修复都能通过的测试什么都证明不了。 - 断言应针对改动引入的具体行为,而不是针对原本就存在的条件:把断言锚定在一个可观察的差异上,比如一个新字段、一个变化的值,或者一条此前缺失的日志。 - 对于bug修复,先在测试中重现该bug;确认它以与该bug实际表现相同的原因失败,然后应用修复并确认测试通过。 - 警惕循环论证式的mock:让方法stub返回一个值,然后再去断言这个值,这是循环逻辑;如果删掉真实实现、只保留mock,测试依然通过,说明这个测试只是在验证mock本身。 ## 破坏性变更分阶段推出,敏感值不写进日志 按分阶段的节奏改变行为,保护敏感值,并让文档同步跟进。 - 分阶段引入破坏性改动:先把新行为作为可选项(opt-in)引入,并在次要版本中加入弃用警告,再在保留退出选项的前提下切换默认行为,最后在主要版本中彻底移除旧行为。 - 对于携带敏感值的配置,使用框架提供的专用密钥类型或包装器,并重写其字符串表示形式使该值被遮蔽;默认情况下,任何级别都不应把这类值写入日志。 - 把文档更新视为改动的一部分:只要行为发生变化,就同步更新相关的参考文档和贡献者文档,遵循各自格式的约定,并要求面向用户的功能在合并前必须附带文档。