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
//! GAP-SG-215: the NDJSON stream contract, decided.
//!
//! [`super`] is defined over one complete envelope. `export` and `ingest` emit
//! something else — N self-contained records followed by a summary — and until
//! v1.2.8 they reached that envelope machinery once per LINE, through
//! `crate::output::emit_json_compact`. Three defects followed, all measured:
//!
//! * `--select name export --limit 3` emitted three correctly projected records
//! and then `exit 2` on the fourth line. The summary carries `namespace`, not
//! `name`, so the projection that resolved for every record failed on the one
//! line that is not a record — after stdout had already been written to.
//! * `--select namespace export` did the mirror of that in SILENCE, `exit 0`:
//! the key resolved, so the summary was projected down to `{"namespace":…}`
//! and lost `summary: true`, the only end-of-stream signal a consumer has. A
//! truncated export then looks exactly like a complete one.
//! * With NO knob at all, every line carried a 278-byte `agent_surface` record —
//! measured over 200 lines — restating one fact about the PROCESS once per
//! memory, absolute database path included. At the default `--limit 100000`
//! that is ~27.8 MB, written into the file `docs/AGENTS.md` recommends
//! creating with `export > backup.ndjson`.
//!
//! # The contract
//!
//! * **A record line carries the record and nothing else.** No `agent_surface`,
//! no `truncated`. This is the invariant `super`'s module docs have declared
//! since GAP-SG-142 — "NDJSON streams bypass the surface" — restored to being
//! true. The NDJSON specification is explicit that the format carries no
//! per-line header, metadata or schema; a stream is data, and the frame around
//! it belongs somewhere else.
//! * **Only per-record knobs act.** `--select` and `--truncate-content` are
//! stateless per record and mean the same thing whether a record arrives alone
//! or in a stream. `crate::output::stream` named exactly that pair as the safe
//! extension and asked for a contract decision before wiring it; this module is
//! that decision. Everything else is refused by [`super::gate::evaluate_stream`]
//! BEFORE the first byte, so a refusal never leaves a half-written stream.
//! * **The trailer is never shaped and carries the one record.** The summary
//! line is already about the stream rather than about a memory, so the resolved
//! target, the query ceiling and the projection findings ride there — once.
//!
//! The published schemas allow all three: `docs/schemas/export-memory-line` and
//! `export-summary` both declare `agent_surface` OPTIONAL, so dropping it from
//! the record and keeping it on the summary breaks no contract. What the old
//! behaviour did break was `export-summary`'s `required` list, every time a
//! projection deleted `summary`, `exported` or `elapsed_ms`.
//!
//! # Why the state is a cell and the decisions are not
//!
//! One process runs one subcommand and emits one stream, so a process-wide cell
//! is the single fact about that stream rather than ambient state — the same
//! reasoning [`super::universe`] documents. But GAP-SG-201 shipped a refusal no
//! test could reach precisely because the DECISION read the cell from inside
//! itself. So every function here that decides anything takes its premises as
//! arguments, and the cell is read at exactly one place: the emitters in
//! `crate::output::stream`.
use ;
use Scope;
use ;
use crateAppError;
use ;
use ;
use OnceLock;
/// Member marking an `agent_surface` record as describing a stream.
///
/// A consumer that reads the block off a summary line needs to know the counts
/// in it are about N lines rather than about the one it is holding.
const STREAM_KEY: &str = "stream";
/// Member counting the records `--truncate-content` actually shortened.
const RECORDS_TRUNCATED_KEY: &str = "records_truncated";
/// What one stream resolved before its first line, and what it did after.
///
/// Built once by [`open_with`] and read by every emission. The projection paths
/// are compiled HERE rather than per line for the same reason
/// [`shape::project`] compiles them once for a `Vec`: splitting a dotted key
/// inside the emission loop would allocate a `Vec<String>`, plus a `String` per
/// segment, for every record times every key. A stream has no `Vec` to hoist the
/// work out of, so the hoisting has to be the stream's own state.
/// Resolves a stream's request against its records, before anything is emitted.
///
/// `sample` is a bounded prefix of the records the command is about to write,
/// and `total` is how many there really are. See [`gate::evaluate_stream`] for
/// why it is a prefix and not the whole set.
///
/// `total` exists so the bound gets DECLARED. [`Scope::vocabulary_is_partial`]
/// compares the elements it was handed against its own sampling constant, and a
/// prefix of exactly that size compares equal — so a 100 000-record export judged
/// on 64 records would have reported a complete vocabulary. Passing the real
/// count is what turns "I judged a prefix" from an implementation detail into a
/// field on the trailer.
///
/// # Errors
/// Returns [`AppError::Usage`] — exit `2` — when a knob cannot act on a stream,
/// or when `--select` names nothing any record carries. Both happen with stdout
/// still untouched, which is the whole point of resolving up front.
/// Applies the per-record knobs to one line. Never annotates it.
///
/// The absence of an `agent_surface` insertion here is the contract, not an
/// omission — see the module docs.
/// Annotates the trailer with the one record for the whole stream.
///
/// Deliberately does NOT project, filter or cap. The summary line is the stream
/// describing itself; a `--select` aimed at the records has no business either
/// failing on it or rewriting it, and both of those were measured defects.
static STREAM: = new;
/// Opens the process's stream. First call wins.
///
/// # Errors
/// Propagates the refusal from [`open_with`], so a streaming command can fail
/// before it writes its first record simply by using `?`.
/// How many records [`open`] needs to see to resolve a projection.
///
/// The surface's own sampling constant, reused rather than restated: judging a
/// stream's vocabulary and suggesting names for a failed key are the same
/// question about the same records, and two constants for one question is how
/// they drift.
pub const SAMPLE_RECORDS: usize = crateK_VOCABULARY_SAMPLE_ELEMENTS;
/// The open stream, or an inert one when the command never opened it.