car_server_core/coder/no_change.rs
1//! The gate behind `CoderState::Reported` — when a session may conclude that
2//! **no code should change**, and when it may not.
3//!
4//! ## Nomination is not adjudication
5//!
6//! The model's `report_no_change` action records a *candidate* finding. It never
7//! transitions the session. Everything that decides the outcome is checked here,
8//! outside inference, against facts the runtime collected itself.
9//!
10//! That split is the whole defence. A verdict the model can reach by giving up
11//! is an escape hatch from every hard contract, and that failure mode is worse
12//! than the gap #1070 describes. So the interesting code in this module is all
13//! refusal.
14//!
15//! ## Mutation history is monotonic
16//!
17//! [`MutationLedger`] latches on the first successful mutating tool call and
18//! never clears. Editing and later reverting does **not** restore eligibility.
19//!
20//! This is deliberately unforgiving, because the alternative is a laundry: try
21//! to fix it, fail, revert, declare the premise wrong, exit 0. The price is real
22//! and worth paying — a session that explored by editing has to keep coding or
23//! start over. A clean worktree is necessary but not sufficient; the ledger is
24//! what makes it not sufficient.
25//!
26//! ## What the runtime can actually verify
27//!
28//! Only one of the three no-change shapes, and only narrowly:
29//!
30//! - [`NoChangeKind::PremiseWrong`] — verifiable *in the operational sense that
31//! a trusted contract already passed against the untouched baseline*. The
32//! verdict this licenses reads "the contract was already satisfied before any
33//! work", and NOT "the model's explanation is correct".
34//! - [`NoChangeKind::DeliberateBehavior`] — not verifiable. Passing tests
35//! establish what the code does now, never that maintainers intended it or
36//! still want it.
37//! - [`NoChangeKind::NonCodeDecision`] — not verifiable. Depends on policy,
38//! ownership, rollout and priorities that live outside the worktree.
39//!
40//! A red baseline proves none of them. Source inspection and model reasoning are
41//! evidence for a human reviewer, never an independent gate.
42//!
43//! ## What the observation still cannot see, and why that is survivable
44//!
45//! [`worktree_fingerprint`] samples at shell-call *boundaries*, so a mutation
46//! made and undone inside a single invocation — `sed -i …; cargo test;
47//! git checkout -- .` — leaves `before == after` and records nothing. Writes to
48//! `.gitignore`d paths are invisible too, deliberately: `--ignored` would make
49//! every `cargo build` register as a mutation and put the terminal permanently
50//! out of reach.
51//!
52//! Both falsify the weaker claim ("this session edited nothing"). Neither
53//! reaches the terminal that matters, and the reason is worth being precise
54//! about, because it is what the design actually rests on:
55//!
56//! **the autonomous path is gated on the baseline, and the baseline is computed
57//! before the loop and held in memory.** No amount of editing, reverting,
58//! committing or hiding during the session can retro-change it. A genuinely
59//! hard task has a red baseline, so `baseline_gates_nothing` is false and the
60//! nomination routes to a human at bucket 3 — a park, never an exit-0 success.
61//! The give-up scenario this module exists to prevent therefore cannot be
62//! reached by defeating the fingerprint; it can only be reached by defeating the
63//! baseline, which the model never gets to touch.
64//!
65//! What the fingerprint and the ledger buy on top is the integrity of the
66//! *report*: a session that says "I changed nothing" should not have changed
67//! something. That is worth having, and it is a weaker claim than the terminal.
68
69use super::contract::{baseline_gates_nothing, CheckResult};
70use super::session::{ContractProvenance, NoChangeKind};
71
72/// Upper bound on the nominated text. Generous for a real finding, small enough
73/// that a transcript dump cannot be laundered into an issue body.
74pub const MAX_FINDING_TEXT: usize = 4096;
75
76/// Latches once a mutating tool has succeeded, and never clears.
77///
78/// See the module docs on why this does not reset when the worktree is restored.
79#[derive(Debug, Default)]
80pub struct MutationLedger {
81 mutated: std::sync::atomic::AtomicBool,
82}
83
84impl MutationLedger {
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 /// Record that a mutating tool call succeeded. Idempotent, and one-way.
90 pub fn record_mutation(&self) {
91 self.mutated
92 .store(true, std::sync::atomic::Ordering::SeqCst);
93 }
94
95 pub fn has_mutated(&self) -> bool {
96 self.mutated.load(std::sync::atomic::Ordering::SeqCst)
97 }
98}
99
100/// A cheap content-sensitive summary of the worktree's state.
101///
102/// Two git reads: `git status --porcelain -uall` (which paths are modified,
103/// staged, or untracked) and `git diff HEAD` (the tracked content itself). Their
104/// hash changes whenever a tool edits, creates or deletes anything git can see.
105///
106/// **Known gap**: editing the *contents* of a file that is already untracked
107/// does not move the hash, because `status` lists the path and `diff HEAD` does
108/// not cover it. That is a narrow hole and it is not the one that matters —
109/// a nomination also requires a clean worktree, and an untracked file's mere
110/// existence already shows up in `status`, so a session cannot arrive at a
111/// "clean" fingerprint while carrying an edit git would report.
112///
113/// Returns `None` when git cannot answer, which callers must treat as
114/// "cannot establish cleanliness" rather than as clean.
115pub fn worktree_fingerprint(dir: &std::path::Path) -> Option<String> {
116 use sha2::{Digest, Sha256};
117 let mut hasher = Sha256::new();
118 for part in observation(dir)? {
119 hasher.update(&part);
120 hasher.update([0u8]);
121 }
122 Some(format!("{:x}", hasher.finalize()))
123}
124
125/// The four git reads that together define "this session changed nothing".
126///
127/// Each one closes a hole the others do not, and all four were found by
128/// reproducing the bypass against a real repository rather than by reasoning:
129///
130/// - `rev-parse HEAD` — **commit identity**. Without it,
131/// `sed -i … && git add -A && git commit` leaves `status` and `diff HEAD`
132/// both empty, so a real, persistent edit is invisible. The only thing
133/// standing against that was a line in the system prompt asking the model not
134/// to commit, and a prompt is not a control: under this feature's own threat
135/// model the reader is a model looking for a non-loss exit.
136/// - `status --porcelain -uall` — modified, staged and untracked paths.
137/// - `diff HEAD` — tracked content, so an edit staged *and* unstaged still shows.
138/// - `ls-files -v` — **index flags**. `git update-index --assume-unchanged f`
139/// changes neither status nor diff, and every later edit to that path is then
140/// invisible to both, permanently. The flag lives in `.git/`, outside
141/// everything else here. Lowercase tags and `S` mark it.
142fn observation(dir: &std::path::Path) -> Option<Vec<Vec<u8>>> {
143 let read = |args: &[&str]| -> Option<Vec<u8>> {
144 let out = std::process::Command::new("git")
145 .current_dir(dir)
146 .args(args)
147 .output()
148 .ok()?;
149 out.status.success().then_some(out.stdout)
150 };
151 Some(vec![
152 read(&["rev-parse", "HEAD"])?,
153 read(&["status", "--porcelain", "-uall"])?,
154 read(&["diff", "HEAD"])?,
155 read(&["ls-files", "-v"])?,
156 ])
157}
158
159/// Whether any path carries an index flag that hides later edits from
160/// `status`/`diff` — `assume-unchanged` (lowercase tag) or `skip-worktree`
161/// (`S`). `git ls-files -v` prefixes each path with its tag.
162fn has_hiding_index_flags(ls_files_v: &[u8]) -> bool {
163 String::from_utf8_lossy(ls_files_v).lines().any(|line| {
164 line.chars()
165 .next()
166 .is_some_and(|c| c == 'S' || c.is_ascii_lowercase())
167 })
168}
169
170/// Whether the worktree currently carries no change git can see.
171///
172/// `None` when git cannot answer. A caller must treat that as "cannot establish
173/// cleanliness", never as clean — the whole point of the check is that the
174/// burden of proof sits on the session claiming it changed nothing.
175pub fn worktree_is_pristine(dir: &std::path::Path, expected_head: &str) -> Option<bool> {
176 let parts = observation(dir)?;
177 let head = String::from_utf8_lossy(&parts[0]).trim().to_string();
178 Some(
179 head == expected_head.trim()
180 && parts[1].is_empty()
181 && parts[2].is_empty()
182 && !has_hiding_index_flags(&parts[3]),
183 )
184}
185
186/// The commit a session started on, captured before any model turn so a later
187/// `git commit` cannot pass itself off as a clean tree.
188pub fn head_commit(dir: &std::path::Path) -> Option<String> {
189 let out = std::process::Command::new("git")
190 .current_dir(dir)
191 .args(["rev-parse", "HEAD"])
192 .output()
193 .ok()?;
194 out.status
195 .success()
196 .then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
197}
198
199/// Why a nomination was refused. Each variant is a distinct thing to tell the
200/// model, because "try again differently" and "you cannot get here from where
201/// you are" are different instructions.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum NominationRefusal {
204 /// `summary` or `evidence` was empty, whitespace, or over the cap.
205 UnusableText(&'static str),
206 /// A mutating tool succeeded earlier in this session.
207 WorktreeWasMutated,
208 /// The worktree differs from the captured baseline right now.
209 WorktreeNotClean,
210 /// The baseline did not complete — starved by the session deadline, or a
211 /// check killed by infrastructure. A baseline that did not finish is not
212 /// evidence of anything.
213 BaselineIncomplete,
214}
215
216impl NominationRefusal {
217 /// The message handed back to the model as the tool result.
218 pub fn message(&self) -> String {
219 match self {
220 Self::UnusableText(which) => format!(
221 "report_no_change refused: `{which}` must be non-empty and under \
222 {MAX_FINDING_TEXT} characters. State the conclusion and what you \
223 examined to reach it."
224 ),
225 Self::WorktreeWasMutated => {
226 "report_no_change refused: this session already made a successful edit. \
227 Reverting does not restore eligibility — a no-change finding is only \
228 available to a session that never changed anything. Continue toward a \
229 diff, or start a fresh session to investigate."
230 .to_string()
231 }
232 Self::WorktreeNotClean => {
233 "report_no_change refused: the worktree differs from the baseline. \
234 Restore it before concluding that nothing should change."
235 .to_string()
236 }
237 Self::BaselineIncomplete => {
238 "report_no_change refused: the baseline evaluation did not complete, so \
239 there is nothing to conclude from. This is not something you can fix — \
240 the session needs more time or a working check environment."
241 .to_string()
242 }
243 }
244 }
245}
246
247/// What the runtime decided to do with a nomination.
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum NominationVerdict {
250 /// Terminal, with no human in the loop. The narrow path.
251 Autonomous,
252 /// Recorded, and parked for a human to accept or reject.
253 NeedsHuman,
254 /// Not recorded at all.
255 Refused(NominationRefusal),
256}
257
258/// Whether a baseline run actually finished.
259///
260/// An empty result set is not a clean baseline — it is a baseline that never
261/// ran. A check the *session clock* killed (`deadline_clamped` alongside
262/// `timed_out`) is not a verdict on the work either, which is the same
263/// distinction `code-task`'s event stream draws.
264pub fn baseline_completed(results: &[CheckResult]) -> bool {
265 !results.is_empty() && !results.iter().any(|r| r.timed_out && r.deadline_clamped)
266}
267
268/// The facts a nomination is judged against. All collected by the runtime; none
269/// asserted by the model.
270#[derive(Debug, Clone, Copy)]
271pub struct NominationContext<'a> {
272 pub kind: NoChangeKind,
273 pub summary: &'a str,
274 pub evidence: &'a str,
275 pub baseline: &'a [CheckResult],
276 pub provenance: ContractProvenance,
277 /// The worktree matches the captured baseline right now.
278 pub worktree_clean: bool,
279 /// A mutating tool has succeeded at some point this session.
280 pub mutated: bool,
281}
282
283/// Judge a nomination.
284///
285/// The ordering matters: the refusals that describe the *session's* history come
286/// before the ones about the finding's content, so a model that has already
287/// disqualified itself is told that rather than being invited to reword.
288pub fn evaluate_nomination(ctx: NominationContext<'_>) -> NominationVerdict {
289 use NominationRefusal::*;
290
291 if ctx.mutated {
292 return NominationVerdict::Refused(WorktreeWasMutated);
293 }
294 if !ctx.worktree_clean {
295 return NominationVerdict::Refused(WorktreeNotClean);
296 }
297 if !baseline_completed(ctx.baseline) {
298 return NominationVerdict::Refused(BaselineIncomplete);
299 }
300 if !usable(ctx.summary) {
301 return NominationVerdict::Refused(UnusableText("summary"));
302 }
303 if !usable(ctx.evidence) {
304 return NominationVerdict::Refused(UnusableText("evidence"));
305 }
306
307 // The one autonomous path. Every conjunct is load-bearing:
308 // - PremiseWrong is the only shape a green baseline can speak to at all.
309 // - baseline_gates_nothing means EVERY check passed untouched.
310 // - is_trusted means the model did not author the checks it is citing.
311 // Drop any one and this becomes self-approval.
312 let autonomous = matches!(ctx.kind, NoChangeKind::PremiseWrong)
313 && baseline_gates_nothing(ctx.baseline)
314 && ctx.provenance.is_trusted();
315
316 if autonomous {
317 NominationVerdict::Autonomous
318 } else {
319 NominationVerdict::NeedsHuman
320 }
321}
322
323fn usable(text: &str) -> bool {
324 let trimmed = text.trim();
325 !trimmed.is_empty() && trimmed.len() <= MAX_FINDING_TEXT
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn check(name: &str, passed: bool) -> CheckResult {
333 CheckResult {
334 credentials_allowed: false,
335 name: name.to_string(),
336 passed,
337 exit_code: Some(if passed { 0 } else { 1 }),
338 duration_ms: 1,
339 output_tail: String::new(),
340 timed_out: false,
341 deadline_clamped: false,
342 }
343 }
344
345 fn ctx<'a>(
346 kind: NoChangeKind,
347 baseline: &'a [CheckResult],
348 provenance: ContractProvenance,
349 ) -> NominationContext<'a> {
350 NominationContext {
351 kind,
352 summary: "the code already handles this",
353 evidence: "read handler.rs and ran the suite",
354 baseline,
355 provenance,
356 worktree_clean: true,
357 mutated: false,
358 }
359 }
360
361 #[test]
362 fn the_narrow_path_terminates_autonomously() {
363 let green = vec![check("a", true), check("b", true)];
364 assert_eq!(
365 evaluate_nomination(ctx(
366 NoChangeKind::PremiseWrong,
367 &green,
368 ContractProvenance::OperatorSupplied
369 )),
370 NominationVerdict::Autonomous
371 );
372 }
373
374 #[test]
375 fn a_model_authored_contract_never_self_approves() {
376 // The whole escape hatch, in one assertion: same green baseline, same
377 // kind, and the only difference is who wrote the checks.
378 let green = vec![check("a", true)];
379 assert_eq!(
380 evaluate_nomination(ctx(
381 NoChangeKind::PremiseWrong,
382 &green,
383 ContractProvenance::ModelDerived
384 )),
385 NominationVerdict::NeedsHuman
386 );
387 }
388
389 #[test]
390 fn a_runtime_generated_reproduction_is_trusted() {
391 // This is what lets autonomous self-triage work at all: the contract
392 // came from telemetry CAR collected before the session existed.
393 let green = vec![check("repro", true)];
394 assert_eq!(
395 evaluate_nomination(ctx(
396 NoChangeKind::PremiseWrong,
397 &green,
398 ContractProvenance::RuntimeGenerated
399 )),
400 NominationVerdict::Autonomous
401 );
402 }
403
404 #[test]
405 fn the_two_judgement_shapes_always_reach_a_human() {
406 let green = vec![check("a", true)];
407 for kind in [
408 NoChangeKind::DeliberateBehavior,
409 NoChangeKind::NonCodeDecision,
410 ] {
411 assert_eq!(
412 evaluate_nomination(ctx(kind, &green, ContractProvenance::OperatorSupplied)),
413 NominationVerdict::NeedsHuman,
414 "{kind:?} is not runtime-verifiable"
415 );
416 }
417 }
418
419 #[test]
420 fn a_red_baseline_proves_nothing_and_parks() {
421 let mixed = vec![check("a", true), check("b", false)];
422 assert_eq!(
423 evaluate_nomination(ctx(
424 NoChangeKind::PremiseWrong,
425 &mixed,
426 ContractProvenance::OperatorSupplied
427 )),
428 NominationVerdict::NeedsHuman
429 );
430 }
431
432 #[test]
433 fn editing_then_reverting_does_not_restore_eligibility() {
434 let green = vec![check("a", true)];
435 let mut c = ctx(
436 NoChangeKind::PremiseWrong,
437 &green,
438 ContractProvenance::OperatorSupplied,
439 );
440 // Worktree is clean again — the revert worked. The ledger still says no.
441 c.worktree_clean = true;
442 c.mutated = true;
443 assert_eq!(
444 evaluate_nomination(c),
445 NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
446 );
447 }
448
449 #[test]
450 fn a_dirty_worktree_is_refused() {
451 let green = vec![check("a", true)];
452 let mut c = ctx(
453 NoChangeKind::PremiseWrong,
454 &green,
455 ContractProvenance::OperatorSupplied,
456 );
457 c.worktree_clean = false;
458 assert_eq!(
459 evaluate_nomination(c),
460 NominationVerdict::Refused(NominationRefusal::WorktreeNotClean)
461 );
462 }
463
464 #[test]
465 fn a_starved_baseline_is_not_evidence() {
466 let mut killed = check("a", false);
467 killed.timed_out = true;
468 killed.deadline_clamped = true;
469 let results = vec![killed];
470 assert_eq!(
471 evaluate_nomination(ctx(
472 NoChangeKind::PremiseWrong,
473 &results,
474 ContractProvenance::OperatorSupplied
475 )),
476 NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
477 );
478 }
479
480 #[test]
481 fn an_empty_baseline_is_not_a_clean_one() {
482 assert!(!baseline_completed(&[]));
483 assert_eq!(
484 evaluate_nomination(ctx(
485 NoChangeKind::PremiseWrong,
486 &[],
487 ContractProvenance::OperatorSupplied
488 )),
489 NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
490 );
491 }
492
493 #[test]
494 fn empty_or_oversized_text_is_refused() {
495 let green = vec![check("a", true)];
496 let mut c = ctx(
497 NoChangeKind::PremiseWrong,
498 &green,
499 ContractProvenance::OperatorSupplied,
500 );
501 c.summary = " ";
502 assert_eq!(
503 evaluate_nomination(c),
504 NominationVerdict::Refused(NominationRefusal::UnusableText("summary"))
505 );
506
507 let huge = "x".repeat(MAX_FINDING_TEXT + 1);
508 let mut c2 = ctx(
509 NoChangeKind::PremiseWrong,
510 &green,
511 ContractProvenance::OperatorSupplied,
512 );
513 c2.evidence = &huge;
514 assert_eq!(
515 evaluate_nomination(c2),
516 NominationVerdict::Refused(NominationRefusal::UnusableText("evidence"))
517 );
518 }
519
520 #[test]
521 fn session_history_is_reported_before_content() {
522 // A disqualified session must be told it is disqualified, not invited to
523 // reword its way in.
524 let green = vec![check("a", true)];
525 let mut c = ctx(
526 NoChangeKind::PremiseWrong,
527 &green,
528 ContractProvenance::OperatorSupplied,
529 );
530 c.mutated = true;
531 c.summary = "";
532 assert_eq!(
533 evaluate_nomination(c),
534 NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
535 );
536 }
537
538 /// Helper: a real git repo with one committed file.
539 fn git_repo() -> tempfile::TempDir {
540 let dir = tempfile::tempdir().unwrap();
541 let git = |args: &[&str]| {
542 std::process::Command::new("git")
543 .current_dir(dir.path())
544 .args(args)
545 .output()
546 .unwrap();
547 };
548 git(&["init", "-q"]);
549 git(&["config", "user.email", "t@t"]);
550 git(&["config", "user.name", "t"]);
551 std::fs::write(dir.path().join("f.txt"), "original\n").unwrap();
552 git(&["add", "-A"]);
553 git(&["commit", "-qm", "init"]);
554 dir
555 }
556
557 fn git(dir: &std::path::Path, args: &[&str]) {
558 std::process::Command::new("git")
559 .current_dir(dir)
560 .args(args)
561 .output()
562 .unwrap();
563 }
564
565 /// Committing an edit leaves `status` and `diff HEAD` both empty, so an
566 /// observation built only from those two reports a pristine tree over a
567 /// real, persistent change. Binding HEAD is what closes it.
568 ///
569 /// The only other thing standing against this is a line in the system
570 /// prompt asking the model not to commit — and under this feature's own
571 /// threat model, a prompt is not a control.
572 #[test]
573 fn a_committed_edit_is_not_a_pristine_tree() {
574 let dir = git_repo();
575 let start = head_commit(dir.path()).expect("HEAD readable");
576 let before = worktree_fingerprint(dir.path()).unwrap();
577 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(true));
578
579 std::fs::write(dir.path().join("f.txt"), "mutated\n").unwrap();
580 git(dir.path(), &["add", "-A"]);
581 git(dir.path(), &["commit", "-qm", "sneak"]);
582
583 assert_ne!(
584 worktree_fingerprint(dir.path()).unwrap(),
585 before,
586 "a commit must move the fingerprint"
587 );
588 assert_eq!(
589 worktree_is_pristine(dir.path(), &start),
590 Some(false),
591 "HEAD moved, so the tree is not the one this session started on"
592 );
593 }
594
595 /// `git update-index --assume-unchanged` changes neither status nor diff,
596 /// and every later edit to that path is then invisible to both —
597 /// permanently, from metadata living in `.git/`. `ls-files -v` is the only
598 /// one of the four reads that sees it.
599 #[test]
600 fn an_assume_unchanged_flag_is_not_a_pristine_tree() {
601 let dir = git_repo();
602 let start = head_commit(dir.path()).expect("HEAD readable");
603 let before = worktree_fingerprint(dir.path()).unwrap();
604
605 git(dir.path(), &["update-index", "--assume-unchanged", "f.txt"]);
606
607 assert_ne!(
608 worktree_fingerprint(dir.path()).unwrap(),
609 before,
610 "setting the flag must itself register as a mutation"
611 );
612 assert_eq!(
613 worktree_is_pristine(dir.path(), &start),
614 Some(false),
615 "a tree that can hide later edits is not pristine"
616 );
617
618 // And the edit it was hiding stays invisible to status/diff — which is
619 // exactly why the flag itself has to be what disqualifies the session.
620 std::fs::write(dir.path().join("f.txt"), "tampered\n").unwrap();
621 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
622 }
623
624 #[test]
625 fn skip_worktree_is_caught_too() {
626 let dir = git_repo();
627 let start = head_commit(dir.path()).expect("HEAD readable");
628 git(dir.path(), &["update-index", "--skip-worktree", "f.txt"]);
629 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
630 }
631
632 #[test]
633 fn an_ordinary_edit_still_shows() {
634 let dir = git_repo();
635 let start = head_commit(dir.path()).expect("HEAD readable");
636 std::fs::write(dir.path().join("f.txt"), "edited\n").unwrap();
637 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
638 }
639
640 #[test]
641 fn a_head_that_does_not_match_is_never_pristine() {
642 let dir = git_repo();
643 assert_eq!(
644 worktree_is_pristine(dir.path(), "0000000000000000000000000000000000000000"),
645 Some(false)
646 );
647 }
648
649 #[test]
650 fn the_ledger_is_one_way() {
651 let ledger = MutationLedger::new();
652 assert!(!ledger.has_mutated());
653 ledger.record_mutation();
654 ledger.record_mutation();
655 assert!(ledger.has_mutated());
656 }
657
658 #[test]
659 fn trusted_provenance_has_exactly_one_untrusted_member() {
660 assert!(ContractProvenance::OperatorSupplied.is_trusted());
661 assert!(ContractProvenance::HumanConfirmed.is_trusted());
662 assert!(ContractProvenance::RuntimeGenerated.is_trusted());
663 assert!(
664 !ContractProvenance::ModelDerived.is_trusted(),
665 "a contract the session's own model wrote is never trusted — widening \
666 this is how the gate stops working"
667 );
668 }
669}