atomwrite 0.1.35

Atomic file operations CLI for LLM agents — read, write, edit, search, replace with NDJSON output
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447



/// GAP-2026-011 L1/L6: Local diagnostics describing why a write was classified
/// as risky (G-031: not product telemetry). Emitted in `WriteOutput.risk_assessment`
/// when the size delta exceeds the user-configured `--risk-threshold`.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct WriteRiskAssessment {
    /// File size before the write.
    pub original_bytes: u64,
    /// New file size after the write.
    pub new_bytes: u64,
    /// |new - original| / original * 100.
    pub size_delta_pct: u32,
    /// "low" (<70%) | "medium" (70-89%) | "high" (>=90%).
    pub risk_level: &'static str,
    /// Which guard triggered the assessment (always "size" for L1).
    pub guard_triggered: &'static str,
}

/// NDJSON output for read operations with metadata and optional content.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct ReadOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Absolute path of the file.
    pub path: String,
    /// File content, omitted in stat-only mode or for binary files.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    /// Number of lines in the returned content.
    ///
    /// As of v0.1.20 (GAP-2026-008), this is the FILTERED count when a partial
    /// read filter (`--head`, `--tail`, `--line`, `--lines`, `--grep`) is
    /// in effect. The original file total is preserved in `lines_total`.
    pub lines: u64,
    /// Total lines in the original file before any filter (GAP-2026-008).
    /// Omitted when no filter is applied or the filter did not change the
    /// line count.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lines_total: Option<u64>,
    /// File size in bytes.
    pub bytes: u64,
    /// BLAKE3 checksum of the file contents.
    pub checksum: String,
    /// Filesystem permissions string.
    pub permissions: String,
    /// Last modification timestamp.
    pub modified: String,
    /// File kind (file, directory, symlink).
    pub kind: String,
    /// Whether the file was detected as binary.
    pub binary: bool,
    /// Line range returned when a subset was requested.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub range: Option<LineRange>,
    /// Checksum verification result, if --verify-checksum was used.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verified: Option<bool>,
    /// Read mode discriminator (GAP-2026-009): "full" | "head" | "tail" |
    /// "line" | "lines" | "grep" | "stat" | "raw". Allows downstream consumers to
    /// distinguish partial reads from full reads without parsing content.
    pub mode: String,
    /// Base64 payload for binary `--format raw` under NDJSON agent contract (G-013).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content_b64: Option<String>,
}

/// NDJSON output for a per-file replace operation.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct ReplaceResult {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Path of the modified file.
    pub path: String,
    /// Number of replacements performed.
    pub replacements: u64,
    /// File size in bytes before replacement.
    pub bytes_before: u64,
    /// File size in bytes after replacement.
    pub bytes_after: u64,
    /// BLAKE3 checksum before replacement.
    pub checksum_before: String,
    /// BLAKE3 checksum after replacement.
    pub checksum_after: String,
    /// Operation duration in milliseconds.
    pub elapsed_ms: u64,
    /// Whether the original modification time was preserved (true) or updated to now (false).
    /// Critical for build systems: false ensures cargo/make/cmake detect the change.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mtime_preserved: Option<bool>,
    /// Whether any replacement used a non-exact fuzzy strategy (v0.1.29).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fuzzy: Option<bool>,
    /// Last fuzzy strategy that applied (v0.1.29).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strategy: Option<String>,
    /// Similarity of the last fuzzy match when applicable (v0.1.29).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub similarity: Option<f64>,
    /// Strategies tried for the last fuzzy application (v0.1.29).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strategies_tried: Option<u64>,
    /// True when `--word` was ignored because fuzzy path was used (v0.1.29).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub word_ignored: Option<bool>,
}

