关于 no-invalid-states
no-invalid-states 是用于审查和重构代码的技能,目标是用程序的类型和结构表达正确性约束。它指导在边界验证、强化内部表示,并在保留现有行为的前提下进行验证。
主要功能
- 01识别重复检查与无效状态组合
- 02区分静态不变量和动态条件
- 03在边界验证并强化数据表示
- 04按不变量选择合适的类型
- 05调整 API 以接收可信值
- 06测试状态转换并验证行为保留
适用场景
- 01重构重复的生命周期检查
- 02明确表示互斥状态
- 03审查 API 中无效的操作顺序
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vivswan/ skills --skill no-invalid-states安装后在 Claude Code 中输入/no-invalid-states确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.mdREADME.md.codex-plugin/plugin.jsonagents/openai.yamlreferences/other-languages.mdpython.mdrust.mdtypescript.md详细介绍
不存在无效状态
审查并重构代码库,让正确性约束由语言所能提供的最强机制来承载。零散的防御性检查并非这种机制。
核心原则:
在边界处验证,转换为更强的表示,并让内部无效状态难以构造或无法构造。
这就是「解析,而非验证」的原则:
- 每项事实只在边界处检查一次。
- 将结果编码为其余程序可以信赖的表示。
- 不要删除运行时验证:对外部、动态或不可信数据的检查应当保留。
适用场景
当代码出现以下任何迹象时,使用此技能:
- 对相同条件进行重复的运行时检查(
if !initialized、if value is None、assert ready) - 用布尔值表示生命周期或互斥状态
- 多个可选字段必须始终同时出现或同时缺失
- 只有在调用另一个方法后才有效的方法
- 类似「必须先调用 X 再调用 Y」或「仅在已连接时有效」的注释
- 函数重复验证调用方已经验证过的数据
- 允许字段组合相互矛盾的结构体、类或接口
- 静态检查本可拒绝的错误操作顺序
- 为了绕过检查器而使用的类型转换、非空断言或类型忽略
不要用此技能为本已简单的代码增加抽象,也不要用它代替错误排查。它改进的是表示方式,而非行为。
指导原则
在添加另一项防御性检查之前,先问问:能否从程序的表示中移除这种无效状态?使用该语言所提供的最强惯用机制。设计应与问题本身一样简单,而不是更加复杂。
工作流程
1. 找出候选不变量
搜索这些明显模式:
if !initialized if value is None state === ...
if !connected if value === undefined is_ready / is_valid
assert ready has_loaded instanceof / isinstance chains
还要检查:
- 布尔生命周期标志和状态枚举周围是否混杂着无关的可选字段
- 会生成部分有效对象的构造函数
- 构造后可能破坏不变量的公共 setter
- 仅因另一个方法未先调用而失败的方法
- 为说服类型检查器而存在的未经检查的类型转换或断言
将每个候选不变量写成一句话。例如:「连接成功后,连接才可以发送数据。」
如果无法陈述该不变量,就无法对其进行编码。跳过它。
2. 将每个不变量分类为静态或动态
- 静态:内部不变量应体现在程序结构中。例如:已初始化与未初始化、已验证身份与未验证身份、已解析与原始、已验证的 ID 与任意字符串、互斥状态。
- 动态:外部条件应保留运行时检查。类型无法证明 HTTP 请求成功、文件仍然存在、令牌尚未过期、用户输入格式正确,或服务可访问。
目标形态是一个单向流水线:
untrusted or raw value
-> validate / parse once, at the boundary
-> strong internal representation
-> trusted internal APIs (no re-checking)
3. 在所有权层级解决问题
在选择表示方式之前,先找出谁会修改不变量背后的状态。解决方案应位于这些修改点,而不是消费者处。
典型信号:N 个调用点都带着相同的守卫,或者每个消费者都必须记得遵循某个标志。这是在 N 个读取点进行校验,而只需在少数写入点阻止问题即可。
示例:关联的端点 URL 发生变化时,已存储的凭据会变得过时。
- 消费者侧设计: 六个请求路径在使用凭据前都会检查
staleCredential标志。六处守卫、六种错误处理方式,而且今后的每个请求路径都必须记得检查。 - 所有者侧设计: 每次 URL 变更(设置界面保存、编辑配置文件、导入)都经过一个统一的所有者转换。它在变更发生时解决问题(“是否将此凭据用于新端点?”)。
- 之后: 请求路径不再需要过时状态守卫。运行时身份验证失败仍按原样处理,因为它们属于动态条件。
两条规则可以确保所有者侧修复可靠:
- 在宣称覆盖完整之前,列举所有修改点。 消费者侧检查可能会意外覆盖你遗漏的写入路径,但所有者侧修复必须明确将每条路径都导向所有者转换。GUI 和它所编辑的设置文件是两条路径,而不是一条。
- 修改点上的解决方案不一定是类型。 验证转换、规范化,或在表达意图时向用户提问,都可行。下一步会介绍适合使用更强表示方式的情况。
4. 选择最强且符合语言习惯的表示方式
根据具体情况选择,而不是偏爱某种机制:
| 情况 | 表示方式 |
|---|---|
| 状态会在运行时变化,代码需要检查当前状态 | 和类型:携带数据的枚举、判别联合、密封类 |
| 可执行的操作取决于状态;错误的操作顺序应无法通过编译 | 类型状态:以状态为参数的类型、消费式转换 |
| 基本类型具有特殊含义,或只需验证一次之后即可信任 | 由智能构造函数封装的新类型或品牌类型 |
| 两种状态携带的有意义数据不同;可选值用于表示是哪一种 | 每种状态分别使用独立类型 |
| 不变量涉及构造过程(字段必须一致) | 私有字段、带验证的工厂或构造函数、不可变性 |
| 不变量实际上与数据存储有关 | 模式约束:NOT NULL、CHECK、外键、唯一约束 |
各语言参考中提供了详细且符合语言习惯的指导及代码示例:
- references/rust.md:枚举、新类型、使用
PhantomData的类型状态、消费self、TryFrom边界、安全 API 背后的 unsafe - references/typescript.md:判别联合、品牌类型、穷尽性
never检查、边界模式 - references/python.md:特定状态的冻结数据类、
Literal联合、NewType、assert_never、严格类型检查 - references/other-languages.md:Go、Java、Kotlin、C#、Swift、函数式语言、动态语言和数据库
如果当前使用的语言未涵盖在内,请将情况表映射到该语言提供的机制。密封层级、智能构造函数、不可变性和模块私有性,几乎所有语言都以某种形式支持。
5. 重构
对于每个已接受的候选项,请依次完成以下检查清单:
- 说明不变量,以及当前仍可表示的无效状态。
- 引入更强的表示方式。
- 在边界处进行验证:使用一个解析器或工厂,将原始数据转换为强类型,并提供实际的错误处理路径。新边界如何处理格式错误的输入(存在唯一明确的修复方式时予以修复,否则拒绝),是
/code-standards技能的规则;根据下方规则,现有边界在本次重构中保持原有行为。 - 修改内部 API,使其接受强类型,这样下游代码就无法收到未经验证的值。
- 删除新表示方式在逻辑上已不可能触发的检查。
- 保留并简要说明仍需保留的动态检查。
重构时的规则:
- **让非法构造难以实现:**使用私有字段、经过验证的构造函数、工厂函数、状态转换函数,以及冻结或只读数据。
- **只验证一次。**将“验证 -> 使用,验证 -> 使用”改为“解析一次 -> 使用、使用、使用”。
- 绝不要通过宽泛的类型转换、
any、非空断言或忽略注释来强行通过重构,从而绕过类型检查器。 - 保留现有行为,除非你已确认存在错误。如果发现错误,应单独报告,而不是悄悄修复。
- **在实际可行的情况下,遵守公共 API 兼容性:**先强化内部,再向外扩展。
- **尽量缩小架构变更范围,**只移除无效状态。不要把每个布尔值都改造成状态机。
- **只有在重构能够消除重复检查、防止顺序错误、移除不可能的字段组合,或明确 API 契约时,才值得进行。**可读性优先于类型系统技巧。
6. 测试与验证
更新或添加以下测试:
- 有效的状态转换
- 被拒绝的边界输入
- 穷尽状态处理
- 行为保持不变
仅当项目已有相应机制时,才添加编译失败或类型级测试。
然后运行仓库自身的工具。检查项目配置(package.json 脚本、Makefile、Cargo.toml、pyproject.toml、CI 工作流),不要臆测命令名称。常见检查项:
- Rust:
cargo fmt --all -- --check、cargo clippy --workspace --all-targets -- -D warnings、cargo test --workspace - TypeScript:项目的类型检查、代码检查、测试和构建脚本
- Python:已配置的 ruff / pyright / mypy / pytest 设置
- 其他语言:仓库 CI 所运行的工具
除非确实适合该仓库,否则不要仅为了这项技能而引入新的类型检查器或依赖项。
7. 报告
当被要求修改代码时,请直接实施修改,而不是描述修改方案。完成后,请报告:
- **改进的不变量:**此前依赖约定或运行时检查来保证的内容
- **移除的无效状态:**此前可能出现的矛盾数据或错误顺序
- **新的表示方式:**现在用于承载各项不变量的机制(例如:Rust typestate、TypeScript 判别联合、Python NewType)
- **保留的运行时验证:**仍需保留的、依赖外部或动态信息的检查
- 变更的文件及原因
- **验证:**实际运行的格式化程序、类型检查器、代码检查器、构建和测试命令,以及是否通过
审查标准
安装此技能后,运行代码审查的技能(例如 /rubber-duck-review)会将本节扩展为审查者提示词。请审查者标出:
- 仅由运行时检查或约定强制执行的不变量:
- 布尔生命周期标志
- 必须同时出现或同时缺失的可选字段
- 由运行时强制执行的“必须先调用 X,再调用 Y”顺序
- 对已验证值的重复验证
- 理应无法表示的字段组合
- 可通过所有者端处理消除的使用方防护:在 N 个调用点重复进行同一项检查,或每个使用方都必须记得遵守的标志。若状态存在可枚举的变更点,且可在其中一次性解决问题,则适用此项
- 对于每项发现,指出可消除无效状态的更强表示方式(和类型、新类型、类型状态、验证构造函数)或所有者端处理方式
按照上述工作流对发现的问题进行分类。