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
448
449
450
451
452
453
454
455
456
457
458
459
//! Narrowed inputs: named probes whose value joins a rule's input digest.
//!
//! A scope can only name whole files, so a rule that depends on part of a file
//! has to hash all of it — one recipe body in a `Justfile` costs every rule
//! sharing that file. A probe closes the gap: it produces bytes, they are
//! hashed, and that hash joins the input digest of every rule whose `inputs:`
//! names the probe, exactly as a scope name does.
//!
//! # Two sources, one of them free
//!
//! `file:` plus `json:` reads a file and selects out of it **with no process at
//! all** — nothing spawned, nothing on `PATH`, no shell quoting, and no
//! per-probe process on a `mmz --is-fresh` that gates every rule at once:
//!
//! ```yaml
//! probes:
//! qahq-input:
//! file: flake.lock
//! json: '.nodes["qahq"]["locked"]["narHash"]'
//! ```
//!
//! `run:` is the other source: a shell command line whose stdout is the bytes.
//! It may carry a `json:` too, which selects out of that stdout instead of a
//! file — half the spawns of the `| jq …` spelling, and canonical by
//! construction (see below).
//!
//! ```yaml
//! probes:
//! fmt-recipe:
//! run: just --dump --dump-format json
//! json: '.recipes["fmt-check"]'
//! commands:
//! - name: just fmt-check
//! inputs: [rust, fmt-recipe]
//! ```
//!
//! The two sources are mutually exclusive, and a probe must declare one. A
//! `file:` with no selector is refused too: hashing a whole file is what a
//! scope is for, and a second spelling of it here would quietly skip the
//! gitignore filter and the per-file digest a scope gives for free.
//!
//! # Two selectors, and why a probe takes one
//!
//! `json:` narrows a document to a value. `ast:` narrows *source code* to a
//! structural slice — a function, a type, an impl block — by parsing it and
//! matching an ast-grep pattern, again with nothing spawned:
//!
//! ```yaml
//! probes:
//! wire-types:
//! file: src/types.rs
//! ast: 'pub struct $NAME { $$$FIELDS }'
//! ```
//!
//! That reaches an input the other keys cannot spell. A scope naming
//! `src/types.rs` hashes the file, so a reworded comment re-runs the rule; the
//! probe above moves only when a public struct definition does. A match is a
//! whole node by default, so what a pattern spans is what the rule depends on
//! — and `capture:` narrows that to the metavariables it names, which is the
//! only way to keep a signature and drop the body a grammar glued to it. See
//! [`crate::ast`] for what it hashes and [`crate::ast_lang`] for which
//! languages a build carries.
//!
//! The two are mutually exclusive. Chaining them would need a rule for how a
//! matched node becomes a JSON document, which is a data model mmz would be
//! inventing, and precedence between them is how a manifest comes to mean
//! something its author cannot see by reading it.
//!
//! # Why the selection is hashed, not the bytes
//!
//! A `json:` probe hashes mmz's own canonical rendering of the selected value
//! — object keys sorted, arrays left alone — never the input's bytes. So key
//! order stops being an input by construction rather than by every author
//! remembering `jq -S`, which is a convention someone can forget and this repo
//! once did. See [`crate::json`].
//!
//! # Failure modes, and where each one is owned
//!
//! The primitive can lie in a way a file hash cannot, so the boundary is the
//! design:
//!
//! - A probe that exits non-zero is a hard error naming the probe, its exit
//! code, and its stderr. mmz exits without consuming the output and without
//! writing a record — a failed command never reaches the hasher.
//! - A probe that cannot be spawned is the same error.
//! - Empty stdout is an error by default, with `allow_empty: true` to opt in. It
//! is the cheapest catch for a selector that matched nothing.
//! - A `file:` that does not exist, or cannot be read, is a hard error naming
//! the probe and the path. The path resolves against the project root — the
//! same base scope globs use.
//! - Bytes that are not one JSON value are a hard error, whether they came off
//! disk or out of a command. A tool that logs a line before its JSON, or a
//! half-written lockfile, is a state mmz refuses rather than hashes.
//! - A `json:` program that does not compile, or that raises against the
//! document it was pointed at, is a hard error naming the probe and quoting
//! the program.
//! - **A `json:` selector that yields nothing — no output at all, or a lone
//! `null` — is a hard error.** This is the same refusal as empty stdout, at
//! the place the selection happens, and it is why `jq -e` is load-bearing on
//! every shelled-out probe here: a probe tracking `null` reports one digest
//! whatever the document does, so the rule reads fresh forever against an
//! input nobody is measuring. `false` is a value and passes; jq's `-e`
//! conflates it with `null` only because a shell exit code cannot tell them
//! apart.
//! - **An `ast:` pattern that matched no node is a hard error**, for exactly
//! the reason above: a probe measuring nothing reports one digest whatever
//! the file does. A pattern that does not compile for its language, bytes
//! that are not UTF-8, and a `lang:` this build has no grammar for are each a
//! hard error too — and the last of those names the cargo feature that fixes
//! it, because it is the only failure here whose answer is a build rather
//! than an edit. mmz never falls back to matching nothing.
//! - `allow_empty: true` opts into exactly that, on every source and selector:
//! with `json:` it accepts a selection that yielded nothing or only `null`,
//! with `ast:` a pattern that matched no node, and with neither it accepts
//! whitespace-only stdout. One key, one meaning — "empty really is a valid
//! input here" — so a reader who knows it from `run:` already knows what it
//! does beside a selector.
//! - Content correctness is the consumer's. A probe that prints valid but wrong
//! output, or that is not deterministic, is the manifest author's bug: pin the
//! ordering, strip the timestamps, assert the shape in the probe itself
//! (`jq -S -e`, a schema check) so a bad shape becomes a non-zero exit and hits
//! the rule above. mmz does not validate meaning, and should not learn to.
//!
//! - The environment is the manifest's too. A `run` line resolves through
//! whatever `PATH` the caller had, so the same probe measured under two
//! shells can disagree without the project changing at all. `probe_shell`
//! pins the argv the line is handed to — `["direnv", "exec", ".", "sh",
//! "-c"]` and the like — so a probe reading project tooling measures the
//! project's tooling rather than the operator's. mmz cannot detect the
//! mismatch, which is why the key exists to prevent it.
//!
//! A wrong scope costs time; a wrong probe can lie. That asymmetry is why every
//! case above fails closed.
use BTreeMap;
use ;
use ;
use Deserialize;
use crate;
use crate;
/// The shell a probe's `run` line is handed to when the manifest does not set
/// `probe_shell`, so a pipeline, a quoted argument, or a redirect all work as
/// written. Lives in [`crate::manifest::default_probe_shell`]; named here only
/// so the module that spawns it says which default it spawns.
pub const DEFAULT_SHELL: = ;
/// Longest stderr excerpt carried in a failure message, in bytes. A runaway
/// probe should not bury its own first line under a megabyte of noise.
const STDERR_CAP: usize = 2000;
/// A named source of input bytes: a file to read, or a command to run, and
/// optionally a jq program to select out of it.
///
/// A `file` path and a `run` line both resolve against the project root — the
/// same base that scope globs resolve against, so a probe reading the project's
/// own files needs no path juggling. A `run` line is executed by `sh -c` with
/// stdin closed, so a probe that waits on input fails instead of hanging a
/// gate; stderr is captured and surfaced only when the probe fails.
///
/// Every field is optional here because the legal combinations are a rule about
/// the *set* of keys, not about any one of them — and refusing them at
/// [`validate`] rather than in a `serde` conversion is what lets the error name
/// the probe. See [`Probe::check_shape`].
/// What a probe reads, once the shape rules have ruled out the combinations
/// that name neither source or both.
/// Which combinations of `run:`, `file:`, `json:`, `ast:` and `lang:` are
/// legal, and what each illegal one says — split out to `probe_shape.rs` once
/// this file reached its line cap. Still `impl Probe`, so no call site knows
/// the split happened.
/// What the bytes become before they are hashed — raw, a `json:` selection or
/// an `ast:` match set — split out to `probe_digest.rs` for the same reason.
/// Why a running probe produced no usable bytes, split out to
/// `probe_failure.rs` because `src/error.rs` was the file at its cap. Public
/// through this module, not that one: the failures belong beside the code that
/// raises them.
pub use ProbeFailure;
/// Rejects a probe that shares a name with a scope, or whose source keys do
/// not describe one readable thing.
///
/// `inputs:` has one namespace, so a reader must never have to guess which kind
/// a name is — and mmz must never have to pick one. The shape check runs over
/// every declared probe, not only the ones a rule names, so a malformed probe
/// is refused the moment the manifest loads rather than on whichever
/// invocation first happens to reach it.
///
/// # Errors
///
/// Returns [`Error::EmptyProbeShell`], [`Error::NameCollision`] naming the
/// doubly-claimed name, or [`Error::ProbeSource`] naming the malformed probe.
/// The first probe whose digest moved since a record was written, comparing the
/// record's stored map against the current one.
///
/// A stale rule whose probe changed should say so: sending a reader to diff the
/// files when no file moved is the same wrong-place failure a missing output
/// would cause. A probe absent from the record (newly added to the rule) counts
/// as changed. Names compare in sorted order, so the answer is stable.
/// Resolves probes for one mmz invocation, running each declared probe at most
/// once however many rules name it.
///
/// A bare `mmz --is-fresh` gates every rule in the manifest and runs in git
/// hooks, so the shape that matters is many rules sharing one probe: eighteen
/// references must cost one process, not eighteen. The memo is per-[`Resolver`]
/// rather than a process-global static on purpose — a long-lived library caller
/// would otherwise be pinned forever to the first digest it ever saw, which is
/// exactly the stale input mmz exists to prevent. `mmz` builds one resolver per
/// invocation, so for the CLI the two readings coincide.
/// Resolves one probe from `base` and hashes what it produced.
///
/// Every failure path returns before the hash, so a command that failed, a file
/// that would not read, a document that would not parse, and a selector that
/// matched nothing all stop here rather than contributing bytes to an input
/// digest.
///
/// The selectors are checked in a fixed order only because the code has to pick
/// one; [`Probe::check_shape`] has already refused a probe declaring both, so
/// the order is never a precedence rule anyone can observe.
/// Runs the probe's `run` line and returns its stdout, refusing a non-zero
/// exit before the bytes are looked at.
/// Reads the probe's `file`, relative to the project root — the base scope
/// globs resolve against, so the two kinds of input name a path the same way.
/// Spawns the probe under `shell` from the project root, with stdin closed and
/// both output streams captured.
///
/// `shell` is the manifest's `probe_shell` (default [`DEFAULT_SHELL`]): its
/// first element is the program, the rest are fixed arguments, and the probe's
/// `run` line is appended as one final argument. [`validate`] has already
/// refused an empty list, so the indexing below cannot panic.
/// Renders a probe's captured stderr for a failure message: trimmed, capped at
/// [`STDERR_CAP`], and named explicitly when the probe said nothing at all (so
/// the message never trails off into blank space).