/// NDJSON output for a surgical edit operation.
#[derive(Debug, Serialize, JsonSchema)]
pub struct EditOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Path of the edited file.
    pub path: String,
    /// Number of edit operations applied.
    pub edits: u64,
    /// Edit mode used (e.g. `after_line`, `range`, `old_new`, `exact`).
    pub mode: String,
    /// File size in bytes before editing.
    pub bytes_before: u64,
    /// File size in bytes after editing.
    pub bytes_after: u64,
    /// BLAKE3 checksum before editing.
    pub checksum_before: String,
    /// BLAKE3 checksum after editing.
    pub checksum_after: String,
    /// Line count before editing.
    pub lines_before: u64,
    /// Line count after editing.
    pub lines_after: u64,
    /// Operation duration in milliseconds.
    pub elapsed_ms: u64,
    /// Whether fuzzy matching was used (only present in --old/--new mode).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fuzzy: Option<bool>,
    /// Fuzzy strategy that succeeded (only present when fuzzy=true).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strategy: Option<String>,
    /// Number of strategies tried before success (only present in --old/--new mode).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub strategies_tried: Option<u64>,
    /// Similarity score of the fuzzy match, 0.0-1.0 (only present for `block_anchor` strategy).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub similarity: Option<f64>,
    /// Unified diff preview showing what the fuzzy match changed (truncated to 500 chars).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub diff_preview: Option<String>,
    /// Total number of `--old`/`--new` pairs requested (multi-pair mode only).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pairs_total: Option<u64>,
    /// Per-pair match results (multi-pair mode only) — the per-item ground truth.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pair_results: Option<Vec<PairResult>>,
    /// Whether the original modification time was preserved (true) or updated to now (false).
    /// Critical for build systems: false ensures cargo/make/cmake detect the change.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mtime_preserved: Option<bool>,
    /// Number of occurrences replaced in old/new mode (1 unless `--replace-all`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub match_count: Option<u64>,
    /// True when fuzzy indent delta realigned the replacement text.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub indent_adjusted: Option<bool>,
}

/// NDJSON output for a rollback (restore from backup) operation.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct RollbackResult {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Path of the restored file.
    pub path: String,
    /// Path of the backup that was restored.
    pub restored_from: String,
    /// BLAKE3 checksum before restoration.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checksum_before: Option<String>,
    /// BLAKE3 checksum after restoration.
    pub checksum_after: String,
    /// Whether checksum was verified post-restore.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verified: Option<bool>,
    /// Operation duration in milliseconds.
    pub elapsed_ms: u64,
}

/// Aggregate summary emitted at the end of a batch run.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct BatchSummary {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Total operations in the manifest.
    pub operations: u64,
    /// Number of operations that succeeded.
    pub succeeded: u64,
    /// Number of operations that failed.
    pub failed: u64,
    /// Whether this was a dry-run execution.
    pub dry_run: bool,
    /// Total batch duration in milliseconds.
    pub elapsed_ms: u64,
    /// Whether transaction mode was active.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub transaction: Option<bool>,
    /// Whether the transaction was committed (all operations succeeded).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub committed: Option<bool>,
}

/// NDJSON summary for a diff operation.
#[derive(Debug, Serialize, JsonSchema)]
pub struct DiffSummaryOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Whether files are identical.
    pub identical: bool,
    /// First file path.
    pub file_a: String,
    /// Second file path.
    pub file_b: String,
    /// Line count of first file.
    pub lines_a: usize,
    /// Line count of second file.
    pub lines_b: usize,
    /// Similarity ratio.
    pub similarity_ratio: f32,
    /// Duration in milliseconds.
    pub elapsed_ms: u64,
}

/// NDJSON output for a completed move operation.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct MoveOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Source path.
    pub source: String,
    /// Target path.
    pub target: String,
    /// File size in bytes.
    pub bytes: u64,
    /// BLAKE3 checksum.
    pub checksum: String,
    /// Whether a cross-device copy was needed.
    pub cross_device: bool,
    /// Whether the operation was atomic.
    pub atomic: bool,
    /// Path to backup file, if created.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backup_path: Option<String>,
    /// Duration in milliseconds.
    pub elapsed_ms: u64,
}

/// NDJSON output for total line/file counts.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct CountTotalOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Count mode name.
    pub mode: &'static str,
    /// Aggregate totals.
    pub total: CountTotals,
    /// Duration in milliseconds.
    pub elapsed_ms: u64,
}

/// NDJSON output for count sorted by file size (top-N).
/// Added in v0.1.20 to close GAP-2026-001 (HELP-FIRST-DRIFT).
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct CountBySizeOutput {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Count mode name.
    pub mode: &'static str,
    /// Top-N files by descending size.
    pub items: Vec<SizeEntry>,
    /// Duration in milliseconds.
    pub elapsed_ms: u64,
}

/// NDJSON dry-run plan for backup operations.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct BackupPlan {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Operation name.
    pub operation: &'static str,
    /// File path.
    pub path: String,
    /// File size in bytes.
    pub bytes: u64,
    /// BLAKE3 checksum.
    pub checksum: String,
}

