car-server-core 0.52.1

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
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
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
//! The gate behind [`CoderState::Reported`] — when a session may conclude that
//! **no code should change**, and when it may not.
//!
//! ## Nomination is not adjudication
//!
//! The model's `report_no_change` action records a *candidate* finding. It never
//! transitions the session. Everything that decides the outcome is checked here,
//! outside inference, against facts the runtime collected itself.
//!
//! That split is the whole defence. A verdict the model can reach by giving up
//! is an escape hatch from every hard contract, and that failure mode is worse
//! than the gap #1070 describes. So the interesting code in this module is all
//! refusal.
//!
//! ## Mutation history is monotonic
//!
//! [`MutationLedger`] latches on the first successful mutating tool call and
//! never clears. Editing and later reverting does **not** restore eligibility.
//!
//! This is deliberately unforgiving, because the alternative is a laundry: try
//! to fix it, fail, revert, declare the premise wrong, exit 0. The price is real
//! and worth paying — a session that explored by editing has to keep coding or
//! start over. A clean worktree is necessary but not sufficient; the ledger is
//! what makes it not sufficient.
//!
//! ## What the runtime can actually verify
//!
//! Only one of the three no-change shapes, and only narrowly:
//!
//! - [`NoChangeKind::PremiseWrong`] — verifiable *in the operational sense that
//!   a trusted contract already passed against the untouched baseline*. The
//!   verdict this licenses reads "the contract was already satisfied before any
//!   work", and NOT "the model's explanation is correct".
//! - [`NoChangeKind::DeliberateBehavior`] — not verifiable. Passing tests
//!   establish what the code does now, never that maintainers intended it or
//!   still want it.
//! - [`NoChangeKind::NonCodeDecision`] — not verifiable. Depends on policy,
//!   ownership, rollout and priorities that live outside the worktree.
//!
//! A red baseline proves none of them. Source inspection and model reasoning are
//! evidence for a human reviewer, never an independent gate.
//!
//! ## What the observation still cannot see, and why that is survivable
//!
//! [`worktree_fingerprint`] samples at shell-call *boundaries*, so a mutation
//! made and undone inside a single invocation — `sed -i …; cargo test;
//! git checkout -- .` — leaves `before == after` and records nothing. Writes to
//! `.gitignore`d paths are invisible too, deliberately: `--ignored` would make
//! every `cargo build` register as a mutation and put the terminal permanently
//! out of reach.
//!
//! Both falsify the weaker claim ("this session edited nothing"). Neither
//! reaches the terminal that matters, and the reason is worth being precise
//! about, because it is what the design actually rests on:
//!
//! **the autonomous path is gated on the baseline, and the baseline is computed
//! before the loop and held in memory.** No amount of editing, reverting,
//! committing or hiding during the session can retro-change it. A genuinely
//! hard task has a red baseline, so `baseline_gates_nothing` is false and the
//! nomination routes to a human at bucket 3 — a park, never an exit-0 success.
//! The give-up scenario this module exists to prevent therefore cannot be
//! reached by defeating the fingerprint; it can only be reached by defeating the
//! baseline, which the model never gets to touch.
//!
//! What the fingerprint and the ledger buy on top is the integrity of the
//! *report*: a session that says "I changed nothing" should not have changed
//! something. That is worth having, and it is a weaker claim than the terminal.

use super::contract::{baseline_gates_nothing, CheckResult};
use super::session::{ContractProvenance, NoChangeKind};

/// Upper bound on the nominated text. Generous for a real finding, small enough
/// that a transcript dump cannot be laundered into an issue body.
pub const MAX_FINDING_TEXT: usize = 4096;

/// Latches once a mutating tool has succeeded, and never clears.
///
/// See the module docs on why this does not reset when the worktree is restored.
#[derive(Debug, Default)]
pub struct MutationLedger {
    mutated: std::sync::atomic::AtomicBool,
}

impl MutationLedger {
    pub fn new() -> Self {
        Self::default()
    }

    /// Record that a mutating tool call succeeded. Idempotent, and one-way.
    pub fn record_mutation(&self) {
        self.mutated
            .store(true, std::sync::atomic::Ordering::SeqCst);
    }

    pub fn has_mutated(&self) -> bool {
        self.mutated.load(std::sync::atomic::Ordering::SeqCst)
    }
}

