litclaude-ai 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. package/scripts/validate-plugin.mjs +72 -0
@@ -0,0 +1,291 @@
1
+ # One-Liners and Disposable Scripts
2
+
3
+ Production hygiene with throwaway ergonomics. Rust scripts get the same strict lints, the same miri rule when `unsafe` is touched, the same type discipline. The difference is dependency declaration lives inline.
4
+
5
+ ## `rust-script` — the recommended path
6
+
7
+ Install once:
8
+
9
+ ```bash
10
+ cargo install rust-script
11
+ ```
12
+
13
+ Write a script:
14
+
15
+ ```rust
16
+ #!/usr/bin/env rust-script
17
+ //! Fetch a URL and print its body length.
18
+ //!
19
+ //! Usage:
20
+ //! ./fetch.rs <url>
21
+ //!
22
+ //! ```cargo
23
+ //! [dependencies]
24
+ //! anyhow = "1"
25
+ //! reqwest = { version = "0.12", features = ["blocking"] }
26
+ //! ```
27
+
28
+ use std::env;
29
+
30
+ fn main() -> anyhow::Result<()> {
31
+ let url = env::args().nth(1).context("usage: fetch.rs <url>")?;
32
+ let body = reqwest::blocking::get(&url)?.error_for_status()?.text()?;
33
+ println!("{} bytes", body.len());
34
+ Ok(())
35
+ }
36
+ ```
37
+
38
+ Make executable: `chmod +x fetch.rs`. Run: `./fetch.rs https://example.com`.
39
+
40
+ The `//! \`\`\`cargo` block is parsed as inline `Cargo.toml`. Everything else is normal Rust.
41
+
42
+ ## With async
43
+
44
+ ```rust
45
+ #!/usr/bin/env rust-script
46
+ //! ```cargo
47
+ //! [dependencies]
48
+ //! anyhow = "1"
49
+ //! tokio = { version = "1", features = ["full"] }
50
+ //! reqwest = "0.12"
51
+ //! ```
52
+
53
+ #[tokio::main]
54
+ async fn main() -> anyhow::Result<()> {
55
+ let urls = [
56
+ "https://example.com",
57
+ "https://example.org",
58
+ ];
59
+ let client = reqwest::Client::new();
60
+ let bodies = futures::future::join_all(urls.iter().map(|u| {
61
+ let c = client.clone();
62
+ async move { c.get(*u).send().await?.text().await }
63
+ })).await;
64
+ for (url, body) in urls.iter().zip(bodies) {
65
+ match body {
66
+ Ok(b) => println!("{url}: {} bytes", b.len()),
67
+ Err(e) => eprintln!("{url}: error {e}"),
68
+ }
69
+ }
70
+ Ok(())
71
+ }
72
+ ```
73
+
74
+ ## With CLI parsing
75
+
76
+ ```rust
77
+ #!/usr/bin/env rust-script
78
+ //! ```cargo
79
+ //! [dependencies]
80
+ //! anyhow = "1"
81
+ //! clap = { version = "4", features = ["derive"] }
82
+ //! ```
83
+
84
+ use clap::Parser;
85
+
86
+ #[derive(Parser, Debug)]
87
+ #[command(version, about = "rename files by pattern")]
88
+ struct Cli {
89
+ /// Glob to match
90
+ pattern: String,
91
+ /// Replacement template (use {n} for sequence)
92
+ template: String,
93
+ /// Show what would happen without doing it
94
+ #[arg(long)]
95
+ dry_run: bool,
96
+ }
97
+
98
+ fn main() -> anyhow::Result<()> {
99
+ let cli = Cli::parse();
100
+ let entries: Vec<_> = glob::glob(&cli.pattern)?.collect::<Result<_, _>>()?;
101
+ for (n, entry) in entries.iter().enumerate() {
102
+ let target = cli.template.replace("{n}", &n.to_string());
103
+ if cli.dry_run {
104
+ println!("{} -> {target}", entry.display());
105
+ } else {
106
+ std::fs::rename(entry, &target)?;
107
+ }
108
+ }
109
+ Ok(())
110
+ }
111
+ ```
112
+
113
+ ## Caching
114
+
115
+ `rust-script` caches the compiled binary in `~/.cache/rust-script/`. First run is slow (full compile), subsequent runs are instant.
116
+
117
+ To clear: `rust-script --clear-cache`.
118
+
119
+ Pin a script's compile target into the script directory for portability:
120
+
121
+ ```bash
122
+ rust-script --build-only --base-path . ./script.rs
123
+ ```
124
+
125
+ This drops a `target/` next to the script with the prebuilt binary.
126
+
127
+ ## `cargo-script` (RFC 3424, stable since Rust 1.85)
128
+
129
+ The official replacement that landed in cargo proper. Same idea, slightly different syntax:
130
+
131
+ ```rust
132
+ #!/usr/bin/env -S cargo +nightly -Zscript
133
+ ---
134
+ package:
135
+ name = "fetch"
136
+ edition = "2024"
137
+
138
+ dependencies:
139
+ anyhow = "1"
140
+ reqwest = { version = "0.12", features = ["blocking"] }
141
+ ---
142
+
143
+ fn main() -> anyhow::Result<()> {
144
+ let url = std::env::args().nth(1).context("url required")?;
145
+ println!("{}", reqwest::blocking::get(&url)?.text()?.len());
146
+ Ok(())
147
+ }
148
+ ```
149
+
150
+ Status as of 2026-05: stabilization in progress. Use `rust-script` for production now, migrate when `cargo script` is stable everywhere your tools live.
151
+
152
+ ## Strict mode for scripts
153
+
154
+ Add a lints block in the inline `Cargo.toml`:
155
+
156
+ ```rust
157
+ //! ```cargo
158
+ //! [dependencies]
159
+ //! anyhow = "1"
160
+ //!
161
+ //! [lints.rust]
162
+ //! unsafe_code = "forbid"
163
+ //!
164
+ //! [lints.clippy]
165
+ //! all = "deny"
166
+ //! pedantic = "warn"
167
+ //! unwrap_used = "deny"
168
+ //! expect_used = "deny"
169
+ //! panic = "deny"
170
+ //! ```
171
+ ```
172
+
173
+ Now the script gets the same strictness as the main project. If you need a one-line `unwrap()` for prototype velocity, switch the lint to `warn` for that one script - never blanket `allow`.
174
+
175
+ Run with lints visible:
176
+
177
+ ```bash
178
+ RUSTFLAGS="-D warnings" rust-script ./script.rs
179
+ ```
180
+
181
+ ## When NOT to use a script
182
+
183
+ - It is going to live longer than a week → make it a real crate with `cargo new --bin`.
184
+ - It needs custom build scripts (`build.rs`) → real crate.
185
+ - It needs binary distribution to other machines → real crate with `cargo dist`.
186
+ - It needs to be tested → real crate (scripts can technically run `#[test]`s under `cargo test`, but the workflow is awkward).
187
+
188
+ A reasonable migration path: start as a script, when complexity grows past ~200 lines or you reach for a second `.rs` file, run `rust-script --emit ./script.rs` to dump a regular Cargo project skeleton and continue from there.
189
+
190
+ ## Inline tests in a script
191
+
192
+ ```rust
193
+ #!/usr/bin/env rust-script
194
+ //! ```cargo
195
+ //! [dependencies]
196
+ //! ```
197
+
198
+ fn double(x: i32) -> i32 { x * 2 }
199
+
200
+ fn main() {
201
+ println!("{}", double(21));
202
+ }
203
+
204
+ #[cfg(test)]
205
+ mod tests {
206
+ use super::*;
207
+
208
+ #[test]
209
+ fn doubles_ints() {
210
+ assert_eq!(double(5), 10);
211
+ }
212
+ }
213
+ ```
214
+
215
+ Run tests: `rust-script --test ./script.rs`.
216
+
217
+ ## A useful "Rust as awk" pattern
218
+
219
+ For ad-hoc data processing on stdin:
220
+
221
+ ```rust
222
+ #!/usr/bin/env rust-script
223
+ //! ```cargo
224
+ //! [dependencies]
225
+ //! serde_json = "1"
226
+ //! ```
227
+
228
+ use std::io::{self, BufRead, Write};
229
+
230
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
231
+ let stdin = io::stdin();
232
+ let stdout = io::stdout();
233
+ let mut out = stdout.lock();
234
+ for line in stdin.lock().lines() {
235
+ let line = line?;
236
+ let value: serde_json::Value = serde_json::from_str(&line)?;
237
+ if let Some(s) = value.get("level").and_then(|v| v.as_str()) {
238
+ if s == "error" {
239
+ writeln!(out, "{line}")?;
240
+ }
241
+ }
242
+ }
243
+ Ok(())
244
+ }
245
+ ```
246
+
247
+ `cat logs.jsonl | ./filter-errors.rs` — filter JSON logs by `level == "error"`. Faster than `jq` for big files, type-safe.
248
+
249
+ For numerics:
250
+
251
+ ```rust
252
+ #!/usr/bin/env rust-script
253
+ //! sum a column of numbers from stdin
254
+ use std::io::{self, BufRead};
255
+ fn main() {
256
+ let total: f64 = io::stdin().lock().lines()
257
+ .filter_map(|l| l.ok())
258
+ .filter_map(|l| l.trim().parse::<f64>().ok())
259
+ .sum();
260
+ println!("{total}");
261
+ }
262
+ ```
263
+
264
+ ## The `rust-script` shebang trick on macOS
265
+
266
+ macOS does not support multi-arg shebangs without `env -S`. Use:
267
+
268
+ ```rust
269
+ #!/usr/bin/env -S rust-script --
270
+ ```
271
+
272
+ The `--` lets clap-style argument parsers see the user's args, not the rust-script arguments.
273
+
274
+ ## Editor support
275
+
276
+ VS Code / Helix / Vim with `rust-analyzer`: open the script file as if it were `src/main.rs` of an inferred crate. Most editors auto-detect the inline manifest. If not, hand-create a `Cargo.toml` next to the script with matching deps for the duration of editing, then delete it.
277
+
278
+ ## When `rust-script` is too heavy
279
+
280
+ For absolutely throwaway "one expression on stdin" use cases, a Rust REPL like `evcxr_jupyter` (Jupyter kernel) or `irust` (terminal REPL) is more appropriate:
281
+
282
+ ```bash
283
+ cargo install irust
284
+ irust
285
+ ```
286
+
287
+ But these are interactive playgrounds, not scriptable. For shell pipelines, stay with `rust-script`.
288
+
289
+ ## The Promise
290
+
291
+ Same strict lints. Same `clippy::pedantic` enforcement. Same `unsafe`-requires-SAFETY rule. The agent does not get a free pass on a 30-line script. The whole point of strict scripts is that **production hygiene is cheap when the toolchain enforces it**.
@@ -0,0 +1,429 @@
1
+ # Property Tests (proptest) + Snapshot Tests (insta)
2
+
3
+ Two test types every Rust project should have alongside unit tests. Proptest hunts for inputs your unit tests forgot to try. Insta locks down output shapes you do not want to silently change.
4
+
5
+ ## When to reach for each
6
+
7
+ | Want to test… | Use |
8
+ |---|---|
9
+ | One specific behavior with a known input | `#[test]` + `assert_eq!` |
10
+ | All inputs of a certain shape work | `proptest!` |
11
+ | Output structure stays stable across refactors | `insta::assert_*_snapshot!` |
12
+ | Parser/serializer round-trips | `proptest!` (the round-trip property) |
13
+ | CLI help text, JSON response shape, debug output | `insta::assert_snapshot!` |
14
+ | Concurrency under all interleavings | `loom` (see `concurrency.md`) |
15
+
16
+ Use all three. They cover different bug classes.
17
+
18
+ ## Proptest — setup
19
+
20
+ `Cargo.toml`:
21
+
22
+ ```toml
23
+ [dev-dependencies]
24
+ proptest = "1"
25
+ proptest-derive = "0.5" # for #[derive(Arbitrary)]
26
+ ```
27
+
28
+ `proptest.toml` at project root (optional, sane defaults):
29
+
30
+ ```toml
31
+ cases = 256 # number of random inputs per property
32
+ max_local_rejects = 65536
33
+ max_global_rejects = 1024
34
+ max_shrink_iters = 1024
35
+ max_shrink_time = 60_000 # ms
36
+ failure_persistence = { source_file = "proptest-regressions/", file_name = "regressions.txt" }
37
+ verbose = 0
38
+ ```
39
+
40
+ `failure_persistence` is the killer feature: every failure is written to a regression file. On the next run, those exact inputs are replayed first, so once a bug is found it never escapes again.
41
+
42
+ ## Basic property test
43
+
44
+ ```rust
45
+ use proptest::prelude::*;
46
+
47
+ fn parse(s: &str) -> Result<Color, ParseError> { /* ... */ }
48
+ fn render(c: &Color) -> String { /* ... */ }
49
+
50
+ proptest! {
51
+ #[test]
52
+ fn parse_render_roundtrips(red in 0u8..=255, green in 0u8..=255, blue in 0u8..=255) {
53
+ let color = Color { red, green, blue };
54
+ let rendered = render(&color);
55
+ let parsed = parse(&rendered).expect("our render should always parse");
56
+ prop_assert_eq!(parsed, color);
57
+ }
58
+ }
59
+ ```
60
+
61
+ `proptest!` macro takes `(arg in strategy, ...)` pairs. Each strategy produces values; proptest runs the body with random samples, then shrinks failing cases to minimal forms.
62
+
63
+ ## Strategies — the value-generation language
64
+
65
+ | Strategy | Produces |
66
+ |---|---|
67
+ | `any::<T>()` | Any value of `T` (if `T: Arbitrary`) |
68
+ | `0u32..100` | Integer ranges |
69
+ | `prop::sample::select(slice)` | Pick from a list |
70
+ | `prop::collection::vec(elem, range)` | Vec of length in range |
71
+ | `prop::collection::hash_map(k, v, n..m)` | HashMap |
72
+ | `prop::option::of(strategy)` | Option |
73
+ | `prop::result::maybe_ok(ok, err)` | Result |
74
+ | `(s1, s2).prop_map(\|(a, b)\| ...)` | Combine, transform |
75
+ | `s.prop_filter("reason", \|v\| pred)` | Reject values |
76
+ | `s.prop_flat_map(\|v\| dependent)` | Sequential dependency |
77
+ | `prop_oneof![strategy1, strategy2]` | Union of strategies |
78
+ | `r"[a-z]{3,10}"` | Regex-generated string |
79
+ | `"\\PC*"` | Any printable non-control string |
80
+
81
+ Example combining several:
82
+
83
+ ```rust
84
+ fn config_strategy() -> impl Strategy<Value = Config> {
85
+ (
86
+ prop::sample::select(vec!["dev", "staging", "prod"]),
87
+ 0u16..=65535,
88
+ prop::collection::hash_map(
89
+ r"[a-z_]{1,20}",
90
+ any::<String>(),
91
+ 0..5,
92
+ ),
93
+ ).prop_map(|(env, port, vars)| Config {
94
+ env: env.into(),
95
+ port,
96
+ env_vars: vars,
97
+ })
98
+ }
99
+
100
+ proptest! {
101
+ #[test]
102
+ fn config_validates(cfg in config_strategy()) {
103
+ let result = validate(&cfg);
104
+ if cfg.port == 0 {
105
+ prop_assert!(result.is_err());
106
+ } else {
107
+ prop_assert!(result.is_ok());
108
+ }
109
+ }
110
+ }
111
+ ```
112
+
113
+ ## Properties to write for every parser
114
+
115
+ 1. **Round-trip:** `parse(render(x)) == x` for all valid `x`.
116
+ 2. **No-panic:** `parse(arbitrary_string)` never panics, always returns `Result`.
117
+ 3. **Idempotent:** `parse(parse(x).unwrap().render()) == parse(x).unwrap()`.
118
+ 4. **Whitespace insensitivity:** `parse(x) == parse(strip_whitespace(x))` (if applicable).
119
+
120
+ For every serializer:
121
+
122
+ 1. **Length bound:** `render(x).len() <= bound(x)`.
123
+ 2. **Charset:** `render(x).chars().all(|c| ALLOWED.contains(&c))`.
124
+
125
+ For every collection operation:
126
+
127
+ 1. **Identity:** `op_identity(x) == x` (sort an already-sorted, dedupe a unique).
128
+ 2. **Idempotence:** `op(op(x)) == op(x)`.
129
+ 3. **Commutativity:** `op(a, b) == op(b, a)` (set union, etc).
130
+ 4. **Length:** `op(a, b).len() == known_relation(a.len(), b.len())`.
131
+
132
+ For every numeric op:
133
+
134
+ 1. **Monotonicity:** `a <= b => f(a) <= f(b)`.
135
+ 2. **Identity element:** `f(x, identity) == x`.
136
+
137
+ Write these mechanically. The agent should reach for proptest the moment any of these properties is checkable.
138
+
139
+ ## Derive `Arbitrary`
140
+
141
+ ```rust
142
+ use proptest_derive::Arbitrary;
143
+
144
+ #[derive(Debug, Clone, PartialEq, Arbitrary)]
145
+ struct Vec3 {
146
+ #[proptest(strategy = "-100.0..=100.0")]
147
+ x: f32,
148
+ #[proptest(strategy = "-100.0..=100.0")]
149
+ y: f32,
150
+ #[proptest(strategy = "-100.0..=100.0")]
151
+ z: f32,
152
+ }
153
+
154
+ proptest! {
155
+ #[test]
156
+ fn dot_product_is_commutative(a: Vec3, b: Vec3) {
157
+ prop_assert!((dot(&a, &b) - dot(&b, &a)).abs() < 1e-5);
158
+ }
159
+ }
160
+ ```
161
+
162
+ `#[derive(Arbitrary)]` auto-implements the strategy. Per-field `#[proptest(strategy = "...")]` overrides.
163
+
164
+ ## Stateful / state machine tests
165
+
166
+ For data structures with operations (queues, maps, trees), use `proptest-state-machine`:
167
+
168
+ ```rust
169
+ use proptest_state_machine::{ReferenceStateMachine, StateMachineTest};
170
+
171
+ struct MyQueueRef { state: VecDeque<i32> }
172
+ struct MyQueueSut { sut: MyQueue<i32> }
173
+
174
+ #[derive(Debug, Clone)]
175
+ enum Op { Push(i32), Pop }
176
+
177
+ impl ReferenceStateMachine for MyQueueRef {
178
+ type State = VecDeque<i32>;
179
+ type Transition = Op;
180
+
181
+ fn init_state() -> BoxedStrategy<Self::State> {
182
+ Just(VecDeque::new()).boxed()
183
+ }
184
+ fn transitions(_: &Self::State) -> BoxedStrategy<Self::Transition> {
185
+ prop_oneof![
186
+ any::<i32>().prop_map(Op::Push),
187
+ Just(Op::Pop),
188
+ ].boxed()
189
+ }
190
+ fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
191
+ match transition {
192
+ Op::Push(x) => state.push_back(*x),
193
+ Op::Pop => { state.pop_front(); }
194
+ }
195
+ state
196
+ }
197
+ }
198
+
199
+ impl StateMachineTest for MyQueueSut {
200
+ type SystemUnderTest = MyQueue<i32>;
201
+ type Reference = MyQueueRef;
202
+
203
+ fn init_test(_: &<Self::Reference as ReferenceStateMachine>::State) -> Self::SystemUnderTest {
204
+ MyQueue::new()
205
+ }
206
+ fn apply(mut sut: Self::SystemUnderTest, _: &VecDeque<i32>, transition: Op) -> Self::SystemUnderTest {
207
+ match transition {
208
+ Op::Push(x) => sut.push(x),
209
+ Op::Pop => { sut.pop(); }
210
+ }
211
+ sut
212
+ }
213
+ fn check_invariants(sut: &Self::SystemUnderTest, state: &VecDeque<i32>) {
214
+ assert_eq!(sut.len(), state.len());
215
+ // also check head/tail/iteration order...
216
+ }
217
+ }
218
+
219
+ proptest_state_machine::prop_state_machine! {
220
+ #[test]
221
+ fn queue_matches_vecdeque(sequential 1..50 => MyQueueSut);
222
+ }
223
+ ```
224
+
225
+ You define a reference implementation (`VecDeque` here), proptest fuzzes operations against both, asserts invariants every step. This is the technique for finding bugs in lock-free or complex containers.
226
+
227
+ ## Shrinking
228
+
229
+ When a property fails, proptest reduces the input to a minimal counter-example. For built-in strategies this is automatic. For custom strategies built with `prop_map`, shrinking goes through the underlying strategy. Avoid breaking shrinking with `prop_filter` (rejection sampling) over wide spaces; prefer `prop_flat_map` or directly-shaped strategies.
230
+
231
+ ## Regression corpus
232
+
233
+ When a property test fails, proptest writes the failing input to `proptest-regressions/<test_name>.txt`. Commit this directory. Future runs replay these failing inputs first, so the bug stays fixed forever.
234
+
235
+ ```
236
+ proptest-regressions/
237
+ └── parse_color.txt # commit this
238
+ ```
239
+
240
+ ## Insta — setup
241
+
242
+ `Cargo.toml`:
243
+
244
+ ```toml
245
+ [dev-dependencies]
246
+ insta = { version = "1", features = ["yaml", "json", "redactions", "filters"] }
247
+
248
+ [dependencies.serde_yaml]
249
+ version = "0.9"
250
+ optional = true
251
+ ```
252
+
253
+ Install the CLI:
254
+
255
+ ```bash
256
+ cargo install cargo-insta
257
+ ```
258
+
259
+ ## Insta — basic snapshots
260
+
261
+ ```rust
262
+ #[test]
263
+ fn renders_default_help() {
264
+ let output = render_help();
265
+ insta::assert_snapshot!(output);
266
+ }
267
+ ```
268
+
269
+ First run: creates `src/snapshots/mycrate__renders_default_help.snap.new`. Run `cargo insta review`, press `a` to accept, the `.new` extension is dropped. Subsequent runs diff against the committed snapshot; mismatches fail the test.
270
+
271
+ ## Insta — typed snapshots
272
+
273
+ ```rust
274
+ #[derive(Debug, serde::Serialize)]
275
+ struct Result {
276
+ status: String,
277
+ user: User,
278
+ duration_ms: u64,
279
+ }
280
+
281
+ #[test]
282
+ fn json_response() {
283
+ let value = compute();
284
+ insta::assert_json_snapshot!(value);
285
+ }
286
+
287
+ #[test]
288
+ fn yaml_response() {
289
+ insta::assert_yaml_snapshot!(value);
290
+ }
291
+
292
+ #[test]
293
+ fn debug_repr() {
294
+ insta::assert_debug_snapshot!(value);
295
+ }
296
+ ```
297
+
298
+ Choose:
299
+ - `assert_snapshot!` for `String`/`Display` output (CLI help, error messages, generated code).
300
+ - `assert_debug_snapshot!` for `{:?}` (Rust-internal data).
301
+ - `assert_json_snapshot!` for structured data crossing process boundaries.
302
+ - `assert_yaml_snapshot!` when YAML is easier to read in diffs.
303
+
304
+ ## Insta — redactions and filters
305
+
306
+ For values that change every run (timestamps, UUIDs, paths):
307
+
308
+ ```rust
309
+ #[test]
310
+ fn with_redactions() {
311
+ let value = ApiResponse {
312
+ id: uuid::Uuid::now_v7(),
313
+ created_at: jiff::Timestamp::now(),
314
+ body: "hello".into(),
315
+ };
316
+ insta::assert_json_snapshot!(value, {
317
+ ".id" => "[uuid]",
318
+ ".created_at" => "[timestamp]",
319
+ });
320
+ }
321
+ ```
322
+
323
+ For regex filters applied to all snapshots in a test:
324
+
325
+ ```rust
326
+ #[test]
327
+ fn with_filters() {
328
+ let mut settings = insta::Settings::clone_current();
329
+ settings.add_filter(r"/tmp/[a-z0-9-]+", "[TMP]");
330
+ settings.add_filter(r"\d+\.\d+ms", "[TIMING]");
331
+ settings.bind(|| {
332
+ let output = run_command();
333
+ insta::assert_snapshot!(output);
334
+ });
335
+ }
336
+ ```
337
+
338
+ `Settings::bind` scopes filters to the closure.
339
+
340
+ ## Insta workflow
341
+
342
+ 1. Write the test, run it. First run creates `.snap.new`.
343
+ 2. `cargo insta review` → interactive UI. Show diff, accept/reject.
344
+ 3. Accepted snapshots commit to the repo.
345
+ 4. Refactor code. Tests run; mismatches show as diffs.
346
+ 5. If the new output is correct, `cargo insta accept` (or selective `review`). If wrong, fix the code.
347
+
348
+ Pair with CI to fail builds when uncommitted `.snap.new` files exist:
349
+
350
+ ```bash
351
+ cargo nextest run
352
+ if find . -name "*.snap.new" | grep -q .; then
353
+ echo "Pending snapshots, run 'cargo insta review'"
354
+ exit 1
355
+ fi
356
+ ```
357
+
358
+ ## Inline snapshots
359
+
360
+ ```rust
361
+ #[test]
362
+ fn small_output() {
363
+ let value = compute();
364
+ insta::assert_snapshot!(value, @"hello world");
365
+ }
366
+ ```
367
+
368
+ The trailing `@"..."` string is the expected snapshot, stored in source. Useful when the value is short enough that pulling out a separate file is overkill. `cargo insta accept` updates them in-place.
369
+
370
+ ## Inline JSON snapshots
371
+
372
+ ```rust
373
+ #[test]
374
+ fn json_inline() {
375
+ insta::assert_json_snapshot!(value, @r###"
376
+ {
377
+ "status": "ok",
378
+ "count": 3
379
+ }
380
+ "###);
381
+ }
382
+ ```
383
+
384
+ ## Anti-patterns
385
+
386
+ 1. **Snapshots of unstable output.** If `HashMap` iteration order changes per run, snapshots will fail. Switch to `BTreeMap` or sort before snapshotting.
387
+ 2. **Massive snapshots.** A 10KB JSON dump where you really care about 3 fields. Either narrow to the fields, or accept that any refactor will require re-reviewing 10KB.
388
+ 3. **Snapshots that bake in implementation details.** "function called 3 times" is not a snapshot - it's a behavior assertion. Use a real assertion.
389
+ 4. **Skipping `cargo insta review`.** Accepting blind via `cargo insta accept --all` defeats the purpose. Always review.
390
+
391
+ ## Combining proptest + insta
392
+
393
+ ```rust
394
+ proptest! {
395
+ #[test]
396
+ fn random_inputs_render_consistently(input: ValidInput) {
397
+ let mut settings = insta::Settings::clone_current();
398
+ settings.set_snapshot_suffix(format!("{}", input.hash()));
399
+ settings.bind(|| {
400
+ insta::assert_snapshot!(render(&input));
401
+ });
402
+ }
403
+ }
404
+ ```
405
+
406
+ But honestly, this is rarely a fit. Proptest tests properties, insta tests output shape. Don't snapshot random inputs - that defeats both tools.
407
+
408
+ ## CI matrix recommendation
409
+
410
+ ```yaml
411
+ - name: Tests
412
+ run: cargo nextest run --all-features
413
+
414
+ - name: Property regressions (replay)
415
+ run: |
416
+ # The regression files in proptest-regressions/ replay first.
417
+ # Failures here mean a previously-fixed bug came back.
418
+ cargo nextest run --all-features --test-threads 1
419
+ ```
420
+
421
+ When a proptest finds a new failure, the regression file appears as a git diff - check it in.
422
+
423
+ ## What proptest cannot do
424
+
425
+ - Find bugs that require multi-process / multi-network coordination → integration tests + fault injection.
426
+ - Find concurrency bugs → use `loom` (see `concurrency.md`).
427
+ - Find performance regressions → use `criterion`.
428
+
429
+ But for any function with a domain (inputs to outputs), proptest can find more bugs than your unit tests. **Write the property first, derive the unit test second.**