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,354 @@
1
+ # Type-State and Newtype Patterns
2
+
3
+ The single highest-leverage thing Rust gives a coding agent: encode invariants in the type system so the compiler refuses incorrect code. The agent does not have to "remember" rules - the rules are physical.
4
+
5
+ ## The Two Core Patterns
6
+
7
+ 1. **Newtype wrappers for distinct semantic units.** Money, IDs, byte offsets, coordinate spaces - each gets its own tuple struct. The agent cannot pass meters where feet are expected, even though both are `f64` under the hood. This is the `euclid::Point<Screen>` vs `euclid::Point<World>` example Chris Allen called out.
8
+ 2. **Type-state for state machines.** Instead of a struct with a `status: enum { Draft, Validated, Persisted }` field and methods that check `if self.status == ...`, model each state as its own type. Transitions are method calls that consume `self` and return a new type. Illegal transitions become unrepresentable.
9
+
10
+ ## Newtype Wrapper Cookbook
11
+
12
+ ### Domain IDs
13
+
14
+ ```rust
15
+ use uuid::Uuid;
16
+
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
18
+ #[serde(transparent)]
19
+ pub struct UserId(Uuid);
20
+
21
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
22
+ #[serde(transparent)]
23
+ pub struct ProductId(Uuid);
24
+
25
+ impl UserId {
26
+ pub fn new() -> Self { Self(Uuid::now_v7()) }
27
+ pub fn as_uuid(&self) -> &Uuid { &self.0 }
28
+ }
29
+
30
+ impl ProductId {
31
+ pub fn new() -> Self { Self(Uuid::now_v7()) }
32
+ pub fn as_uuid(&self) -> &Uuid { &self.0 }
33
+ }
34
+
35
+ // `fn buy(user: UserId, product: ProductId)` cannot be called with arguments swapped.
36
+ ```
37
+
38
+ `#[serde(transparent)]` keeps JSON/SQL round-trips identical to a bare `Uuid` - the wrapper is purely a compile-time discipline.
39
+
40
+ ### Quantities with Phantom Type Tags
41
+
42
+ ```rust
43
+ use core::marker::PhantomData;
44
+ use core::ops::{Add, Sub, Mul};
45
+
46
+ #[derive(Debug, Clone, Copy)]
47
+ pub struct Quantity<Unit> {
48
+ raw: f64,
49
+ _unit: PhantomData<Unit>,
50
+ }
51
+
52
+ // Tag types - zero-sized, never instantiated.
53
+ pub struct Meters;
54
+ pub struct Feet;
55
+ pub struct Seconds;
56
+
57
+ impl<U> Quantity<U> {
58
+ pub const fn new(value: f64) -> Self {
59
+ Self { raw: value, _unit: PhantomData }
60
+ }
61
+ pub fn raw(self) -> f64 { self.raw }
62
+ }
63
+
64
+ // Adding same-unit quantities: allowed.
65
+ impl<U> Add for Quantity<U> {
66
+ type Output = Self;
67
+ fn add(self, rhs: Self) -> Self { Self::new(self.raw + rhs.raw) }
68
+ }
69
+
70
+ // Subtraction: allowed.
71
+ impl<U> Sub for Quantity<U> {
72
+ type Output = Self;
73
+ fn sub(self, rhs: Self) -> Self { Self::new(self.raw - rhs.raw) }
74
+ }
75
+
76
+ // Multiplying by scalar: allowed.
77
+ impl<U> Mul<f64> for Quantity<U> {
78
+ type Output = Self;
79
+ fn mul(self, rhs: f64) -> Self { Self::new(self.raw * rhs) }
80
+ }
81
+
82
+ // Conversions are explicit, named methods - never `From`/`Into` between units.
83
+ impl Quantity<Meters> {
84
+ pub fn to_feet(self) -> Quantity<Feet> {
85
+ Quantity::new(self.raw * 3.280_84)
86
+ }
87
+ }
88
+ ```
89
+
90
+ Now:
91
+
92
+ ```rust
93
+ let distance: Quantity<Meters> = Quantity::new(100.0);
94
+ let height: Quantity<Feet> = Quantity::new(50.0);
95
+ let combined = distance + height; // ❌ compile error
96
+ let combined = distance + height.to_feet().to_meters_oops(); // ❌ no such method
97
+ let combined = distance + distance; // ✅
98
+ ```
99
+
100
+ The agent cannot accidentally mix units. Refactors that change a quantity's underlying unit are caught at compile time everywhere the type flows.
101
+
102
+ ### Byte Offsets vs Character Offsets
103
+
104
+ ```rust
105
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
106
+ pub struct ByteOffset(pub u32);
107
+
108
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
109
+ pub struct CharOffset(pub u32);
110
+
111
+ impl ByteOffset {
112
+ pub fn add(self, delta: u32) -> Self { Self(self.0 + delta) }
113
+ }
114
+
115
+ // Converting between them is a function on the actual text.
116
+ pub fn byte_to_char(text: &str, byte: ByteOffset) -> Option<CharOffset> {
117
+ text.get(..byte.0 as usize).map(|prefix| CharOffset(prefix.chars().count() as u32))
118
+ }
119
+ ```
120
+
121
+ A function signature `fn slice(text: &str, start: ByteOffset, end: ByteOffset)` cannot be called with character offsets. The UTF-8 boundary bug is now a compile error.
122
+
123
+ ### Currency
124
+
125
+ ```rust
126
+ use rust_decimal::Decimal;
127
+
128
+ pub struct Krw;
129
+ pub struct Usd;
130
+ pub struct Jpy;
131
+
132
+ #[derive(Debug, Clone, Copy)]
133
+ pub struct Money<Currency> {
134
+ amount: Decimal,
135
+ _ccy: PhantomData<Currency>,
136
+ }
137
+
138
+ impl<C> Money<C> {
139
+ pub const fn new(amount: Decimal) -> Self { Self { amount, _ccy: PhantomData } }
140
+ }
141
+
142
+ impl<C> Add for Money<C> {
143
+ type Output = Self;
144
+ fn add(self, rhs: Self) -> Self { Self::new(self.amount + rhs.amount) }
145
+ }
146
+
147
+ // No blanket From<Money<X>> for Money<Y> - conversions go through an explicit
148
+ // FX rate function that takes a `Rate<From, To>` argument.
149
+
150
+ pub struct Rate<From, To> {
151
+ factor: Decimal,
152
+ _from: PhantomData<From>,
153
+ _to: PhantomData<To>,
154
+ }
155
+
156
+ impl<From, To> Money<From> {
157
+ pub fn convert(self, rate: Rate<From, To>) -> Money<To> {
158
+ Money::new(self.amount * rate.factor)
159
+ }
160
+ }
161
+ ```
162
+
163
+ The agent cannot add KRW and USD by accident. They cannot convert without a rate. They cannot apply a USD→JPY rate to a KRW value.
164
+
165
+ ### Paths Rooted at Different Bases
166
+
167
+ ```rust
168
+ use std::path::{Path, PathBuf};
169
+
170
+ /// A path guaranteed to be relative to the project root.
171
+ #[derive(Debug, Clone)]
172
+ pub struct ProjectRel(PathBuf);
173
+
174
+ /// A path guaranteed to be relative to the user's home directory.
175
+ #[derive(Debug, Clone)]
176
+ pub struct HomeRel(PathBuf);
177
+
178
+ impl ProjectRel {
179
+ pub fn new(path: impl AsRef<Path>) -> Result<Self, PathError> {
180
+ let path = path.as_ref();
181
+ if path.is_absolute() { return Err(PathError::NotRelative); }
182
+ if path.components().any(|c| matches!(c, std::path::Component::ParentDir)) {
183
+ return Err(PathError::EscapesRoot);
184
+ }
185
+ Ok(Self(path.to_path_buf()))
186
+ }
187
+
188
+ pub fn resolve(&self, project_root: &Path) -> PathBuf {
189
+ project_root.join(&self.0)
190
+ }
191
+ }
192
+ ```
193
+
194
+ The agent's path-handling code now distinguishes between project-relative and home-relative paths at the type level. A function taking `ProjectRel` cannot be called with a `HomeRel`.
195
+
196
+ ## Type-State State Machines
197
+
198
+ Encode the lifecycle of a value as a sequence of types. Each transition consumes the previous state and returns the next.
199
+
200
+ ### HTTP Request Builder
201
+
202
+ ```rust
203
+ pub struct RequestBuilder<State> {
204
+ url: String,
205
+ method: Method,
206
+ headers: HeaderMap,
207
+ body: Option<Vec<u8>>,
208
+ _state: PhantomData<State>,
209
+ }
210
+
211
+ pub struct NeedsUrl;
212
+ pub struct NeedsMethod;
213
+ pub struct Ready;
214
+
215
+ impl RequestBuilder<NeedsUrl> {
216
+ pub fn new() -> Self {
217
+ Self {
218
+ url: String::new(),
219
+ method: Method::GET,
220
+ headers: HeaderMap::new(),
221
+ body: None,
222
+ _state: PhantomData,
223
+ }
224
+ }
225
+ pub fn url(mut self, url: impl Into<String>) -> RequestBuilder<NeedsMethod> {
226
+ self.url = url.into();
227
+ RequestBuilder { url: self.url, method: self.method, headers: self.headers, body: self.body, _state: PhantomData }
228
+ }
229
+ }
230
+
231
+ impl RequestBuilder<NeedsMethod> {
232
+ pub fn method(mut self, method: Method) -> RequestBuilder<Ready> {
233
+ self.method = method;
234
+ RequestBuilder { url: self.url, method: self.method, headers: self.headers, body: self.body, _state: PhantomData }
235
+ }
236
+ }
237
+
238
+ // .header(), .body() available in any state that has at least URL.
239
+ impl<S> RequestBuilder<S> {
240
+ pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
241
+ self.headers.insert(name, value);
242
+ self
243
+ }
244
+ }
245
+
246
+ // .send() only available once URL and method are set.
247
+ impl RequestBuilder<Ready> {
248
+ pub async fn send(self, client: &Client) -> reqwest::Result<Response> { /* ... */ }
249
+ }
250
+ ```
251
+
252
+ `client.send(RequestBuilder::new().send(...))` - compile error. The agent has to fill in the required steps. The IDE autocomplete also reflects only the legal next steps.
253
+
254
+ ### File Handles
255
+
256
+ ```rust
257
+ pub struct File<State> {
258
+ fd: RawFd,
259
+ _state: PhantomData<State>,
260
+ }
261
+
262
+ pub struct Open;
263
+ pub struct Locked;
264
+ pub struct Closed;
265
+
266
+ impl File<Open> {
267
+ pub fn open(path: &Path) -> std::io::Result<Self> { /* ... */ }
268
+ pub fn lock_exclusive(self) -> std::io::Result<File<Locked>> { /* flock */ }
269
+ pub fn close(self) -> std::io::Result<File<Closed>> { /* ... */ }
270
+ }
271
+
272
+ impl File<Locked> {
273
+ pub fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { /* ... */ }
274
+ pub fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { /* ... */ }
275
+ pub fn unlock(self) -> std::io::Result<File<Open>> { /* ... */ }
276
+ }
277
+
278
+ impl File<Closed> {
279
+ // No methods. The type only exists to be dropped.
280
+ }
281
+ ```
282
+
283
+ You cannot `read()` an unlocked file. You cannot `close()` while holding a lock without unlocking first. You cannot use a closed file at all - it has no methods.
284
+
285
+ ## Sealed Traits
286
+
287
+ Sometimes you want a closed set of types implementing a trait, defined by the crate, not extensible by downstream. The sealed trait pattern:
288
+
289
+ ```rust
290
+ mod sealed {
291
+ pub trait Sealed {}
292
+ }
293
+
294
+ pub trait Renderer: sealed::Sealed {
295
+ fn render(&self, frame: &mut Frame);
296
+ }
297
+
298
+ pub struct OpenGl;
299
+ pub struct Vulkan;
300
+ pub struct Metal;
301
+
302
+ impl sealed::Sealed for OpenGl {}
303
+ impl sealed::Sealed for Vulkan {}
304
+ impl sealed::Sealed for Metal {}
305
+
306
+ impl Renderer for OpenGl { fn render(&self, f: &mut Frame) { /* ... */ } }
307
+ impl Renderer for Vulkan { fn render(&self, f: &mut Frame) { /* ... */ } }
308
+ impl Renderer for Metal { fn render(&self, f: &mut Frame) { /* ... */ } }
309
+ ```
310
+
311
+ Downstream code cannot add new `impl Renderer for Whatever` because they cannot implement `sealed::Sealed` (its module is private). Useful when you want trait dispatch but maintain the invariant that you control all implementations.
312
+
313
+ ## NonEmpty Collections
314
+
315
+ ```rust
316
+ pub struct NonEmptyVec<T> {
317
+ head: T,
318
+ tail: Vec<T>,
319
+ }
320
+
321
+ #[derive(Debug, thiserror::Error)]
322
+ #[error("vector was empty")]
323
+ pub struct Empty;
324
+
325
+ impl<T> NonEmptyVec<T> {
326
+ pub fn try_from_vec(mut v: Vec<T>) -> Result<Self, Empty> {
327
+ if v.is_empty() { return Err(Empty); }
328
+ let tail = v.split_off(1);
329
+ let head = v.into_iter().next().expect("checked non-empty");
330
+ Ok(Self { head, tail })
331
+ }
332
+
333
+ pub fn first(&self) -> &T { &self.head }
334
+ pub fn len(&self) -> usize { self.tail.len() + 1 }
335
+ }
336
+ ```
337
+
338
+ Functions taking `NonEmptyVec<T>` cannot receive an empty vector. The `first()` method returns `&T`, not `Option<&T>`. The agent never has to write `match v.first() { Some(x) => ..., None => panic!(...) }` again.
339
+
340
+ ## When NOT to Newtype
341
+
342
+ - For one-off internal computations where the unit lives in a single function and never crosses a boundary.
343
+ - When the wrapper does not change behavior or invariants vs. the underlying type (e.g., a `struct Count(u32)` that is only ever used in one struct).
344
+ - When `From`/`Into` conversions would be ergonomic but would defeat the purpose (if you find yourself wanting `impl From<UserId> for Uuid`, you do not want a newtype - you want a type alias).
345
+
346
+ The cost of a newtype is one tuple struct + the impls you need. The break-even is around three uses across different functions, or any use that crosses an API boundary.
347
+
348
+ ## When NOT to Use Type-State
349
+
350
+ - When the state space is small and transitions are simple (`Option<T>` and `Result<T, E>` are state machines already).
351
+ - When the type-state would force runtime branching upward (e.g., reading "should this run as Open or Locked?" from config means you store `Box<dyn FileLike>` anyway).
352
+ - When the API is consumed by code that does not know the state at compile time (heterogeneous collections, dynamic dispatch boundaries).
353
+
354
+ In those cases, regular enum-tagged states are correct. The line is: **can the call site know the state statically?** If yes, type-state. If no, enum-with-tag.
@@ -0,0 +1,250 @@
1
+ # Unsafe Discipline
2
+
3
+ The reason Chris Allen's "implementing a persistent memory arena in Rust was not hard" works: the unsafe surface area is microscopic, it lives behind one newtype with one constructor, and every block has a SAFETY comment that names a specific invariant. Coding agents follow the pattern mechanically once the shape is established.
4
+
5
+ ## The Three Required Components
6
+
7
+ Every `unsafe` block needs all three. No exceptions.
8
+
9
+ 1. **Safe wrapper.** No `unsafe fn` or raw pointer types in the crate's public API. If a caller needs to construct an instance, the constructor either does the work safely or is `unsafe` with a documented contract.
10
+ 2. **SAFETY comment.** A `// SAFETY:` line within 5 lines above the `unsafe { ... }` block, stating which invariant is upheld and where it comes from. Generic phrases ("this is safe because we checked") fail review.
11
+ 3. **miri proof.** A test that exercises the unsafe path under `cargo +nightly miri nextest run`. If the path cannot be exercised under miri (FFI, syscalls), provide an alternate proof and gate behind a feature flag ending in `-skip-miri`.
12
+
13
+ ## The Wrapper Pattern (`NonNull<T>` style)
14
+
15
+ Reference the screenshot Chris Allen quoted - `std::ptr::NonNull<T>`. Mirror this shape for every raw pointer, raw slice, raw transmute, or uninit memory operation in your own code.
16
+
17
+ ```rust
18
+ use core::marker::PhantomData;
19
+ use core::ptr::NonNull;
20
+
21
+ /// A non-null, properly aligned, initialized pointer that does not alias.
22
+ ///
23
+ /// All invariants are upheld by [`Self::new`] (checked) or [`Self::new_unchecked`]
24
+ /// (delegated to the caller's contract). Once you hold an `InitPtr<T>`, every
25
+ /// public method on it is safe to call.
26
+ #[derive(Debug)]
27
+ #[repr(transparent)]
28
+ pub struct InitPtr<T> {
29
+ inner: NonNull<T>,
30
+ _marker: PhantomData<T>,
31
+ }
32
+
33
+ // Send/Sync are NOT automatic for raw-pointer-bearing types. Decide deliberately.
34
+ // SAFETY: `InitPtr<T>` owns no concurrency state of its own; whether it is
35
+ // Send/Sync depends on `T`. The bounds below mirror `Box<T>`.
36
+ unsafe impl<T: Send> Send for InitPtr<T> {}
37
+ unsafe impl<T: Sync> Sync for InitPtr<T> {}
38
+
39
+ impl<T> InitPtr<T> {
40
+ /// Wrap a raw pointer after checking alignment and non-null. The
41
+ /// initialization invariant is not statically checkable here; callers must
42
+ /// only feed pointers to memory that was written before this call.
43
+ pub fn new(ptr: *mut T) -> Option<Self> {
44
+ if !ptr.is_aligned() {
45
+ return None;
46
+ }
47
+ // SAFETY: alignment checked above; `NonNull::new` filters null. The
48
+ // caller is documented to provide an initialized location.
49
+ NonNull::new(ptr).map(|inner| Self { inner, _marker: PhantomData })
50
+ }
51
+
52
+ /// Wrap a raw pointer the caller asserts is valid.
53
+ ///
54
+ /// # Safety
55
+ ///
56
+ /// - `ptr` is non-null.
57
+ /// - `ptr` is aligned to `align_of::<T>()`.
58
+ /// - `*ptr` is initialized at the time of this call.
59
+ /// - For the lifetime of the returned value, no other `&T` or `&mut T`
60
+ /// aliases `*ptr`.
61
+ pub unsafe fn new_unchecked(ptr: *mut T) -> Self {
62
+ // SAFETY: caller upholds non-null per the function contract.
63
+ Self { inner: unsafe { NonNull::new_unchecked(ptr) }, _marker: PhantomData }
64
+ }
65
+
66
+ pub fn as_ref(&self) -> &T {
67
+ // SAFETY: the constructor's invariants guarantee `inner` points at an
68
+ // initialized, aligned, non-aliased `T`. Reborrowing through `&self`
69
+ // ties the resulting lifetime to `self`, enforcing the rest via
70
+ // standard borrow rules.
71
+ unsafe { self.inner.as_ref() }
72
+ }
73
+
74
+ pub fn as_mut(&mut self) -> &mut T {
75
+ // SAFETY: `&mut self` proves no other reference can alias `inner` for
76
+ // the lifetime of the returned `&mut T`; remaining invariants come
77
+ // from construction.
78
+ unsafe { self.inner.as_mut() }
79
+ }
80
+ }
81
+ ```
82
+
83
+ The list of features this single shape gives you:
84
+
85
+ - The agent cannot construct `InitPtr<T>` without going through a checked path or accepting the `unsafe` obligation explicitly.
86
+ - The agent cannot leak the raw pointer; `as_ref` / `as_mut` return safe references with proper lifetimes.
87
+ - The agent cannot accidentally Send/Sync where it shouldn't - the `unsafe impl` is explicit per-bound.
88
+ - `#[repr(transparent)]` means the type is layout-compatible with `*mut T` for FFI, without exposing the raw pointer.
89
+
90
+ ## SAFETY Comment Grammar
91
+
92
+ Every comment maps one-to-one to an invariant. Format:
93
+
94
+ ```rust
95
+ // SAFETY: <which invariant is upheld>: <how it is established here>.
96
+ ```
97
+
98
+ Anti-examples (do not pass review):
99
+
100
+ ```rust
101
+ // SAFETY: this is fine
102
+ // SAFETY: we know what we're doing
103
+ // SAFETY: the caller will not pass null
104
+ // SAFETY: tested
105
+ ```
106
+
107
+ Good examples:
108
+
109
+ ```rust
110
+ // SAFETY: `len <= self.capacity` was checked at line 87 and `self.ptr` was
111
+ // allocated by the same allocator we are reading through.
112
+
113
+ // SAFETY: `read_volatile` requires alignment and non-null; both hold because
114
+ // `self.inner` is an `InitPtr<T>` whose constructor enforced them.
115
+
116
+ // SAFETY: We hold `&mut self`, so no concurrent reader exists. The slice
117
+ // reference is dropped before the next `&self` borrow because we shrink the
118
+ // returned scope manually.
119
+ ```
120
+
121
+ ## Persistent Memory Arena Pattern (the Chris Allen example)
122
+
123
+ A persistent memory arena (PMA) is an `mmap`-backed bump allocator that survives process restarts. It is the classic "lots of unsafe under one safe surface" project.
124
+
125
+ Shape:
126
+
127
+ ```rust
128
+ pub struct Arena {
129
+ map: Mmap, // wraps `mmap(2)` - safe wrapper from `memmap2` crate
130
+ head: AtomicUsize, // current bump offset, atomic for multi-writer if needed
131
+ }
132
+
133
+ impl Arena {
134
+ pub fn open(path: &Path, capacity: usize) -> std::io::Result<Self> { /* mmap, init header */ }
135
+
136
+ pub fn alloc<T>(&self, value: T) -> Result<Handle<T>, ArenaFull> {
137
+ let layout = Layout::new::<T>();
138
+ let aligned = align_up(self.head.load(Acquire), layout.align());
139
+ let next = aligned.checked_add(layout.size()).ok_or(ArenaFull)?;
140
+ if next > self.map.len() { return Err(ArenaFull); }
141
+ // CAS the head forward; retry on contention.
142
+ // ... omitted for brevity ...
143
+
144
+ // SAFETY: `aligned + layout.size() <= self.map.len()` was just proven.
145
+ // The mmap region is exclusively owned by this arena while we hold the
146
+ // bump. `aligned` is aligned to `layout.align()` by `align_up`. No
147
+ // other writer can have observed this offset because the CAS above
148
+ // returned `Ok`.
149
+ let ptr = unsafe { self.map.as_mut_ptr().add(aligned) as *mut T };
150
+ // SAFETY: `ptr` is non-null (mmap base + offset), aligned (above),
151
+ // exclusively owned (CAS), and we are about to initialize it.
152
+ unsafe { ptr::write(ptr, value) };
153
+
154
+ // SAFETY: same invariants; we wrap the now-initialized pointer in the
155
+ // safe handle which encapsulates further accesses.
156
+ Ok(unsafe { Handle::new_unchecked(ptr, self) })
157
+ }
158
+ }
159
+
160
+ pub struct Handle<'a, T> {
161
+ inner: InitPtr<T>,
162
+ _arena: PhantomData<&'a Arena>,
163
+ }
164
+ ```
165
+
166
+ Three `unsafe` blocks, three SAFETY comments, one safe handle type emerging on the other side. The agent now uses `Handle<T>` everywhere - never `*mut T`.
167
+
168
+ ## Miri Invocation
169
+
170
+ ```bash
171
+ # install once
172
+ rustup install nightly
173
+ rustup component add miri rust-src --toolchain nightly
174
+
175
+ # run on every change that touches unsafe
176
+ MIRIFLAGS="-Zmiri-strict-provenance -Zmiri-symbolic-alignment-check" \
177
+ cargo +nightly miri nextest run --all-features
178
+ ```
179
+
180
+ What miri catches that the borrow checker cannot:
181
+
182
+ - Use-after-free
183
+ - Double-free
184
+ - Reads of uninitialized memory
185
+ - Pointer-from-integer reconstruction that violates strict provenance
186
+ - Alignment lies (transmuting unaligned data)
187
+ - Stacked borrows / Tree borrows aliasing violations
188
+ - Data races (single-threaded model, but catches concurrent access through `UnsafeCell` misuse)
189
+ - Atomic ordering bugs in some patterns
190
+ - Memory leaks (with `-Zmiri-track-pointer-tag`)
191
+
192
+ ## When Miri Cannot Run
193
+
194
+ Certain paths are off-limits for miri: most syscalls beyond a curated allowlist, real network I/O, `std::process` calls, OS-specific FFI, hardware-dependent intrinsics on non-x86. Strategy:
195
+
196
+ 1. **Isolate.** Put the un-mirifiable code in its own module behind `#[cfg(feature = "ffi-real")]` or similar.
197
+ 2. **Mock at the boundary.** For everything below the FFI boundary, write a safe Rust fake (a `Vec<u8>`-backed "disk", a fake clock, an in-memory socket pair). Expose it as a trait the production code consumes.
198
+ 3. **Test the fake under miri.** The fake implementation exercises the same logic minus the syscall. If the logic is unsafe (raw pointer manipulation in the fake "disk" buffer), miri catches the bug.
199
+ 4. **Test the real path under regular `cargo test`.** With `cargo nextest run --features ffi-real`. No miri, but the surface area is now just the syscall boundary.
200
+ 5. **Document.** A `# Safety` section in the rustdoc names the obligations the FFI puts on us, and a `# Testing` section explains the mock-vs-real split.
201
+
202
+ ## Loom for Concurrency
203
+
204
+ When `unsafe` participates in a concurrent algorithm (lock-free queue, hazard pointers, custom Arc), miri's single-thread model is insufficient. Use `loom`:
205
+
206
+ ```rust
207
+ #[cfg(loom)]
208
+ use loom::sync::atomic::{AtomicUsize, Ordering};
209
+ #[cfg(not(loom))]
210
+ use std::sync::atomic::{AtomicUsize, Ordering};
211
+
212
+ #[cfg(test)]
213
+ mod tests {
214
+ use super::*;
215
+
216
+ #[test]
217
+ #[cfg(loom)]
218
+ fn concurrent_push_pop() {
219
+ loom::model(|| {
220
+ let queue = std::sync::Arc::new(MyQueue::new());
221
+ let q1 = queue.clone();
222
+ let q2 = queue.clone();
223
+ let h1 = loom::thread::spawn(move || q1.push(1));
224
+ let h2 = loom::thread::spawn(move || q2.pop());
225
+ h1.join().unwrap();
226
+ h2.join().unwrap();
227
+ });
228
+ }
229
+ }
230
+ ```
231
+
232
+ Run: `RUSTFLAGS="--cfg loom" cargo test --release`. Loom exhaustively explores thread interleavings for the test scope. Combined with miri on the single-thread paths, you have machine-checked soundness over the full state space.
233
+
234
+ ## The Forbidden List
235
+
236
+ Reject in code review, automatic CI fail:
237
+
238
+ - `unsafe { ... }` with no SAFETY comment within 5 lines above.
239
+ - `unsafe { unsafe_op_a(); unsafe_op_b(); }` (multiple unsafe ops in one block - split them, one SAFETY each). Clippy: `multiple_unsafe_ops_per_block`.
240
+ - `unsafe fn` exposed publicly without a documented `# Safety` section in rustdoc.
241
+ - `std::mem::transmute` for anything but lifetime extension on the same layout (and that should usually be `core::mem::transmute_copy` or `bytemuck::cast` if the relayout is well-defined).
242
+ - `std::ptr::read_unaligned` / `write_unaligned` without a comment explaining why aligned access is impossible.
243
+ - `from_raw_parts` / `from_raw_parts_mut` without proving the source pointer's provenance covers the entire slice.
244
+ - `Arc::get_mut_unchecked`, `Box::leak` to bypass ownership, `MaybeUninit::assume_init` on partially-initialized data.
245
+ - `unsafe impl Send`, `unsafe impl Sync` on types containing raw pointers, without a comment naming exactly which interior-mutability rule is upheld.
246
+ - Any `unsafe` block whose justification depends on "in practice this never happens".
247
+
248
+ ## The One-Line Summary
249
+
250
+ > Wrap once. Prove once. Test under miri. Never let `unsafe` escape.