/// A cheap content-sensitive summary of the worktree's state.
///
/// Two git reads: `git status --porcelain -uall` (which paths are modified,
/// staged, or untracked) and `git diff HEAD` (the tracked content itself). Their
/// hash changes whenever a tool edits, creates or deletes anything git can see.
///
/// **Known gap**: editing the *contents* of a file that is already untracked
/// does not move the hash, because `status` lists the path and `diff HEAD` does
/// not cover it. That is a narrow hole and it is not the one that matters —
/// a nomination also requires a clean worktree, and an untracked file's mere
/// existence already shows up in `status`, so a session cannot arrive at a
/// "clean" fingerprint while carrying an edit git would report.
///
/// Returns `None` when git cannot answer, which callers must treat as
/// "cannot establish cleanliness" rather than as clean.
pub fn worktree_fingerprint(dir: &std::path::Path) -> Option<String> {
    use sha2::{Digest, Sha256};
    let mut hasher = Sha256::new();
    for part in observation(dir)? {
        hasher.update(&part);
        hasher.update([0u8]);
    }
    Some(format!("{:x}", hasher.finalize()))
}

/// The four git reads that together define "this session changed nothing".
///
/// Each one closes a hole the others do not, and all four were found by
/// reproducing the bypass against a real repository rather than by reasoning:
///
/// - `rev-parse HEAD` — **commit identity**. Without it,
///   `sed -i … && git add -A && git commit` leaves `status` and `diff HEAD`
///   both empty, so a real, persistent edit is invisible. The only thing
///   standing against that was a line in the system prompt asking the model not
///   to commit, and a prompt is not a control: under this feature's own threat
///   model the reader is a model looking for a non-loss exit.
/// - `status --porcelain -uall` — modified, staged and untracked paths.
/// - `diff HEAD` — tracked content, so an edit staged *and* unstaged still shows.
/// - `ls-files -v` — **index flags**. `git update-index --assume-unchanged f`
///   changes neither status nor diff, and every later edit to that path is then
///   invisible to both, permanently. The flag lives in `.git/`, outside
///   everything else here. Lowercase tags and `S` mark it.
fn observation(dir: &std::path::Path) -> Option<Vec<Vec<u8>>> {
    let read = |args: &[&str]| -> Option<Vec<u8>> {
        let out = std::process::Command::new("git")
            .current_dir(dir)
            .args(args)
            .output()
            .ok()?;
        out.status.success().then_some(out.stdout)
    };
    Some(vec![
        read(&["rev-parse", "HEAD"])?,
        read(&["status", "--porcelain", "-uall"])?,
        read(&["diff", "HEAD"])?,
        read(&["ls-files", "-v"])?,
    ])
}

/// Whether any path carries an index flag that hides later edits from
/// `status`/`diff` — `assume-unchanged` (lowercase tag) or `skip-worktree`
/// (`S`). `git ls-files -v` prefixes each path with its tag.
fn has_hiding_index_flags(ls_files_v: &[u8]) -> bool {
    String::from_utf8_lossy(ls_files_v).lines().any(|line| {
        line.chars()
            .next()
            .is_some_and(|c| c == 'S' || c.is_ascii_lowercase())
    })
}

/// Whether the worktree currently carries no change git can see.
///
/// `None` when git cannot answer. A caller must treat that as "cannot establish
/// cleanliness", never as clean — the whole point of the check is that the
/// burden of proof sits on the session claiming it changed nothing.
pub fn worktree_is_pristine(dir: &std::path::Path, expected_head: &str) -> Option<bool> {
    let parts = observation(dir)?;
    let head = String::from_utf8_lossy(&parts[0]).trim().to_string();
    Some(
        head == expected_head.trim()
            && parts[1].is_empty()
            && parts[2].is_empty()
            && !has_hiding_index_flags(&parts[3]),
    )
}

/// The commit a session started on, captured before any model turn so a later
/// `git commit` cannot pass itself off as a clean tree.
pub fn head_commit(dir: &std::path::Path) -> Option<String> {
    let out = std::process::Command::new("git")
        .current_dir(dir)
        .args(["rev-parse", "HEAD"])
        .output()
        .ok()?;
    out.status
        .success()
        .then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}

/// Why a nomination was refused. Each variant is a distinct thing to tell the
/// model, because "try again differently" and "you cannot get here from where
/// you are" are different instructions.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NominationRefusal {
    /// `summary` or `evidence` was empty, whitespace, or over the cap.
    UnusableText(&'static str),
    /// A mutating tool succeeded earlier in this session.
    WorktreeWasMutated,
    /// The worktree differs from the captured baseline right now.
    WorktreeNotClean,
    /// The baseline did not complete — starved by the session deadline, or a
    /// check killed by infrastructure. A baseline that did not finish is not
    /// evidence of anything.
    BaselineIncomplete,
}

