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
//! `ops.jsonl`: the durable action-outcome log (DESIGN §4.2, §15 Y15).
//!
//! yog is a pure renderer of disk except for two owned files; this is one of
//! them. Every **attempted** yog-initiated CLI action appends one JSON line to
//! `<yog_state_root>/ops.jsonl` — `{ts, argv, cwd, exit, stdout, stderr}` — so
//! both instances tail one shared history instead of holding gate/close output
//! RAM-only (§4.2 as amended: "closes the one durability leak"). A *completed*
//! run logs its real outcome; a spawn or non-spawn **step failure** logs a
//! synthetic line ([`OpEntry::synthetic_failure`] / [`OpEntry::step_failure`])
//! so no error class is un-logged — the §7.3 failed-action row depends on it.
//!
//! **Atomicity by size cap.** The line *including its newline* is hard-capped
//! at [`CAP`] = 4096 bytes (PIPE_BUF): a write that size or under is a single
//! atomic `O_APPEND` on Linux, so two instances never interleave.
//! [`build_line`] is the pure capper — it truncates `stdout`, then `stderr`,
//! keeping heads and stamping `"truncated":true`; the fixed fields
//! (`ts`/`argv`/`cwd`/`exit`) never truncate, so a pathological argv is the one
//! case a line may exceed the cap (structurally unavoidable; §4.2 is silent
//! there — we stay strict-otherwise).
//!
//! **No clock here.** `ts` is a data field, stamped upstream from the caller's
//! clock; nothing in this module reads wall-clock time, keeping [`build_line`]
//! and the tail parser pure and deterministic.
//!
//! **One field is not stored here.** A detached spawn's `stderr` is captured to
//! a per-spawn sink file and folded into the row at read time
//! ([`detached`]) — the log records the launch, the sink records what the
//! launched process said.
use fs;
use ;
use Path;
use ;
/// The hard cap on a serialized line *including* its trailing newline, in
/// bytes. 4096 = PIPE_BUF: at or under it an `O_APPEND` write is atomic.
pub const CAP: usize = 4096;
/// The log's leaf name under the yog state root (§4.2).
const FILENAME: &str = "ops.jsonl";
/// `exit` sentinel for a piped verb whose status was unobservable
/// (`ExitInfo::Unknown`, [`crate::cli_outbound::ExitInfo::shell_code`]): the
/// process **ran** — not a rendered failure ([`rows::OpRow::failed`]).
pub const PIPED_UNOBSERVED: i32 = -1;
/// `exit` sentinel for the detached `lernie prompt` (§8.1, §13.3): a detached
/// child in its own process group has no waitable status, so `-2` records
/// "launched detached; exit deliberately unobserved". A *clean, silent* launch
/// is a success; a spawn *failure* rides the same line with the error in
/// `stderr`, and a child that spoke on stderr *after* launching has that text
/// folded in from its sink at read time ([`detached`]) — both make the one
/// detached case [`rows::OpRow::failed`] flags.
pub const DETACHED_EXIT: i32 = -2;
/// `exit` sentinel for a **synthetic failure line** (§4.2 as amended): an
/// attempted action that produced no process status — a piped spawn that never
/// launched, or a non-spawn yog-step failure ([`OpEntry::synthetic_failure`] /
/// [`OpEntry::step_failure`]). The failure text always rides in `stderr`.
pub const SYNTHETIC_EXIT: i32 = -3;
/// `argv[0]` of a non-spawn step-failure line (§4.2): the logical step name
/// rides as `argv[1]`, e.g. `["yog-step","mint"]` — an error class with no real
/// binary still gets a rendered ops row (the §7.3 failed-action row depends on
/// every error class having one).
pub const YOG_STEP: &str = "yog-step";
/// One attempted CLI action — the on-disk `ops.jsonl` record (§4.2 as amended):
/// a completed run's captured outcome, or a synthetic failure line for a spawn
/// or non-spawn step that never produced a process status.
///
/// `ts` is an already-formatted timestamp string (e.g. RFC3339) supplied by
/// the caller's clock; this module never reads time.
/// The pure ≤[`CAP`] line serializer and the caller-side argv clip (§4.2). Split
/// out of this file per §12's line-budget discipline.
pub use ;
/// Append `entry`'s capped line to `<state_root>/ops.jsonl` via `O_APPEND`,
/// creating the state dir if absent. Atomic against a concurrent instance by
/// the [`CAP`] size bound.
/// The last `max` parseable entries, oldest-first (newest-last). A missing file
/// or unreadable bytes yield an empty view; each line parses forgivingly — a
/// corrupt or mid-write-torn line is skipped, never an error.
/// Parse one line into an [`OpEntry`], or `None` when it is not a JSON object.
/// Individual fields default when absent or mistyped (forgiving, per §4.2).
/// A string field of `obj`, or `""` when absent or non-string.
/// View-models over the log (§4.2, §7.3, §11), split out per §12's line budget:
/// `rows` = the expandable [`OpRow`] and the [`SurfaceFailure`] a surface holds;
/// `live` = §6's retirement projection over a tail of rows (which failures are
/// still live — the log keeps every failure, prominence is derived) + [`Activity`].
pub use ;
pub use ;
/// The detached driver's captured stderr (§8.1, §13.3): the per-spawn sink file
/// and the read-time fold that projects its tail into the row.