No invalid states
Installable agent skills and plugin-ready workflows for coding agents
npx -y skills add Vivswan/skills --skill no-invalid-statesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Refactor code so invariants are enforced by the type system instead of repeated runtime checks. Parse and validate at trust boundaries, then make invalid internal states impossible to construct. Use when reviewing or refactoring code with boolean lifecycle flags, optional fields that must appear together, "must call X before Y" comments, repeated validation of the same value, or field combinations that should be contradictory. Also triggers on requests like "make illegal states unrepresentable", "parse, don't validate", or "strengthen these types".
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
9.3 KB, as published. Nobody here has run it
No Invalid States
Review and refactor a codebase so that correctness constraints are carried by the strongest mechanism the language provides, not by scattered defensive checks.
The core principle:
Validate at boundaries, convert into stronger representations, and make invalid internal states difficult or impossible to construct.
This is the "parse, don't validate" discipline. It does not mean deleting runtime validation: checks on external, dynamic, or untrusted data stay. The goal is that each fact is checked once, at the edge, and then encoded in a representation the rest of the program can trust.
When to Apply
Use this skill when the code shows any of these signals:
- repeated runtime checks of the same condition (
if !initialized,if value is None,assert ready) - booleans that encode lifecycle or mutually exclusive states
- several optional fields that must always appear or be absent together
- methods that are only valid after another method has been called
- comments such as "must call X before Y" or "only valid when connected"
- functions re-validating data that a caller already validated
- structs, classes, or interfaces that permit contradictory field combinations
- incorrect operation ordering that could be rejected statically
- casts, non-null assertions, or type ignores used to quiet the checker
Do not use this skill to add abstraction to code that is already simple, or in place of a bug hunt; it improves representation, not behavior.
Workflow
1. Find candidate invariants
Search for the telltale patterns:
if !initialized if value is None state === ...
if !connected if value === undefined is_ready / is_valid
assert ready has_loaded instanceof / isinstance chains
Also inspect:
- boolean lifecycle flags and state enums surrounded by unrelated optionals
- constructors that produce partially valid objects
- public setters that can break an invariant after construction
- methods that fail only because another method was not called first
- unchecked casts or assertions that exist to convince the type checker
For each candidate, write the invariant as one sentence. For example: "a connection may only send data after it has successfully connected." If you cannot state the invariant, you cannot encode it; skip it.
2. Classify each invariant: static or dynamic
Static, internal invariants belong in the program's structure. Examples: initialized vs uninitialized, authenticated vs unauthenticated, parsed vs raw, validated ID vs arbitrary string, mutually exclusive states.
Dynamic, external conditions keep their runtime checks. Types cannot prove that an HTTP request succeeded, a file still exists, a token has not expired, user input is well-formed, or a service is reachable.
The target shape is a one-way pipeline:
untrusted or raw value
-> validate / parse once, at the boundary
-> strong internal representation
-> trusted internal APIs (no re-checking)
3. Choose the strongest idiomatic representation
Pick by situation, not by favorite mechanism:
| Situation | Representation |
|---|---|
| State changes at runtime and code inspects which state it is in | Sum type: enum with data, discriminated union, sealed class |
| Available operations depend on state; wrong ordering should fail to compile | Typestate: state-parameterized types, consuming transitions |
| A primitive has special meaning or is validated once then trusted | Newtype or branded type behind a smart constructor |
| Two states carry meaningfully different data; optionals encode which one | Separate types per state |
| An invariant spans construction (fields must agree) | Private fields, factory or constructor that validates, immutability |
| The invariant is really about the data store | Schema constraints: NOT NULL, CHECK, foreign keys, unique |
Detailed, idiomatic guidance with code examples lives in the per-language references:
- references/rust.md: enums, newtypes, typestate with
PhantomData, consumingself,TryFromboundaries - references/typescript.md: discriminated unions,
branded types, exhaustive
neverchecks, boundary schemas - references/python.md: state-specific frozen
dataclasses,
Literalunions,NewType,assert_never, strict typing - references/other-languages.md: Go, Java, Kotlin, C#, Swift, functional languages, dynamic languages, and databases
If the language at hand is not covered, map the situation table onto whatever the language offers: sealed hierarchies, smart constructors, immutability, and module privacy exist almost everywhere in some form.
4. Refactor
For each accepted candidate, work through this checklist:
- State the invariant and the invalid state currently representable.
- Introduce the stronger representation.
- Put validation at the boundary: one parser or factory that turns raw data into the strong type, with a real error path.
- Change internal APIs to accept the strong type, so downstream code cannot receive an unvalidated value.
- Remove checks the new representation makes logically impossible.
- Keep, and briefly justify, checks that remain because they are dynamic.
Rules while refactoring:
- Make illegal construction hard: private fields, validating constructors, factory functions, state-transition functions, frozen or readonly data.
- Validate once.
validate -> use, validate -> usebecomesparse once -> use, use, use. - Never silence the type checker with broad casts,
any, non-null assertions, or ignore comments to force a refactor through. - Preserve existing behavior unless you have confirmed a bug; if you find one, report it separately rather than fixing it silently.
- Respect public API compatibility where practical; strengthen internals first, then widen outward.
- Make the smallest architectural change that removes the invalid state. Do not convert every boolean into a state machine. A refactor is worthwhile only when it eliminates repeated checks, prevents wrong ordering, removes impossible field combinations, or clarifies the API contract. Readability beats type-system cleverness.
5. Test and verify
Update or add tests for valid state transitions, rejected boundary input, exhaustive state handling, and behavior preservation. Add compile-fail or type-level tests only if the project already has a mechanism for them.
Then run the repository's own tooling. Inspect the project configuration
(package.json scripts, Makefile, Cargo.toml, pyproject.toml, CI
workflows) rather than assuming command names. Typical gates:
- Rust:
cargo fmt --all -- --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace - TypeScript: the project's typecheck, lint, test, and build scripts
- Python: the configured ruff / pyright / mypy / pytest setup
- anything else: whatever the repo's CI runs
Do not introduce a new type checker or dependency just for this skill unless that is genuinely appropriate for the repository.
6. Report
When asked to modify code, implement the changes rather than describing them. At completion, report:
- Invariants improved: what was previously enforced by convention or runtime checks
- Invalid states removed: what contradictory data or wrong ordering was previously possible
- New representation: which mechanism now carries each invariant (for example: Rust typestate, TypeScript discriminated union, Python NewType)
- Runtime validation retained: which checks stay because they depend on external or dynamic information
- Files changed and why
- Verification: the exact formatter, type-checker, lint, build, and test commands run, and whether they passed
Review Criteria
Skills that run code reviews (such as rubber-duck-review) expand this
section into their reviewer prompt when this skill is installed. Ask the
reviewer to flag:
- invariants enforced only by runtime checks or convention: boolean lifecycle flags, optional fields that must appear or be absent together, "must call X before Y" ordering enforced at runtime, repeated validation of already-validated values, and field combinations that should be impossible to represent
- for each finding, the stronger representation (sum type, newtype, typestate, validating constructor) that would remove the invalid state
Triage the resulting findings with the workflow above.
Guiding Principle
Before adding another defensive check, ask: can this invalid state be removed from the program's representation instead? Use the strongest idiomatic mechanism the language offers, and keep the design simpler, not more complicated, than the problem itself.