impl NominationRefusal {
    /// The message handed back to the model as the tool result.
    pub fn message(&self) -> String {
        match self {
            Self::UnusableText(which) => format!(
                "report_no_change refused: `{which}` must be non-empty and under \
                 {MAX_FINDING_TEXT} characters. State the conclusion and what you \
                 examined to reach it."
            ),
            Self::WorktreeWasMutated => {
                "report_no_change refused: this session already made a successful edit. \
                 Reverting does not restore eligibility — a no-change finding is only \
                 available to a session that never changed anything. Continue toward a \
                 diff, or start a fresh session to investigate."
                    .to_string()
            }
            Self::WorktreeNotClean => {
                "report_no_change refused: the worktree differs from the baseline. \
                 Restore it before concluding that nothing should change."
                    .to_string()
            }
            Self::BaselineIncomplete => {
                "report_no_change refused: the baseline evaluation did not complete, so \
                 there is nothing to conclude from. This is not something you can fix — \
                 the session needs more time or a working check environment."
                    .to_string()
            }
        }
    }
}

/// What the runtime decided to do with a nomination.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NominationVerdict {
    /// Terminal, with no human in the loop. The narrow path.
    Autonomous,
    /// Recorded, and parked for a human to accept or reject.
    NeedsHuman,
    /// Not recorded at all.
    Refused(NominationRefusal),
}

/// Whether a baseline run actually finished.
///
/// An empty result set is not a clean baseline — it is a baseline that never
/// ran. A check the *session clock* killed (`deadline_clamped` alongside
/// `timed_out`) is not a verdict on the work either, which is the same
/// distinction `code-task`'s event stream draws.
pub fn baseline_completed(results: &[CheckResult]) -> bool {
    !results.is_empty() && !results.iter().any(|r| r.timed_out && r.deadline_clamped)
}

/// The facts a nomination is judged against. All collected by the runtime; none
/// asserted by the model.
#[derive(Debug, Clone, Copy)]
pub struct NominationContext<'a> {
    pub kind: NoChangeKind,
    pub summary: &'a str,
    pub evidence: &'a str,
    pub baseline: &'a [CheckResult],
    pub provenance: ContractProvenance,
    /// The worktree matches the captured baseline right now.
    pub worktree_clean: bool,
    /// A mutating tool has succeeded at some point this session.
    pub mutated: bool,
}

/// Judge a nomination.
///
/// The ordering matters: the refusals that describe the *session's* history come
/// before the ones about the finding's content, so a model that has already
/// disqualified itself is told that rather than being invited to reword.
pub fn evaluate_nomination(ctx: NominationContext<'_>) -> NominationVerdict {
    use NominationRefusal::*;

    if ctx.mutated {
        return NominationVerdict::Refused(WorktreeWasMutated);
    }
    if !ctx.worktree_clean {
        return NominationVerdict::Refused(WorktreeNotClean);
    }
    if !baseline_completed(ctx.baseline) {
        return NominationVerdict::Refused(BaselineIncomplete);
    }
    if !usable(ctx.summary) {
        return NominationVerdict::Refused(UnusableText("summary"));
    }
    if !usable(ctx.evidence) {
        return NominationVerdict::Refused(UnusableText("evidence"));
    }

    // The one autonomous path. Every conjunct is load-bearing:
    //   - PremiseWrong is the only shape a green baseline can speak to at all.
    //   - baseline_gates_nothing means EVERY check passed untouched.
    //   - is_trusted means the model did not author the checks it is citing.
    // Drop any one and this becomes self-approval.
    let autonomous = matches!(ctx.kind, NoChangeKind::PremiseWrong)
        && baseline_gates_nothing(ctx.baseline)
        && ctx.provenance.is_trusted();

    if autonomous {
        NominationVerdict::Autonomous
    } else {
        NominationVerdict::NeedsHuman
    }
}

fn usable(text: &str) -> bool {
    let trimmed = text.trim();
    !trimmed.is_empty() && trimmed.len() <= MAX_FINDING_TEXT
}

#[cfg(test)]
mod tests {
    use super::*;

    fn check(name: &str, passed: bool) -> CheckResult {
        CheckResult {
            name: name.to_string(),
            passed,
            exit_code: Some(if passed { 0 } else { 1 }),
            duration_ms: 1,
            output_tail: String::new(),
            timed_out: false,
            deadline_clamped: false,
        }
    }

