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
//! Refs #390: the failure text an operator actually reads.
//!
//! # The incident
//!
//! An operator building llama.cpp with CUDA on `gx10` ran `forjar apply` six
//! times against the same task, editing `command:` between runs to add `echo`,
//! `nvcc --version` and `grep GGML_CUDA` diagnostics. Every run printed, byte
//! for byte:
//!
//! ```text
//! JIDOKA: gx10/llama-cpp-build failed — dependents will be skipped:
//! exit code 1: CMake Warning:
//! Manually-specified variables were not used by the project:
//! CMAKE_BUILD_TYPE=Release
//! CMake Deprecation Warning: compat
//! task=not-converged: command exited 0 but completion_check still fails
//! task=not-converged: the declared state was not reached
//! ```
//!
//! Not one of the added diagnostics ever appeared. The two cmake lines appeared
//! every time, unchanged. From that they concluded — reasonably, and wrongly —
//! that forjar was replaying a cached transcript instead of running the edited
//! script, and filed it as a caching defect.
//!
//! Seven independent reproduction lanes, one of them over a real SSH transport,
//! later proved with append-only counter files that the command re-ran on every
//! single apply. Nothing was cached. The whole symptom was stream routing:
//!
//! * `echo`, `nvcc --version` and `grep` write to STDOUT.
//! * cmake's `CMake Warning` lines, and llama.cpp's own bare
//! `message("CMAKE_BUILD_TYPE=...")` (CMake NOTICE mode), write to STDERR.
//! * The operator's only failure line was built as
//! `format!("exit code {}: {}", out.exit_code, out.stderr.trim())` — in
//! `resource_ops.rs` and, duplicated, in `machine_wave.rs`. `out.stdout`
//! was structurally absent from it.
//!
//! "Identical output across six runs" was forced by construction: the message
//! is a pure function of the exit code and stderr, and the operator's edits
//! changed neither.
//!
//! The headline also named the wrong failure. The command had exited 0 every
//! time; what exited 1 was the `completion_check` that GH-254 re-asserts at the
//! end of the generated script (`resources::task::batch_script`). Six builds
//! were spent hunting a compiler error that never existed, under a line reading
//! `exit code 1`.
//!
//! # What this module is
//!
//! The one place an `ExecOutput` becomes text a human reads. Before it there
//! were five constructors on the apply path and they disagreed about which
//! stream mattered. `resource_ops.rs` and `machine_wave.rs` reported stderr and
//! destroyed stdout. `output_verify::verify_against_host` reported stdout and
//! destroyed stderr — the exact mirror image, on the branch every `type: task`
//! without a `completion_check` lands in, because
//! `resources::task::check_script` falls through to
//! `verdict::always_diverged("task=pending")`. Whichever half of a failure
//! mattered, some path was built to throw it away.
//!
//! # What the text is allowed to cost
//!
//! It is not console-only. `record_failure` writes it verbatim into
//! `ProvenanceEvent::ResourceFailed`, i.e. into `state/<machine>/events.jsonl`,
//! which is append-only and which `forjar history --resource <id>` replays. A
//! cmake build emits megabytes on stdout, so each stream is excerpted
//! head-AND-tail with the middle elided and the true byte count stated — never
//! more than `HEAD_BYTES + TAIL_BYTES` per stream.
//!
//! Head as well as tail, and that is the load-bearing half. #390's missing
//! diagnostics ran BEFORE the build, so the tail-only excerpt a log viewer
//! would keep is exactly the one that would still have elided the lines the
//! operator spent six runs looking for.
//!
//! Note which direction this moves: today's `out.stderr.trim()` has no bound at
//! all, so a 3 MB stderr goes verbatim into an append-only log on every failed
//! apply. This is the first ceiling that string has ever had.
//!
//! # The pointer is conditional on purpose
//!
//! The full, unelided transcript is at
//! `state/<machine>/runs/<run_id>/<resource>.<action>.log`, and nothing at the
//! failure site had ever named it. It does now — but the path is handed in by
//! the code that WROTE it (`run_capture::capture_exec_output` returns it), so
//! the message can only name a file that exists. Under `--parallel` no run log
//! is written at all today; there the pointer is absent and the excerpt is the
//! only surviving copy. That gap is tracked as #390-A; this module will not
//! paper over it with a path that lies.
use *;
use cratetruncate_at_boundary;
use crateNOT_CONVERGED_MARKER;
use Path;
/// Bytes kept from the FRONT of an over-long stream.
const HEAD_BYTES: usize = 800;
/// Bytes kept from the END of an over-long stream.
const TAIL_BYTES: usize = 1200;
/// Head-only ceiling for an error that is prose rather than a stream.
const PROSE_BYTES: usize = 4096;
/// Where an execution happened, and where its transcript landed.
pub
/// The report for a command that RAN and exited non-zero.
///
/// Refs #390: replaces `format!("exit code {}: {}", out.exit_code,
/// out.stderr.trim())` at `resource_ops.rs` and its duplicate in
/// `machine_wave.rs`.
pub
/// FAILED vs NOT CONVERGED — different diagnoses, different next actions.
///
/// Both used to print as `exit code N:`. #390 is entirely the second kind and
/// its reporter read it as the first for six runs.
/// Refs #390-E: `timeout:` and `sudo: true` run the command in a NESTED `bash`
/// that does not inherit the outer `set -euo pipefail` — see
/// `resources::task::batch_script`. There an early failing line neither aborts
/// the script nor changes its exit status, so "the command itself exited 0" is
/// a claim this module cannot honestly make.
///
/// Say so rather than assert a diagnosis known to be wrong under a documented,
/// still-open defect. Trading one confidently wrong label for a more
/// authoritative one is the failure mode this whole module exists to end.
/// The report for a command that exited 0 whose POST-APPLY verification then
/// said no: a `post_apply` hook, missing `output_artifacts` (FJ-2731), or the
/// host itself (FJ-2732).
///
/// Its own entry point because the diagnosis differs from a command failure —
/// nothing the operator wrote returned an error, forjar asked a second question
/// afterwards and the host answered no. Rendering that as `exit code 1:` is how
/// one sentence came to mean three things.
pub
/// The report for an execution that never produced an exit code: a timeout, a
/// spawn failure, or an I8 (bashrs) rejection.
///
/// The `transport error: ` prefix is byte-identical to the string it replaces
/// and the error's own text is clipped from the FRONT, both deliberately.
/// `core::error::DECLARED_MARKERS` classifies an I8 rejection by a marker that
/// sits at the head of `e`, and forjar#281's numbered-script diagnostic is the
/// next thing after it. A tail cut would silently re-code a deterministic
/// validation failure as a retryable connection failure.
///
/// No streams and no log pointer: on this path forjar has neither. Saying so is
/// the point — #390's reporter went hunting for a file that was never written.
pub
/// The report for a `pre_apply` / `post_apply` hook that exited non-zero.
///
/// Refs #390: three byte-identical copies of this existed —
/// `output_verify::run_pre_apply_hook`, `output_verify::check_post_hook` and
/// `machine_wave::exec_validated_hook` — every one of them stderr-only. A hook
/// that explains itself with `echo "nginx config invalid: line 42"` and no
/// `>&2`, which is what people actually write, lost its diagnostic in exactly
/// the way the reporter's task did.
///
/// No log pointer: hooks run through `exec_script_timeout` and are not
/// captured, so naming a run log here would name one that does not exist.
pub
/// The report for a hook that could not be executed at all.
pub
/// FJ-2732's verdict, with BOTH of the check script's streams.
///
/// Refs #390: `verify_against_host` reported `out.stdout.trim()` and destroyed
/// `out.stderr` — the mirror image of the defect this module exists for, on the
/// branch every `type: task` without a `completion_check` reaches. A check that
/// explains itself on stderr (`test: /opt/x: No such file or directory`)
/// reported only `task=pending`.
pub
/// Both streams, always both, labelled and excerpted — and never silent.
///
/// An empty stream still gets a line. "(empty)" is itself a diagnosis: its
/// absence is what left #390's reporter unable to tell "my echoes produced
/// nothing" from "forjar is hiding my echoes" across six builds.
///
/// `pub(super)` because `helpers::copia_apply_file` reports a signature-phase
/// failure that is neither a resource command nor a hook, and it was the fifth
/// stderr-only constructor in this module tree.
pub
/// One labelled stream, stating the TRUE size it was excerpted from.
/// Head AND tail of one stream, with the middle elided and the drop stated.
///
/// Byte indices are walked to a char boundary in both directions: build output
/// is arbitrary UTF-8 and slicing it at a fixed offset panics mid-codepoint. A
/// diagnostic that panics is worse than the bug it was printing, and this code
/// runs only when something has already gone wrong.
/// Keep the FIRST `max` bytes on a char boundary, and state what was dropped.
/// Name the transcript and the command that renders it — but only when one was
/// written, and always as an ABSOLUTE path.
///
/// `--state-dir` defaults to the RELATIVE path `state`, so a relative path
/// printed here resolves only from the directory the apply ran in, and a
/// stateless CI runner deletes it with the checkout. That is how #390's
/// reporter lost evidence which had been on disk the whole time, so when the
/// state dir is relative the note says so.