/// NDJSON dry-run plan for patch apply operations.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct ApplyPlan {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Operation name.
    pub operation: &'static str,
    /// Target file path.
    pub path: String,
    /// Detected patch format.
    pub format_detected: String,
    /// Number of hunks detected.
    pub hunks: usize,
}

/// NDJSON output for text value extraction.
#[derive(Debug, Serialize, JsonSchema)]
pub struct TextValuesOutput<'a> {
    /// Event type discriminator.
    pub r#type: &'static str,
    /// Extracted values.
    pub values: Vec<&'a str>,
}

// ============================================================================
// G119 L5 — Re-export of WAL stats for `--json-schema wal-stats` consumers
// ============================================================================

pub use crate::wal::{AutoHealReport, WalDirEntry, WalStateBreakdown, WalStats};

// ============================================================================
// v0.1.22 — edit-loop NDJSON output types (ADR-0039)
// ============================================================================

/// Per-pair result emitted in the `pair_results` array of `edit-loop`.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct EditLoopPairResult {
    /// 1-based index of the pair in NDJSON order.
    pub index: usize,
    /// Whether the pair matched and was applied.
    pub matched: bool,
    /// The search text for this pair.
    pub old: String,
    /// The replacement text for this pair.
    pub new: String,
}

/// NDJSON event for `watch` (feature-gated command).
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct WatchEvent {
    /// Event type discriminator: `"watch"`.
    pub r#type: &'static str,
    /// Path that changed.
    pub path: String,
    /// Notify event kind (debug format of the backend kind).
    pub kind: String,
    /// Optional BLAKE3 hex when `--checksum` is set and path is a regular file.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checksum: Option<String>,
}

/// Terminal NDJSON summary for `watch` (A-WATCH-001 — always emitted on exit).
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct WatchSummary {
    /// Event type discriminator: `"watch_summary"`.
    pub r#type: &'static str,
    /// Number of filesystem change events emitted.
    pub events: u64,
    /// Exit reason: `idle`, `max_events`, `signal`, or `complete`.
    pub reason: String,
    /// Idle-exit bound in milliseconds (0 = disabled).
    pub idle_exit_ms: u64,
    /// Debounce window in milliseconds.
    pub debounce_ms: u64,
    /// Configured max events (0 = unlimited until other bound).
    pub max_events: u64,
    /// Wall time of the watch session in milliseconds.
    pub elapsed_ms: u64,
}

/// NDJSON final summary for `semantic-search`.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct SemanticSummaryEvent {
    /// Event type discriminator: `"semantic_summary"`.
    pub r#type: &'static str,
    /// Original free-text query.
    pub query: String,
    /// Requested top-k.
    pub k: u64,
    /// Number of results emitted.
    pub results: usize,
    /// Ranking backend label.
    pub backend: &'static str,
}

/// Built-in recipe listing row (`recipe list`).
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct RecipeListEvent {
    /// Event type discriminator: `"recipe"`.
    pub r#type: &'static str,
    /// Recipe name.
    pub name: &'static str,
    /// Always true for built-ins.
    pub builtin: bool,
}

/// `codemod` campaign start envelope.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct CodemodStartEvent {
    /// Event type discriminator: `"codemod"`.
    pub r#type: &'static str,
    /// Phase label (`"start"`).
    pub phase: &'static str,
    /// Rules file path.
    pub rules: String,
    /// Parsed rule ids from the manifest.
    pub rule_ids: Vec<String>,
    /// Campaign id (rules file stem).
    pub rule_id: String,
    /// Whether mutations are suppressed.
    pub dry_run: bool,
}

/// Final `codemod` summary.
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct CodemodSummaryEvent {
    /// Event type discriminator: `"codemod_summary"`.
    pub r#type: &'static str,
    /// Rules file path.
    pub rules: String,
    /// Campaign id.
    pub rule_id: String,
    /// Dry-run flag.
    pub dry_run: bool,
    /// Stats keyed by rule id.
    pub by_rule_id: std::collections::BTreeMap<String, CodemodRuleStats>,
}

/// Multi-rule transform: rule failure (partial success continues).
#[derive(Debug, PartialEq, Serialize, JsonSchema)]
pub struct TransformRuleError {
    /// Event type discriminator: `"rule_error"`.
    pub r#type: &'static str,
    /// Optional rule id from YAML.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// Error message.
    pub error: String,
}