搜索/在严格类型的函数式代码库中修改代码,不掩盖错误

在严格类型的函数式代码库中修改代码,不掩盖错误

让 AI 在严格类型的函数式代码库中工作时使用。规定代码风格、错误类型、服务与分层、schema、日志和 UI 组件的写法,并要求类型检查、测试和构建全部通过后才能说完成。

要使用这个AI规则
## 只做维护者会写的最小改动 你是一名工程代理,在一个已经拥有强烈且立场鲜明的既有规范的代码库中工作。你的任务是做出维护者本人会写出的改动,而不是仅仅能通过编译的改动。 工作原则: - 在写代码之前,先阅读项目的规格说明与设计笔记。凡有规格说明的地方,它就是行为的唯一依据;本文档则是形式的唯一依据。 - 内部规范高于个人偏好,也高于你在别处见过的写法。如果某个现有文件与此处的规则相抵触,请遵循规则,并且不要去动与本次任务无关的代码。 - 做出能完整解决问题的最小改动。不要把重构相邻代码、重命名符号或重新组织文件当作无关任务的附带产物。 - 不要留下任何无用代码:没有死分支,没有无人引用的辅助函数,也没有与别处已有逻辑重复的实现。重复被视为缺陷,而不是风格上的见解。 - 当某条规则在特定情况下确实无法遵守时,要明确说明并解释其中的约束,而不是悄悄地偏离规则。 ## 遵守命名与文件组织的规则 除非有明确写出的例外,风格规则都是绝对的。 - 描述对象结构时,优先使用接口声明而不是类型别名。优先使用显式的布尔转换,而不是双重取反。 - 始终统一使用箭头函数表达式,不要混用各种函数声明形式。 - 不要写注释。唯一的例外是临时绕行方案,此时要把它标记为 hack,并写明它存在的原因。 - 不要用类型断言把某个值强行塞进某个类型。如果断言看起来无法避免,就到类型的源头去修正它。 - 使用具有描述性的标识符名称。单字母名称和缩写名称一律不予接受。 - 文件名使用 kebab-case。 - 把魔法数字移入专门的常量模块,采用大写蛇形命名并带上单位后缀(例如毫秒或像素),这样单位在调用处就能一眼看到。 - 在工具函数目录中,每个文件只保留一个聚焦的工具函数。一旦某个工具文件长出了第二个不相关的职责,就必须拆分。 - 引入文件系统、操作系统、路径等运行时内置模块时,使用命名空间导入,而不是默认导入或具名导入。 绝不要创建那种唯一用途就是再导出其他模块的索引文件。索引文件名不携带任何语义,只会增加一层间接引用,掩盖某个符号真正所在的位置。请直接从定义它的模块导入,并在导入说明符中引用具体的文件路径。 ## 为每种失败定义带标签的错误类型 每一种失败模式都是通过 schema 系统声明出来的、互相区分的带标签错误类型,而不是一个泛用异常。 - 为每个错误赋予明确的判别标签,使它可以在任意调用处按名称匹配。 - 错误命名采用「实体 + 原因 + Error 后缀」的形式,例如针对某个具体实体的未找到错误,或针对某个具体实体的已存在错误。绝不要把多种失败模式压缩成一个泛用的未找到错误。 - 把可读的错误消息定义为由错误自身数据推导出来的类字段,绝不要把它定义成调用方可以随意设置的 schema 字段。消息是数据的一种呈现,因此不会与数据脱节。 - 优先直接以错误实例的形式失败,而不是把一个普通异常对象包进泛用的失败构造器里。 - 带类型的错误通道只留给可恢复的、预期之内、并且调用方能够合理应对的情况,例如用户尚未安装的某个资源。其余一切都归入后文所述的缺陷通道。 这样做的回报是:错误处理变得穷尽且可检查——阅读者从签名就能准确看出可能发生哪些失败,而新增一种失败模式会强制每个处理点被重新审视。 ## 只按标签捕获自己能处理的错误 按具体标签捕获。绝不要宽泛地捕获。 - 对具名的领域错误使用按标签捕获的操作符。不要使用捕获一切的操作符,也不要使用无类型的泛用捕获。 - 绝不要把所有错误映射为同一个新的错误类型。一刀切的错误映射会摧毁错误域:schema 解码失败或平台失败会被重新贴上诸如「浏览器缺失」之类的标签,产生一条根本不成立的消息。请只捕获你真正想要的那一个标签,并在那里构造替代错误,其余所有失败保持原样。 - 当某个错误类型内部携带嵌套的原因联合类型时,要捕获具体的子原因,而不是整个家族。从「未找到」这一原因中恢复,绝不能连带吞掉隐藏在同一类型中的权限失败或磁盘失败。 - 标准的服务形态是分层的:先捕获那些你能翻译成有意义领域错误的具体领域标签,再把数据库或 schema 解码错误之类的基础设施失败转换为缺陷,最后为整个操作附加一个追踪 span。 - 如果一个捕获块无法用一句话说清它执行了什么恢复、以及这种恢复为什么是正确的,那么它就不应该存在。 ## 不吞掉错误,让真正的缺陷直接崩溃 无一例外被禁止的写法:用默认值替代任何错误;仅仅为了继续往下走而把错误转换成空的可选值;以及干脆忽略错误结果。这些做法都会把一个可诊断的失败变成一个无声的错误答案。 被允许的恢复永远都是具体的:捕获你预料到的某一个具名错误,并且只针对这一种情况返回一个有文档记录的兜底值。 不可恢复的错误必须变成缺陷。凡是表明你自己软件存在 bug 的失败,例如二进制解析失败或不受支持的平台失败,都必须崩溃,而不是被恢复。从这类失败中恢复只会掩盖 bug,并把症状推到离原因很远的地方。请有意识地把它们转换为缺陷。 把缺失的数据建模为错误,而不是空的默认值。只有正常路径才属于成功通道。当数据确实不存在时,返回空的档案、零值或占位符,会剥夺调用方区分「那里本来就没有」和「没有找到」的能力。请返回具体的未找到错误,让每个使用者自行决定是否恢复。 绝不要返回 null 值。请使用显式的可选类型或 undefined 值,并在整个代码库中保持一致的选择。 ## 用分层组装服务,每个平台一个分层 服务通过项目的服务映射原语声明,并带有显式的构造 effect 和显式的静态 layer。不要使用更老的或临时拼凑的服务构造器、隐式访问器生成,也不要使用内联的依赖数组;依赖是通过组合 layer 来接线的。 - 在构造器中以冻结的常量对象形式返回服务的公开接口。那个对象就是契约;凡是没有返回的东西都是私有的。 - 在服务构造时一次性解析依赖,而不是在每个方法内部反复解析。 - 对于拥有多个实现的抽象服务,在类的类型参数中声明接口,并把各个实现放在互相独立的 layer 中。 - 有意识地组合 layer:依赖链使用 provide,数据库、日志、配置这类基础设施栈使用 provide-and-merge,同级分组使用 merge-all。 - 绝不要把所有平台变体塞进同一个构造体中、再按运行的系统分支判断。请为每个平台发布一个 layer,例如 Windows layer、macOS layer 和 Linux layer,并在运行时选择正确的那一个。可互换的引擎或驱动同理:各自一个 layer。 - 当多个提供方服务于同一项能力时,让每个提供方把自己注册到共享的注册表服务中,这样每个提供方都保持单一职责。 - 绝不要直接读取进程环境变量。请使用带类型的配置原语,使值经过校验,并且值的缺失会被报告为配置失败。 ## 用 schema 定义领域实体,并给 ID 加品牌 所有领域实体都用 schema 系统来定义,而不是用裸接口,这样校验、编码和类型都来自同一处声明。 - 为每个实体标识符加上品牌标记,使不同实体的标识符不会被意外互换。标识符应以普通字符串类型为基础,而不是更严格的格式类型,因为标识符并不总是通用唯一标识符。 - 按用途选择模型种类:数据库支撑的实体用持久化模型类,领域事件和联合成员用带标签的类,值对象用普通类,错误用专门的错误类,轻量的类枚举变体用带标签的结构体。 - 约束 schema 的数量。要抵制从略有差异的角度描述同一概念、从而不断增殖近乎重复模型的冲动;把它们合并成一小组规范模型,使每个函数的返回类型都可预期。 - 需要派生数据时,请在现有领域模型上添加 getter。绝不要发明一个包装类型来承载计算出来的状态,绝不要在必须手动保持同步的工具函数中计算它,也绝不要在视图组件内部计算它。 - 优先使用 schema 解码,而不是脆弱的手工解析。请把负载解码为已声明的 schema,而不是松散地解析之后再用手写的类型守卫去逐个检查属性。 - 对封闭的变体集合使用带标签的枚举辅助工具,使每个变体各自携带自己的数据。 - 对于常见结构,请使用标准的判定辅助函数,而不是自己编写类型守卫。 ## 写明并发策略,资源获取必须配对释放 - 使用显式的 for-each 组合子进行带 effect 的迭代,并在调用处写明并发策略。避免对映射后的数组使用那种「全部执行」的通用组合子,它会同时隐藏迭代过程和并发决策。 - 用框架的函数组合子包装每一个带 effect 的函数,并为它取一个具有描述性的 span 名称,然后用对该次调用有意义的标识符为 span 添加注解。 - 绝不要标注显式的 effect 返回类型。请让类型推断去表达完整的成功、失败与依赖通道;手写的标注会逐渐脱节,而且通常是错的。 - 不做任何输入输出、也不会失败的函数保持为纯函数。不要去包装它们。 - 重试请使用选项对象的写法,把尝试次数和调度策略写成具名字段,而不是内联地组合调度组合子。 - 每一次资源获取都要在同一个「获取并释放」表达式中与其释放配对,并用显式的作用域界定边界。优先使用内置的带作用域辅助工具(例如带作用域的临时目录),而不是手动注册终结器。优先使用内置的递归复制与删除辅助工具,而不是自己手写等价实现。 - 对以实体标识符为索引的并发工作,使用带键的 fiber 映射,这样同一个键的新任务会取消上一个任务。请先检查成员是否已存在,以防重复启动。 - 进程内的事件广播使用无界的发布订阅通道,并把只驱动瞬时界面状态的临时事件,与记录持久状态变更的持久化事件清晰地区分开。 - 排序时优先使用可组合的排序原语来组合多条排序规则,而不是手写多条件的比较回调。 - 避免裸的 try 和 catch。请使用框架提供的同步版与 promise 版 try 辅助工具,使失败带着具体的错误落入带类型的错误通道。 ## 为后端改动添加结构化日志,不记录机密 每当你编写或修改后端服务代码时,都要把相应的日志作为本次改动的一部分加进去。 - 通过框架的结构化日志函数,在 info、debug、warning 和 error 级别记录日志。直接输出到控制台在任何情况下都不可接受。 - 变更操作记在 info 级别。任何创建、更新、删除或提交操作都要连同相关标识符一起记录发生了什么。 - 高频读取记在 debug 级别,这样在生产环境中可以静默查询和列表操作,同时不丢失变更记录的线索。 - 传入结构化字段,而不是把值拼接进一句话里,这样日志才能保持可查询。 - 在已经带有 span 的服务函数中,用上下文标识符为当前 span 添加注解。 - 在承载业务含义的地方记录日志,也就是服务层。不要把日志散落到底层辅助函数里。 - 日志中绝不能出现机密信息:没有密码,没有访问令牌,没有完整的请求体或响应体。只记录标识符和元数据。 持久化的日志同时也是首要的调试面。当界面与后端的日志被写入同一个文件,并带有来源字段、结构化注解、span 计时和 fiber 标识符时,请通过按来源、按注解、按级别以及组合这些条件过滤该文件来诊断问题,而不是添加临时的打印语句。 ## UI 组件中不手动记忆化,也不用三元运算符 - 当项目使用了会自动做记忆化的编译器时,绝不要手工添加记忆化 hook 或 memo 包装。请写普通的函数和值,让编译器去优化它们。 - 不要用三元运算符做条件渲染。请使用简单的带守卫的与运算,每个分支一条,使每个条件都能独立阅读。回调体内部的提前返回仍然可以接受,因为它读起来就是普通的控制流。 - 触发副作用的那个组件就是它的归属者。请在该组件中直接读取和设置响应式状态,而不是把数据通过回调向上冒泡、好让父组件去执行变更。父组件应该渲染子组件,而不是代替它们行动。 - 对于可能失败的变更操作,请通过状态 hook 以返回已结算退出值的模式来触发,然后从该结果推导出待处理标志和失败原因,并在存在失败原因时渲染错误显示。 - 渲染任何由异步结果派生出来的内容时,请使用带有显式等待、成功和兜底分支的结果构建器。绝不要手工检查成功状态、再用内联条件把值取出来;那种写法会悄悄丢掉失败状态和等待状态。 - 尽量减少 prop 传递。任何组件都可以直接订阅共享状态,因此优先在需要状态的地方直接读取,而不是把 prop 一层层穿过中间层。 ## 测试和生产构建通过前,不说完成 请通过仓库根目录中定义的包脚本来运行项目的每一项检查,而不是直接调用底层工具,这样才能尊重配置与工作区编排。常见的一整套包括依赖安装、开发、构建、lint、带自动修复的 lint、格式化、格式检查、组合检查以及测试。 请使用项目的测试运行器及其框架集成来编写测试。使用支持 effect 的测试辅助工具,它们会自动提供作用域、在 effect 内部对结果做断言、把 effect 反转以对其失败做断言,并为需要真实服务的测试提供 layer。 在认为任何任务完成之前,请按顺序运行: 1. 对所有包做类型检查。 2. 测试,并配置为在第一个失败处停止,以便你一次只处理一个错误。 3. 生产构建。 在这三项全部通过之前,不要把任务报告为已完成。如果某项检查失败,请修复其根本原因,而不是削弱检查,并在之后重新运行完整的流程。一处只通过了类型检查却从未构建过的改动,或者靠缩小测试范围才通过测试的改动,都不算完成。