@agentsfleet/orly 0.5.0
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.
- package/.githooks/pre-commit +42 -0
- package/.githooks/pre-push +67 -0
- package/AGENTS.md +348 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SOUL.md +90 -0
- package/audits/agents-md.md +497 -0
- package/audits/agents-md.sh +353 -0
- package/audits/cross-tier-rates.sh +105 -0
- package/audits/data.sh +185 -0
- package/audits/deinit-pairs.sh +224 -0
- package/audits/design-tokens.sh +143 -0
- package/audits/doc-read.sh +183 -0
- package/audits/error-codes.sh +221 -0
- package/audits/fixtures/clean.diff +15 -0
- package/audits/fixtures/dirty.diff +16 -0
- package/audits/lifecycle-anchors.sh +22 -0
- package/audits/logging.sh +217 -0
- package/audits/msid-ui.sh +144 -0
- package/audits/parity-dispatch.sh +51 -0
- package/audits/rule-ledger-lib.sh +264 -0
- package/audits/rule-ledger.sh +225 -0
- package/audits/rule-paths.sh +108 -0
- package/audits/spec-template.sh +258 -0
- package/audits/sql-mod.sh +102 -0
- package/audits/ufs.sh +303 -0
- package/bin/orly +26 -0
- package/core/operating-model.md +256 -0
- package/dispatch/edit_rules.md +69 -0
- package/dispatch/lib.sh +213 -0
- package/dispatch/lifecycle.md +187 -0
- package/dispatch/name_architecture.md +93 -0
- package/dispatch/verify.md +111 -0
- package/dispatch/write_any.md +740 -0
- package/dispatch/write_any.sh +49 -0
- package/dispatch/write_auth.md +36 -0
- package/dispatch/write_changelog.md +64 -0
- package/dispatch/write_documentation.md +45 -0
- package/dispatch/write_go.md +24 -0
- package/dispatch/write_http.md +45 -0
- package/dispatch/write_mdx.md +7 -0
- package/dispatch/write_python.md +8 -0
- package/dispatch/write_rust.md +38 -0
- package/dispatch/write_shell.md +8 -0
- package/dispatch/write_spec.md +105 -0
- package/dispatch/write_sql.md +112 -0
- package/dispatch/write_sql.sh +36 -0
- package/dispatch/write_ts_adhere_bun.md +493 -0
- package/dispatch/write_ts_adhere_bun.sh +37 -0
- package/dispatch/write_zig.md +852 -0
- package/dispatch/write_zig.sh +40 -0
- package/docs/CHANGELOG_VOICE.md +18 -0
- package/docs/DISPATCH_ARCHITECTURE.md +664 -0
- package/docs/DISPATCH_REVIEW_DISPOSITION.md +65 -0
- package/docs/DOCUMENTATION_RULES.md +209 -0
- package/docs/EXECUTE_DOC_READS.md +29 -0
- package/docs/HARNESS_VERIFY_OUTPUT.md +40 -0
- package/docs/LIFECYCLE_PATTERNS.md +316 -0
- package/docs/LOGGING_STANDARD.md +397 -0
- package/docs/ORLY_ARCHITECTURE.md +129 -0
- package/docs/REST_API_DESIGN_GUIDELINES.md +783 -0
- package/docs/RULE_ENFORCEMENT.md +41 -0
- package/docs/SCHEMA_CONVENTIONS.md +93 -0
- package/docs/TEMPLATE.md +411 -0
- package/docs/VERIFY_TIERS.md +57 -0
- package/docs/greptile-learnings/README.md +51 -0
- package/docs/greptile-learnings/RULES.md +1045 -0
- package/fixtures/registry-invalid.json +1 -0
- package/fixtures/registry-valid.json +18 -0
- package/fixtures/unclosed-pack-block.md +8 -0
- package/package.json +63 -0
- package/packs/language/go/rules.md +24 -0
- package/packs/language/mdx/rules.md +7 -0
- package/packs/language/python/rules.md +8 -0
- package/packs/language/rust/rules.md +38 -0
- package/packs/language/shell/rules.md +8 -0
- package/registry.json +405 -0
- package/schemas/evidence.schema.json +43 -0
- package/schemas/profile.schema.json +12 -0
- package/schemas/registry.schema.json +12 -0
- package/skills/orly-babysit-prs/SKILL.md +326 -0
- package/skills/orly-spec-new/SKILL.md +157 -0
- package/skills/orly-write-integration-test/SKILL.md +470 -0
- package/skills/orly-write-unit-test/SKILL.md +520 -0
- package/src/cli.test.ts +70 -0
- package/src/cli.ts +287 -0
- package/src/config.test.ts +226 -0
- package/src/config.ts +288 -0
- package/src/criteria.test.ts +137 -0
- package/src/criteria.ts +166 -0
- package/src/criteria_spec.ts +127 -0
- package/src/criteria_support.ts +53 -0
- package/src/gates.test.ts +183 -0
- package/src/gates.ts +169 -0
- package/src/gates_spec.test.ts +98 -0
- package/src/gates_test_support.ts +194 -0
- package/src/git_env.ts +20 -0
- package/src/install.test.ts +514 -0
- package/src/install.ts +447 -0
- package/src/install_nested.test.ts +32 -0
- package/src/model.test.ts +31 -0
- package/src/model.ts +245 -0
- package/src/references.test.ts +47 -0
- package/src/references.ts +147 -0
- package/src/render.test.ts +60 -0
- package/src/render.ts +94 -0
- package/src/surfaces.test.ts +43 -0
- package/src/surfaces.ts +89 -0
- package/src/validation.test.ts +11 -0
- package/src/validation.ts +87 -0
- package/src/verify.test.ts +13 -0
- package/src/verify.ts +59 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# LIFECYCLE_PATTERNS — init/deinit/defer/errdefer convention for Zig
|
|
2
|
+
|
|
3
|
+
The contract for memory ownership and cleanup in Zig structs. Sister doc to `dispatch/write_zig.md` (which covers per-edit discipline) and `LOGGING_STANDARD.md` (which covers the cross-language wire format).
|
|
4
|
+
|
|
5
|
+
Pre-design rules, decisive defaults, and the anti-patterns each rule exists to prevent. Every rule has a one-line "why" so you can judge edge cases.
|
|
6
|
+
|
|
7
|
+
## §1 · Scope
|
|
8
|
+
|
|
9
|
+
Triggers on every `Edit`/`Write` that adds, removes, or reshapes lifecycle methods on a Zig struct:
|
|
10
|
+
|
|
11
|
+
- New `pub fn init(`
|
|
12
|
+
- New `pub fn deinit(`
|
|
13
|
+
- New `errdefer` or `defer` adjacent to an allocation
|
|
14
|
+
- New struct field that holds heap memory, an arena, or an opaque handle
|
|
15
|
+
- Refactor of an existing init/deinit pair
|
|
16
|
+
- Cross-thread allocator handoff
|
|
17
|
+
|
|
18
|
+
Out of scope:
|
|
19
|
+
|
|
20
|
+
- Stack-only structs with no allocation (zero-cost values; no lifecycle).
|
|
21
|
+
- Test-only mocks/stubs in `*_test.zig` that fake init/deinit for assertion purposes.
|
|
22
|
+
|
|
23
|
+
The **LIFECYCLE GATE** (`dispatch/write_zig.md`, lifecycle) sits on top of this file and audits init/deinit pairing + errdefer placement on every Zig edit. It is **not** a substitute for the **PUB GATE** (`dispatch/write_zig.md`, pub-surface) — see §11 for the carve-out.
|
|
24
|
+
|
|
25
|
+
## §2 · Today's de-facto convention (survey-derived)
|
|
26
|
+
|
|
27
|
+
Documented honestly, not aspirationally. As of this milestone:
|
|
28
|
+
|
|
29
|
+
- Init/deinit pairing is inconsistent. Some structs have both (`HttpServer.init` + `HttpServer.deinit`), some have init but no deinit (caller assumed-omniscient), a handful have deinit but no init (tail end of refactors).
|
|
30
|
+
- `errdefer` placement varies. Sometimes immediately after the alloc it protects (correct), sometimes batched at the bottom of init (fragile if init reorders), sometimes missing entirely (leaks on init failure).
|
|
31
|
+
- Allocator storage varies. Some structs store `allocator: std.mem.Allocator` (then `deinit(self: *Self) void`), others omit it (then `deinit(self: *Self, allocator: std.mem.Allocator) void`). Both work; the inconsistency makes call-site reading slow.
|
|
32
|
+
- Arena vs GPA boundaries are vague. A handful of subsystems mix per-allocation `defer` with arena `reset` in the same scope. Either pattern is fine alone; mixing is a leak class.
|
|
33
|
+
- Ownership transfer at the call site is undocumented. `MyStruct.init(alloc, slice)` may or may not take ownership of `slice`; readers must grep `init` body to find out.
|
|
34
|
+
|
|
35
|
+
The proposed convention below is what every new struct must conform to and what the fix-pass converges existing structs toward.
|
|
36
|
+
|
|
37
|
+
## §3 · Bun's convention (synthesized from `~/Projects/oss/bun/src`)
|
|
38
|
+
|
|
39
|
+
Reference patterns, with file:line citations:
|
|
40
|
+
|
|
41
|
+
**Heap-returned init with immediate errdefer** — `~/Projects/oss/bun/src/Watcher.zig:65-90`:
|
|
42
|
+
|
|
43
|
+
```zig
|
|
44
|
+
pub fn init(comptime T: type, ctx: *T, fs: *bun.fs.FileSystem, allocator: std.mem.Allocator) !*Watcher {
|
|
45
|
+
const watcher = try allocator.create(Watcher);
|
|
46
|
+
errdefer allocator.destroy(watcher);
|
|
47
|
+
watcher.* = .{
|
|
48
|
+
.fs = fs,
|
|
49
|
+
.allocator = allocator,
|
|
50
|
+
// ... more fields, some allocated
|
|
51
|
+
.watch_events = try allocator.alloc(WatchEvent, max_count),
|
|
52
|
+
};
|
|
53
|
+
try Platform.init(&watcher.platform, fs.top_level_dir);
|
|
54
|
+
return watcher;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Allocator stored in the struct. errdefer placed immediately after the first allocation. Subsequent allocations protected by the same errdefer (the destroy frees everything when init unwinds).
|
|
59
|
+
|
|
60
|
+
**Allocator-as-deinit-param (value type)** — `~/Projects/oss/bun/src/StaticHashMap.zig`:
|
|
61
|
+
|
|
62
|
+
```zig
|
|
63
|
+
pub fn deinit(self: *Self, gpa: std.mem.Allocator) void {
|
|
64
|
+
gpa.free(self.slice());
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Allocator NOT stored. Caller passes it at deinit time. Suitable when the struct is short-lived and the allocator is unambiguous in the caller's scope.
|
|
69
|
+
|
|
70
|
+
**Owned ownership wrapper** — `~/Projects/oss/bun/src/ptr/owned.zig:14-95`:
|
|
71
|
+
|
|
72
|
+
```zig
|
|
73
|
+
pub fn Owned(comptime Pointer: type) type { /* uses DefaultAllocator (ZST) */ }
|
|
74
|
+
pub fn Dynamic(comptime Pointer: type) type { /* stores std.mem.Allocator at runtime */ }
|
|
75
|
+
pub fn OwnedIn(comptime Pointer: type, comptime Allocator: type) type { /* generic */ }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When ownership semantics matter and a comment isn't enough, `Owned<T>` makes the ownership compile-time-explicit.
|
|
79
|
+
|
|
80
|
+
**`bun.sys.Error.deinit` — only after clone** — `~/Projects/oss/bun/src/sys/Error.zig:106-120`:
|
|
81
|
+
|
|
82
|
+
```zig
|
|
83
|
+
pub fn deinit(this: *Error) void {
|
|
84
|
+
this.deinitWithAllocator(bun.default_allocator);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Only call this after it's been .clone()'d
|
|
88
|
+
pub fn deinitWithAllocator(this: *Error, allocator: std.mem.Allocator) void {
|
|
89
|
+
if (this.path.len > 0) allocator.free(this.path);
|
|
90
|
+
if (this.dest.len > 0) allocator.free(this.dest);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Idempotent (safe to call on a non-cloned Error — the `len > 0` check no-ops). Documentation calls out the pre-condition (`Only call this after it's been .clone()'d`).
|
|
95
|
+
|
|
96
|
+
## §4 · Our convention — `init` shape rules
|
|
97
|
+
|
|
98
|
+
**Decision matrix:**
|
|
99
|
+
|
|
100
|
+
| Struct lifetime / size | `init` signature | Reason |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| Heap-allocated, lives across function returns | `pub fn init(allocator: std.mem.Allocator) !*Self` | Caller stores the pointer; deinit knows where the struct lives. |
|
|
103
|
+
| Stack-allocated, fits on the caller's stack, no internal heap | `pub fn init() Self` (infallible) | No allocation, no failure mode. |
|
|
104
|
+
| Stack-allocated, internal heap (e.g. `ArrayList`) | `pub fn init(allocator: std.mem.Allocator) Self` (infallible) | Allocation deferred to first append; init cannot fail. |
|
|
105
|
+
| Stack-allocated, eager heap | `pub fn init(allocator: std.mem.Allocator) !Self` | Caller's stack holds the value; allocator stored in the struct or remembered by the caller. |
|
|
106
|
+
|
|
107
|
+
**Mandatory rules:**
|
|
108
|
+
|
|
109
|
+
1. **Allocator is the first parameter.** Always. Even when the struct is short-lived and won't store it.
|
|
110
|
+
2. **`errdefer` immediately after the first allocation.** Never batched. The Zig compiler reorders neither allocations nor cleanups; explicit-by-position is the convention.
|
|
111
|
+
3. **Every allocation that occurs after `errdefer` must be protected.** If init makes 3 allocations, you need either 1 errdefer that frees the wrapper struct (which then frees the children in deinit) OR 3 errdefers that free each individually as init unwinds.
|
|
112
|
+
4. **No partial state.** A struct that returns from init MUST be fully initialized — every field set, every invariant established. Half-initialized structs are a debugging hellmouth.
|
|
113
|
+
5. **Init cannot block on I/O for >100 ms unless the call is documented as blocking.** If init does network or filesystem work, the doc comment must say so. Synchronous init that secretly hits the network is a recurring incident class.
|
|
114
|
+
|
|
115
|
+
Example (heap-returned, allocator stored):
|
|
116
|
+
|
|
117
|
+
```zig
|
|
118
|
+
pub const Watcher = struct {
|
|
119
|
+
allocator: std.mem.Allocator,
|
|
120
|
+
fs: *FileSystem,
|
|
121
|
+
events: []Event,
|
|
122
|
+
|
|
123
|
+
pub fn init(allocator: std.mem.Allocator, fs: *FileSystem) !*Watcher {
|
|
124
|
+
const self = try allocator.create(Watcher);
|
|
125
|
+
errdefer allocator.destroy(self);
|
|
126
|
+
|
|
127
|
+
const events = try allocator.alloc(Event, 64);
|
|
128
|
+
errdefer allocator.free(events);
|
|
129
|
+
|
|
130
|
+
self.* = .{
|
|
131
|
+
.allocator = allocator,
|
|
132
|
+
.fs = fs,
|
|
133
|
+
.events = events,
|
|
134
|
+
};
|
|
135
|
+
return self;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## §5 · Our convention — `deinit` shape rules
|
|
141
|
+
|
|
142
|
+
**Two valid signatures:**
|
|
143
|
+
|
|
144
|
+
| Signature | When | Notes |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `pub fn deinit(self: *Self) void` | Allocator is stored in the struct (most common heap case). | Caller writes `var w = try Watcher.init(alloc, fs); defer w.deinit();` — clean. |
|
|
147
|
+
| `pub fn deinit(self: *Self, allocator: std.mem.Allocator) void` | Allocator NOT stored (short-lived value type, or struct is returned-by-value and the caller's scope owns the allocator). | Caller writes `defer thing.deinit(alloc);` — slightly noisier but explicit. |
|
|
148
|
+
|
|
149
|
+
**Mandatory rules:**
|
|
150
|
+
|
|
151
|
+
1. **`deinit` must free everything `init` allocated.** Lints catch the inverse (`init` allocates, `deinit` doesn't free) but not silently-correct cases (`init` allocates A and B, `deinit` frees only A — silent leak). Reviewer responsibility: walk the alloc list against the free list.
|
|
152
|
+
2. **`deinit` must be idempotent.** Calling it twice must not crash. Either set freed pointers to undefined (`self.events = &.{}` after free) or guard with `if (self.events.len > 0)`.
|
|
153
|
+
3. **`deinit` does not return errors.** Cleanup that *can* fail (closing a file, flushing a buffer) must be split into a separate explicit method (`pub fn close(self: *Self) !void`) called before `deinit`, or the failure must be logged and swallowed inside deinit.
|
|
154
|
+
4. **No partial deinit.** Once `deinit` runs, the struct is dead. Don't leave half-freed state for "the caller will free the rest" — that's a leak waiting to happen.
|
|
155
|
+
5. **Deinit order is reverse of init order.** Free children before freeing the parent that owns them. `errdefer` chains in init naturally produce the right order; mirror it in deinit.
|
|
156
|
+
|
|
157
|
+
Example matching the §4 init:
|
|
158
|
+
|
|
159
|
+
```zig
|
|
160
|
+
pub fn deinit(self: *Watcher) void {
|
|
161
|
+
self.allocator.free(self.events);
|
|
162
|
+
self.allocator.destroy(self);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## §6 · `defer` vs `errdefer` decision
|
|
167
|
+
|
|
168
|
+
**Rule:** `defer` for unconditional cleanup; `errdefer` for cleanup that should NOT run on success.
|
|
169
|
+
|
|
170
|
+
- `defer` runs on every exit from the scope (success or error).
|
|
171
|
+
- `errdefer` runs only when the scope exits via an `error` return.
|
|
172
|
+
|
|
173
|
+
**The most common bug** is `errdefer` left where `defer` belonged — produces a leak on the success path, because the cleanup never runs. Symptom: memleak gate flags it; trace shows a successful return path.
|
|
174
|
+
|
|
175
|
+
**The mirror bug** is `defer` left where `errdefer` belonged — produces a use-after-free, because the cleanup runs on success and frees memory the caller now owns.
|
|
176
|
+
|
|
177
|
+
**Decision tree:**
|
|
178
|
+
|
|
179
|
+
| Situation | Choice |
|
|
180
|
+
|---|---|
|
|
181
|
+
| Allocation that the function will own and dispose of regardless of outcome | `defer alloc.free(buf);` |
|
|
182
|
+
| Allocation that on success becomes the caller's responsibility (returned, stored in a struct field, transferred to a callback) | `errdefer alloc.free(buf);` — runs only if init unwinds before ownership transfers |
|
|
183
|
+
| Resource that must be released regardless (file descriptor, mutex, lock) | `defer file.close();` / `defer mutex.unlock();` |
|
|
184
|
+
| Resource only opened to facilitate init that becomes part of the returned struct | `errdefer file.close();` — and the struct's `deinit` closes it on success |
|
|
185
|
+
|
|
186
|
+
**Anti-pattern:** mixing `defer` and `errdefer` on the same allocation. If `defer` is correct, errdefer is dead code. If errdefer is correct, defer would double-free. Pick one.
|
|
187
|
+
|
|
188
|
+
## §7 · Ownership transfer at the boundary
|
|
189
|
+
|
|
190
|
+
When a function takes a slice/pointer and stores it in a struct, the call site must know who owns the memory after the call. Without convention this is grep-the-implementation; with convention it's call-site-readable.
|
|
191
|
+
|
|
192
|
+
**Convention:**
|
|
193
|
+
|
|
194
|
+
1. **`init`/`fromSlice`/`takeOwnership` semantics are explicit in the function name.** A function ending in `Borrowed` (`initBorrowed`) does not take ownership; the caller must outlive the struct.
|
|
195
|
+
2. **Doc-comment pattern.** When a function takes ownership, the doc comment leads with `/// Takes ownership of <param>. Caller MUST NOT free.`
|
|
196
|
+
3. **Call-site marker for ownership transfer.** When the call site transfers ownership, an inline comment marks it: `// owns: thing` immediately above the call. Forces a reader to confirm the transfer.
|
|
197
|
+
|
|
198
|
+
```zig
|
|
199
|
+
const events = try allocator.alloc(Event, 64);
|
|
200
|
+
// owns: events
|
|
201
|
+
const watcher = try Watcher.fromOwnedEvents(allocator, events);
|
|
202
|
+
// (no `defer allocator.free(events)` — watcher.deinit() does it)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Anti-pattern:** silent transfers. `fn foo(slice: []u8) void` whose body squirrels `slice` into a global without warning the caller — bug magnet. If unavoidable (third-party API), wrap behind an `Owns<T>` newtype.
|
|
206
|
+
|
|
207
|
+
## §8 · Arena vs GPA boundaries
|
|
208
|
+
|
|
209
|
+
Two allocator kinds, two patterns. **Don't mix in the same scope.**
|
|
210
|
+
|
|
211
|
+
**Arena** (`std.heap.ArenaAllocator`):
|
|
212
|
+
|
|
213
|
+
- One allocator per request/task/document. Lifetime tied to the surrounding work.
|
|
214
|
+
- Per-allocation `defer alloc.free(...)` is **forbidden** inside an arena scope — it's wasted work; the arena `reset()` reclaims everything at scope exit.
|
|
215
|
+
- Cleanup is one `arena.reset()` or `arena.deinit()` at scope end.
|
|
216
|
+
- Use when: many small short-lived allocations whose total lifetime matches the scope.
|
|
217
|
+
|
|
218
|
+
**GPA / specific allocator** (`std.heap.GeneralPurposeAllocator`, `std.testing.allocator`, custom):
|
|
219
|
+
|
|
220
|
+
- Long-lived. May be shared across threads.
|
|
221
|
+
- Every `alloc` paired with `defer alloc.free(...)` (or `errdefer` per §6).
|
|
222
|
+
- Use when: allocations have varied lifetimes, or memory is shared across components with different scopes.
|
|
223
|
+
|
|
224
|
+
**Boundary rule:** when crossing from a GPA scope into a function expected to use an arena (e.g. parsing a request body), pass the arena explicitly:
|
|
225
|
+
|
|
226
|
+
```zig
|
|
227
|
+
pub fn handleRequest(gpa: std.mem.Allocator, req: Request) !Response {
|
|
228
|
+
var arena = std.heap.ArenaAllocator.init(gpa);
|
|
229
|
+
defer arena.deinit();
|
|
230
|
+
|
|
231
|
+
return parseAndDispatch(arena.allocator(), req); // child sees arena
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Child functions receive the arena and don't know (or care) about the GPA above. They just allocate freely.
|
|
236
|
+
|
|
237
|
+
**Anti-pattern:** an arena leaking into a long-lived structure. Arena memory becomes invalid when the arena is destroyed; storing arena-allocated slices in a struct that outlives the arena is a use-after-free.
|
|
238
|
+
|
|
239
|
+
## §9 · RAII via struct method
|
|
240
|
+
|
|
241
|
+
The Zig idiom for owned resources at the call site:
|
|
242
|
+
|
|
243
|
+
```zig
|
|
244
|
+
var thing = try Thing.init(alloc);
|
|
245
|
+
defer thing.deinit();
|
|
246
|
+
// ... use thing ...
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Anything else is a smell:
|
|
250
|
+
|
|
251
|
+
- `defer alloc.free(thing.buf)` — hides ownership in the caller; if `Thing` later grows a second allocation, the caller forgets to free it.
|
|
252
|
+
- Manual cleanup at function tail — works but unwinds wrong on early return / error.
|
|
253
|
+
- Ad-hoc cleanup in `errdefer` blocks at the call site — encodes init/deinit logic in two places.
|
|
254
|
+
|
|
255
|
+
The struct's `deinit` method is the single source of truth for cleanup. Call sites use `defer self.deinit()` (or `errdefer` if ownership might transfer mid-function) and never reach inside the struct.
|
|
256
|
+
|
|
257
|
+
## §10 · Anti-patterns flagged by `deinit-pairs.sh`
|
|
258
|
+
|
|
259
|
+
| Pattern | Severity | Fix |
|
|
260
|
+
|---|---|---|
|
|
261
|
+
| `pub fn init(` with no matching `pub fn deinit(` in the same struct | **blocking** | Add `deinit`, or remove `init` if the struct is value-type without resources. |
|
|
262
|
+
| `pub fn deinit(` with no matching `pub fn init(` | **blocking** | Likely tail of a refactor — restore init, or rename deinit to a non-lifecycle name (e.g. `close`, `release`). |
|
|
263
|
+
| `errdefer` at end of init scope (batched) | informational | Move adjacent to the allocation it protects. |
|
|
264
|
+
| `defer` and `errdefer` on the same allocation | **blocking** | Pick one. |
|
|
265
|
+
| `init` returning `*Self` without `errdefer alloc.destroy(self)` | **blocking** | The most common leak class on init failure. |
|
|
266
|
+
| `deinit` that calls allocator-storing struct's `allocator` field after some other field's `.deinit()` (use-after-free risk if children depend on parent's allocator) | informational | Reorder cleanup to free children first. |
|
|
267
|
+
| Stored allocator field never used in deinit | informational | Either remove the field or wire it. |
|
|
268
|
+
|
|
269
|
+
## §10A · Tightening clauses (closures of common skip rationalizations)
|
|
270
|
+
|
|
271
|
+
Failure modes the audit script and reviewer must close. These are **not aspirational** — each closes a specific way an agent could otherwise dodge the rule.
|
|
272
|
+
|
|
273
|
+
| # | Rationalization | Closure |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| LC1 | "Struct is small / one allocation, skip `deinit`" | Any heap allocation OR opaque handle (file descriptor, mutex, socket, GPU resource, lock) requires `deinit`. **No size threshold**. Audit script flags struct definitions with `allocator.alloc`/`alloc.create`/`std.fs.File.open` whose enclosing struct lacks `pub fn deinit`. |
|
|
276
|
+
| LC2 | "Renamed `deinit` to `close` / `release` to dodge pair audit" | Audit script treats `deinit`, `close`, `release`, `destroy`, `shutdown`, `free`, `dispose` as **lifecycle methods**. All require a paired `init` and trigger LIFECYCLE GATE. Rename does not bypass; intent does. |
|
|
277
|
+
| LC3 | "I'll batch `errdefer` at the bottom of init, easier to read" | The LAST `errdefer` line in init must lexically precede the LAST allocation it protects. Audit script enforces line-position. Batched-at-bottom errdefer is **blocking**. |
|
|
278
|
+
| LC4 | "Both `defer` and `errdefer` on the same allocation, belt-and-suspenders" | Audit greps for `defer X.free(Y)` + `errdefer X.free(Y)` (or the reverse) in the same scope. **Blocking violation** — pick one per §6. |
|
|
279
|
+
| LC5 | "`deinit` is idempotent in practice; no test needed" | Every struct with `deinit` must have a `*_test.zig` test that calls `s.deinit(); s.deinit();` and asserts no crash (sentinel-on-second-call is fine). Reviewer responsibility. Audit cannot fully grep for this; reviewer must verify. |
|
|
280
|
+
| LC6 | "Storing arena slice in long-lived struct works in my test" | Heuristic flag: struct fields of slice type AND a stored `*ArenaAllocator` field → audit raises informational warning. Reviewer must acknowledge or restructure. |
|
|
281
|
+
| LC7 | "Empty no-op `init` paired with empty no-op `deinit` to satisfy the gate" | Audit informational-flags when both `init` and `deinit` bodies are empty or single `_ = self;`. Pair exists for shape, not for state — likely should be a `const` value or removed. |
|
|
282
|
+
| LC8 | "Auto-mode is on" | **Auto-mode does NOT cover gate skips.** Skip without explicit user-given override = automatic violation. |
|
|
283
|
+
| LC9 | "Renamed-only file (no content change), gate doesn't fire" | Rename without content change does **not** fire LIFECYCLE GATE. Rename + content change does. Pure rename is a no-op for the gate. |
|
|
284
|
+
| LC10 | "Two docs disagree (e.g. write_zig.md says X, LIFECYCLE_PATTERNS says Y)" | Precedence: dispatch façade > standards doc > spec. The façade (`dispatch/write_zig.md`, lifecycle) is the canonical enforcement layer. |
|
|
285
|
+
|
|
286
|
+
These are enforced by `deinit-pairs.sh` (mechanical) and the dispatch façade (`dispatch/write_zig.md`, lifecycle — output discipline). When in conflict, the façade wins.
|
|
287
|
+
|
|
288
|
+
## §11 · Carve-out: PUB GATE vs LIFECYCLE GATE
|
|
289
|
+
|
|
290
|
+
Both gates touch the same `pub fn init` / `pub fn deinit` symbols, but ask different questions:
|
|
291
|
+
|
|
292
|
+
| Gate | Question |
|
|
293
|
+
|---|---|
|
|
294
|
+
| **PUB GATE** (`dispatch/write_zig.md`, pub-surface) | Should this symbol be `pub`? Does the file shape (file-as-struct vs functions-module) justify it? |
|
|
295
|
+
| **LIFECYCLE GATE** (`dispatch/write_zig.md`, lifecycle) | If `init` exists, does `deinit`? Is `errdefer` placed correctly? Is allocator ownership clear? Is the call-site contract documented? |
|
|
296
|
+
|
|
297
|
+
When a `pub fn init` is added, **both gates may fire**. Print both blocks. Neither gate skips deferring to the other; the redundancy on this critical surface is a feature.
|
|
298
|
+
|
|
299
|
+
## §12 · Override syntax
|
|
300
|
+
|
|
301
|
+
Per-rule override (rare, user-only):
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
LIFECYCLE RULE §<N>: SKIPPED per user override (reason: ...)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
immediately preceding the edit. Generic "save for later" / "tactical decision" not valid — name a concrete external constraint (third-party library shape that prescribes lifecycle, vendored code we can't change). Auto-mode does NOT cover this override.
|
|
308
|
+
|
|
309
|
+
## §13 · Family
|
|
310
|
+
|
|
311
|
+
- `dispatch/write_zig.md` — Zig discipline umbrella (absorbs ZIG GATE + PUB + LIFECYCLE). This doc is the lifecycle-specific layer below it.
|
|
312
|
+
- `LOGGING_STANDARD.md` §7 — orthogonal: how lifecycle hooks (init/deinit) emit observability records.
|
|
313
|
+
- `dispatch/write_zig.md` (pub-surface) — PUB GATE; carve-out in §11.
|
|
314
|
+
- `dispatch/write_any.md` (File & Function Length) — file ≤ 350, fn ≤ 50, method ≤ 70 (deinit/init don't escape these limits).
|
|
315
|
+
- Universal rules (RULE UFS, RULE TGU, RULE ORP) live in `docs/greptile-learnings/RULES.md` and apply via the GREPTILE GATE.
|
|
316
|
+
- This file is the **lifecycle convention** that those universal rules cannot express. Read it once at session start; on sub-task shape change, section-scan (`grep -n "^## "`) and re-read only the touched sections — never the full file again.
|