    fn ctx<'a>(
        kind: NoChangeKind,
        baseline: &'a [CheckResult],
        provenance: ContractProvenance,
    ) -> NominationContext<'a> {
        NominationContext {
            kind,
            summary: "the code already handles this",
            evidence: "read handler.rs and ran the suite",
            baseline,
            provenance,
            worktree_clean: true,
            mutated: false,
        }
    }

    #[test]
    fn the_narrow_path_terminates_autonomously() {
        let green = vec![check("a", true), check("b", true)];
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &green,
                ContractProvenance::OperatorSupplied
            )),
            NominationVerdict::Autonomous
        );
    }

    #[test]
    fn a_model_authored_contract_never_self_approves() {
        // The whole escape hatch, in one assertion: same green baseline, same
        // kind, and the only difference is who wrote the checks.
        let green = vec![check("a", true)];
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &green,
                ContractProvenance::ModelDerived
            )),
            NominationVerdict::NeedsHuman
        );
    }

    #[test]
    fn a_runtime_generated_reproduction_is_trusted() {
        // This is what lets autonomous self-triage work at all: the contract
        // came from telemetry CAR collected before the session existed.
        let green = vec![check("repro", true)];
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &green,
                ContractProvenance::RuntimeGenerated
            )),
            NominationVerdict::Autonomous
        );
    }

    #[test]
    fn the_two_judgement_shapes_always_reach_a_human() {
        let green = vec![check("a", true)];
        for kind in [
            NoChangeKind::DeliberateBehavior,
            NoChangeKind::NonCodeDecision,
        ] {
            assert_eq!(
                evaluate_nomination(ctx(kind, &green, ContractProvenance::OperatorSupplied)),
                NominationVerdict::NeedsHuman,
                "{kind:?} is not runtime-verifiable"
            );
        }
    }

    #[test]
    fn a_red_baseline_proves_nothing_and_parks() {
        let mixed = vec![check("a", true), check("b", false)];
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &mixed,
                ContractProvenance::OperatorSupplied
            )),
            NominationVerdict::NeedsHuman
        );
    }

    #[test]
    fn editing_then_reverting_does_not_restore_eligibility() {
        let green = vec![check("a", true)];
        let mut c = ctx(
            NoChangeKind::PremiseWrong,
            &green,
            ContractProvenance::OperatorSupplied,
        );
        // Worktree is clean again — the revert worked. The ledger still says no.
        c.worktree_clean = true;
        c.mutated = true;
        assert_eq!(
            evaluate_nomination(c),
            NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
        );
    }

    #[test]
    fn a_dirty_worktree_is_refused() {
        let green = vec![check("a", true)];
        let mut c = ctx(
            NoChangeKind::PremiseWrong,
            &green,
            ContractProvenance::OperatorSupplied,
        );
        c.worktree_clean = false;
        assert_eq!(
            evaluate_nomination(c),
            NominationVerdict::Refused(NominationRefusal::WorktreeNotClean)
        );
    }

    #[test]
    fn a_starved_baseline_is_not_evidence() {
        let mut killed = check("a", false);
        killed.timed_out = true;
        killed.deadline_clamped = true;
        let results = vec![killed];
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &results,
                ContractProvenance::OperatorSupplied
            )),
            NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
        );
    }

    #[test]
    fn an_empty_baseline_is_not_a_clean_one() {
        assert!(!baseline_completed(&[]));
        assert_eq!(
            evaluate_nomination(ctx(
                NoChangeKind::PremiseWrong,
                &[],
                ContractProvenance::OperatorSupplied
            )),
            NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
        );
    }

    #[test]
    fn empty_or_oversized_text_is_refused() {
        let green = vec![check("a", true)];
        let mut c = ctx(
            NoChangeKind::PremiseWrong,
            &green,
            ContractProvenance::OperatorSupplied,
        );
        c.summary = "   ";
        assert_eq!(
            evaluate_nomination(c),
            NominationVerdict::Refused(NominationRefusal::UnusableText("summary"))
        );

        let huge = "x".repeat(MAX_FINDING_TEXT + 1);
        let mut c2 = ctx(
            NoChangeKind::PremiseWrong,
            &green,
            ContractProvenance::OperatorSupplied,
        );
        c2.evidence = &huge;
        assert_eq!(
            evaluate_nomination(c2),
            NominationVerdict::Refused(NominationRefusal::UnusableText("evidence"))
        );
    }

    #[test]
    fn session_history_is_reported_before_content() {
        // A disqualified session must be told it is disqualified, not invited to
        // reword its way in.
        let green = vec![check("a", true)];
        let mut c = ctx(
            NoChangeKind::PremiseWrong,
            &green,
            ContractProvenance::OperatorSupplied,
        );
        c.mutated = true;
        c.summary = "";
        assert_eq!(
            evaluate_nomination(c),
            NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
        );
    }

    /// Helper: a real git repo with one committed file.
    fn git_repo() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        let git = |args: &[&str]| {
            std::process::Command::new("git")
                .current_dir(dir.path())
                .args(args)
                .output()
                .unwrap();
        };
        git(&["init", "-q"]);
        git(&["config", "user.email", "t@t"]);
        git(&["config", "user.name", "t"]);
        std::fs::write(dir.path().join("f.txt"), "original\n").unwrap();
        git(&["add", "-A"]);
        git(&["commit", "-qm", "init"]);
        dir
    }

    fn git(dir: &std::path::Path, args: &[&str]) {
        std::process::Command::new("git")
            .current_dir(dir)
            .args(args)
            .output()
            .unwrap();
    }

    /// Committing an edit leaves `status` and `diff HEAD` both empty, so an
    /// observation built only from those two reports a pristine tree over a
    /// real, persistent change. Binding HEAD is what closes it.
    ///
    /// The only other thing standing against this is a line in the system
    /// prompt asking the model not to commit — and under this feature's own
    /// threat model, a prompt is not a control.
    #[test]
    fn a_committed_edit_is_not_a_pristine_tree() {
        let dir = git_repo();
        let start = head_commit(dir.path()).expect("HEAD readable");
        let before = worktree_fingerprint(dir.path()).unwrap();
        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(true));

        std::fs::write(dir.path().join("f.txt"), "mutated\n").unwrap();
        git(dir.path(), &["add", "-A"]);
        git(dir.path(), &["commit", "-qm", "sneak"]);

        assert_ne!(
            worktree_fingerprint(dir.path()).unwrap(),
            before,
            "a commit must move the fingerprint"
        );
        assert_eq!(
            worktree_is_pristine(dir.path(), &start),
            Some(false),
            "HEAD moved, so the tree is not the one this session started on"
        );
    }

    /// `git update-index --assume-unchanged` changes neither status nor diff,
    /// and every later edit to that path is then invisible to both —
    /// permanently, from metadata living in `.git/`. `ls-files -v` is the only
    /// one of the four reads that sees it.
    #[test]
    fn an_assume_unchanged_flag_is_not_a_pristine_tree() {
        let dir = git_repo();
        let start = head_commit(dir.path()).expect("HEAD readable");
        let before = worktree_fingerprint(dir.path()).unwrap();

        git(dir.path(), &["update-index", "--assume-unchanged", "f.txt"]);

        assert_ne!(
            worktree_fingerprint(dir.path()).unwrap(),
            before,
            "setting the flag must itself register as a mutation"
        );
        assert_eq!(
            worktree_is_pristine(dir.path(), &start),
            Some(false),
            "a tree that can hide later edits is not pristine"
        );

        // And the edit it was hiding stays invisible to status/diff — which is
        // exactly why the flag itself has to be what disqualifies the session.
        std::fs::write(dir.path().join("f.txt"), "tampered\n").unwrap();
        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
    }

    #[test]
    fn skip_worktree_is_caught_too() {
        let dir = git_repo();
        let start = head_commit(dir.path()).expect("HEAD readable");
        git(dir.path(), &["update-index", "--skip-worktree", "f.txt"]);
        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
    }

    #[test]
    fn an_ordinary_edit_still_shows() {
        let dir = git_repo();
        let start = head_commit(dir.path()).expect("HEAD readable");
        std::fs::write(dir.path().join("f.txt"), "edited\n").unwrap();
        assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
    }

    #[test]
    fn a_head_that_does_not_match_is_never_pristine() {
        let dir = git_repo();
        assert_eq!(
            worktree_is_pristine(dir.path(), "0000000000000000000000000000000000000000"),
            Some(false)
        );
    }

    #[test]
    fn the_ledger_is_one_way() {
        let ledger = MutationLedger::new();
        assert!(!ledger.has_mutated());
        ledger.record_mutation();
        ledger.record_mutation();
        assert!(ledger.has_mutated());
    }

    #[test]
    fn trusted_provenance_has_exactly_one_untrusted_member() {
        assert!(ContractProvenance::OperatorSupplied.is_trusted());
        assert!(ContractProvenance::HumanConfirmed.is_trusted());
        assert!(ContractProvenance::RuntimeGenerated.is_trusted());
        assert!(
            !ContractProvenance::ModelDerived.is_trusted(),
            "a contract the session's own model wrote is never trusted — widening \
             this is how the gate stops working"
        );
    }
}