mcpField
N
no-invalid-states

no-invalid-states

github.com/vivswan开发166 次安装2 天前更新
代码仓库

关于 no-invalid-states

no-invalid-states 是用于审查和重构代码的技能,目标是用程序的类型和结构表达正确性约束。它指导在边界验证、强化内部表示,并在保留现有行为的前提下进行验证。

主要功能

  1. 01识别重复检查与无效状态组合
  2. 02区分静态不变量和动态条件
  3. 03在边界验证并强化数据表示
  4. 04按不变量选择合适的类型
  5. 05调整 API 以接收可信值
  6. 06测试状态转换并验证行为保留

适用场景

  1. 01重构重复的生命周期检查
  2. 02明确表示互斥状态
  3. 03审查 API 中无效的操作顺序

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vivswan/skills --skill no-invalid-states

安装后在 Claude Code 中输入/no-invalid-states确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE.md
README.md
.codex-plugin/
plugin.json
agents/
openai.yaml
references/
other-languages.md
python.md
rust.md
typescript.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. 重构

对于每个已接受的候选项,请依次完成以下检查清单:

  1. 说明不变量,以及当前仍可表示的无效状态。
  2. 引入更强的表示方式。
  3. 在边界处进行验证:使用一个解析器或工厂,将原始数据转换为强类型,并提供实际的错误处理路径。新边界如何处理格式错误的输入(存在唯一明确的修复方式时予以修复,否则拒绝),是 /code-standards 技能的规则;根据下方规则,现有边界在本次重构中保持原有行为。
  4. 修改内部 API,使其接受强类型,这样下游代码就无法收到未经验证的值。
  5. 删除新表示方式在逻辑上已不可能触发的检查。
  6. 保留并简要说明仍需保留的动态检查。

重构时的规则:

  • **让非法构造难以实现:**使用私有字段、经过验证的构造函数、工厂函数、状态转换函数,以及冻结或只读数据。
  • **只验证一次。**将“验证 -> 使用,验证 -> 使用”改为“解析一次 -> 使用、使用、使用”。
  • 绝不要通过宽泛的类型转换、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 个调用点重复进行同一项检查,或每个使用方都必须记得遵守的标志。若状态存在可枚举的变更点,且可在其中一次性解决问题,则适用此项
  • 对于每项发现,指出可消除无效状态的更强表示方式(和类型、新类型、类型状态、验证构造函数)或所有者端处理方式

按照上述工作流对发现的问题进行分类。

评论

支持 Markdown