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 name: name.to_string(),
335 passed,
336 exit_code: Some(if passed { 0 } else { 1 }),
337 duration_ms: 1,
338 output_tail: String::new(),
339 timed_out: false,
340 deadline_clamped: false,
341 }
342 }
343
344 fn ctx<'a>(
345 kind: NoChangeKind,
346 baseline: &'a [CheckResult],
347 provenance: ContractProvenance,
348 ) -> NominationContext<'a> {
349 NominationContext {
350 kind,
351 summary: "the code already handles this",
352 evidence: "read handler.rs and ran the suite",
353 baseline,
354 provenance,
355 worktree_clean: true,
356 mutated: false,
357 }
358 }
359
360 #[test]
361 fn the_narrow_path_terminates_autonomously() {
362 let green = vec![check("a", true), check("b", true)];
363 assert_eq!(
364 evaluate_nomination(ctx(
365 NoChangeKind::PremiseWrong,
366 &green,
367 ContractProvenance::OperatorSupplied
368 )),
369 NominationVerdict::Autonomous
370 );
371 }
372
373 #[test]
374 fn a_model_authored_contract_never_self_approves() {
375 // The whole escape hatch, in one assertion: same green baseline, same
376 // kind, and the only difference is who wrote the checks.
377 let green = vec![check("a", true)];
378 assert_eq!(
379 evaluate_nomination(ctx(
380 NoChangeKind::PremiseWrong,
381 &green,
382 ContractProvenance::ModelDerived
383 )),
384 NominationVerdict::NeedsHuman
385 );
386 }
387
388 #[test]
389 fn a_runtime_generated_reproduction_is_trusted() {
390 // This is what lets autonomous self-triage work at all: the contract
391 // came from telemetry CAR collected before the session existed.
392 let green = vec![check("repro", true)];
393 assert_eq!(
394 evaluate_nomination(ctx(
395 NoChangeKind::PremiseWrong,
396 &green,
397 ContractProvenance::RuntimeGenerated
398 )),
399 NominationVerdict::Autonomous
400 );
401 }
402
403 #[test]
404 fn the_two_judgement_shapes_always_reach_a_human() {
405 let green = vec![check("a", true)];
406 for kind in [
407 NoChangeKind::DeliberateBehavior,
408 NoChangeKind::NonCodeDecision,
409 ] {
410 assert_eq!(
411 evaluate_nomination(ctx(kind, &green, ContractProvenance::OperatorSupplied)),
412 NominationVerdict::NeedsHuman,
413 "{kind:?} is not runtime-verifiable"
414 );
415 }
416 }
417
418 #[test]
419 fn a_red_baseline_proves_nothing_and_parks() {
420 let mixed = vec![check("a", true), check("b", false)];
421 assert_eq!(
422 evaluate_nomination(ctx(
423 NoChangeKind::PremiseWrong,
424 &mixed,
425 ContractProvenance::OperatorSupplied
426 )),
427 NominationVerdict::NeedsHuman
428 );
429 }
430
431 #[test]
432 fn editing_then_reverting_does_not_restore_eligibility() {
433 let green = vec![check("a", true)];
434 let mut c = ctx(
435 NoChangeKind::PremiseWrong,
436 &green,
437 ContractProvenance::OperatorSupplied,
438 );
439 // Worktree is clean again — the revert worked. The ledger still says no.
440 c.worktree_clean = true;
441 c.mutated = true;
442 assert_eq!(
443 evaluate_nomination(c),
444 NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
445 );
446 }
447
448 #[test]
449 fn a_dirty_worktree_is_refused() {
450 let green = vec![check("a", true)];
451 let mut c = ctx(
452 NoChangeKind::PremiseWrong,
453 &green,
454 ContractProvenance::OperatorSupplied,
455 );
456 c.worktree_clean = false;
457 assert_eq!(
458 evaluate_nomination(c),
459 NominationVerdict::Refused(NominationRefusal::WorktreeNotClean)
460 );
461 }
462
463 #[test]
464 fn a_starved_baseline_is_not_evidence() {
465 let mut killed = check("a", false);
466 killed.timed_out = true;
467 killed.deadline_clamped = true;
468 let results = vec![killed];
469 assert_eq!(
470 evaluate_nomination(ctx(
471 NoChangeKind::PremiseWrong,
472 &results,
473 ContractProvenance::OperatorSupplied
474 )),
475 NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
476 );
477 }
478
479 #[test]
480 fn an_empty_baseline_is_not_a_clean_one() {
481 assert!(!baseline_completed(&[]));
482 assert_eq!(
483 evaluate_nomination(ctx(
484 NoChangeKind::PremiseWrong,
485 &[],
486 ContractProvenance::OperatorSupplied
487 )),
488 NominationVerdict::Refused(NominationRefusal::BaselineIncomplete)
489 );
490 }
491
492 #[test]
493 fn empty_or_oversized_text_is_refused() {
494 let green = vec![check("a", true)];
495 let mut c = ctx(
496 NoChangeKind::PremiseWrong,
497 &green,
498 ContractProvenance::OperatorSupplied,
499 );
500 c.summary = " ";
501 assert_eq!(
502 evaluate_nomination(c),
503 NominationVerdict::Refused(NominationRefusal::UnusableText("summary"))
504 );
505
506 let huge = "x".repeat(MAX_FINDING_TEXT + 1);
507 let mut c2 = ctx(
508 NoChangeKind::PremiseWrong,
509 &green,
510 ContractProvenance::OperatorSupplied,
511 );
512 c2.evidence = &huge;
513 assert_eq!(
514 evaluate_nomination(c2),
515 NominationVerdict::Refused(NominationRefusal::UnusableText("evidence"))
516 );
517 }
518
519 #[test]
520 fn session_history_is_reported_before_content() {
521 // A disqualified session must be told it is disqualified, not invited to
522 // reword its way in.
523 let green = vec![check("a", true)];
524 let mut c = ctx(
525 NoChangeKind::PremiseWrong,
526 &green,
527 ContractProvenance::OperatorSupplied,
528 );
529 c.mutated = true;
530 c.summary = "";
531 assert_eq!(
532 evaluate_nomination(c),
533 NominationVerdict::Refused(NominationRefusal::WorktreeWasMutated)
534 );
535 }
536
537 /// Helper: a real git repo with one committed file.
538 fn git_repo() -> tempfile::TempDir {
539 let dir = tempfile::tempdir().unwrap();
540 let git = |args: &[&str]| {
541 std::process::Command::new("git")
542 .current_dir(dir.path())
543 .args(args)
544 .output()
545 .unwrap();
546 };
547 git(&["init", "-q"]);
548 git(&["config", "user.email", "t@t"]);
549 git(&["config", "user.name", "t"]);
550 std::fs::write(dir.path().join("f.txt"), "original\n").unwrap();
551 git(&["add", "-A"]);
552 git(&["commit", "-qm", "init"]);
553 dir
554 }
555
556 fn git(dir: &std::path::Path, args: &[&str]) {
557 std::process::Command::new("git")
558 .current_dir(dir)
559 .args(args)
560 .output()
561 .unwrap();
562 }
563
564 /// Committing an edit leaves `status` and `diff HEAD` both empty, so an
565 /// observation built only from those two reports a pristine tree over a
566 /// real, persistent change. Binding HEAD is what closes it.
567 ///
568 /// The only other thing standing against this is a line in the system
569 /// prompt asking the model not to commit — and under this feature's own
570 /// threat model, a prompt is not a control.
571 #[test]
572 fn a_committed_edit_is_not_a_pristine_tree() {
573 let dir = git_repo();
574 let start = head_commit(dir.path()).expect("HEAD readable");
575 let before = worktree_fingerprint(dir.path()).unwrap();
576 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(true));
577
578 std::fs::write(dir.path().join("f.txt"), "mutated\n").unwrap();
579 git(dir.path(), &["add", "-A"]);
580 git(dir.path(), &["commit", "-qm", "sneak"]);
581
582 assert_ne!(
583 worktree_fingerprint(dir.path()).unwrap(),
584 before,
585 "a commit must move the fingerprint"
586 );
587 assert_eq!(
588 worktree_is_pristine(dir.path(), &start),
589 Some(false),
590 "HEAD moved, so the tree is not the one this session started on"
591 );
592 }
593
594 /// `git update-index --assume-unchanged` changes neither status nor diff,
595 /// and every later edit to that path is then invisible to both —
596 /// permanently, from metadata living in `.git/`. `ls-files -v` is the only
597 /// one of the four reads that sees it.
598 #[test]
599 fn an_assume_unchanged_flag_is_not_a_pristine_tree() {
600 let dir = git_repo();
601 let start = head_commit(dir.path()).expect("HEAD readable");
602 let before = worktree_fingerprint(dir.path()).unwrap();
603
604 git(dir.path(), &["update-index", "--assume-unchanged", "f.txt"]);
605
606 assert_ne!(
607 worktree_fingerprint(dir.path()).unwrap(),
608 before,
609 "setting the flag must itself register as a mutation"
610 );
611 assert_eq!(
612 worktree_is_pristine(dir.path(), &start),
613 Some(false),
614 "a tree that can hide later edits is not pristine"
615 );
616
617 // And the edit it was hiding stays invisible to status/diff — which is
618 // exactly why the flag itself has to be what disqualifies the session.
619 std::fs::write(dir.path().join("f.txt"), "tampered\n").unwrap();
620 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
621 }
622
623 #[test]
624 fn skip_worktree_is_caught_too() {
625 let dir = git_repo();
626 let start = head_commit(dir.path()).expect("HEAD readable");
627 git(dir.path(), &["update-index", "--skip-worktree", "f.txt"]);
628 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
629 }
630
631 #[test]
632 fn an_ordinary_edit_still_shows() {
633 let dir = git_repo();
634 let start = head_commit(dir.path()).expect("HEAD readable");
635 std::fs::write(dir.path().join("f.txt"), "edited\n").unwrap();
636 assert_eq!(worktree_is_pristine(dir.path(), &start), Some(false));
637 }
638
639 #[test]
640 fn a_head_that_does_not_match_is_never_pristine() {
641 let dir = git_repo();
642 assert_eq!(
643 worktree_is_pristine(dir.path(), "0000000000000000000000000000000000000000"),
644 Some(false)
645 );
646 }
647
648 #[test]
649 fn the_ledger_is_one_way() {
650 let ledger = MutationLedger::new();
651 assert!(!ledger.has_mutated());
652 ledger.record_mutation();
653 ledger.record_mutation();
654 assert!(ledger.has_mutated());
655 }
656
657 #[test]
658 fn trusted_provenance_has_exactly_one_untrusted_member() {
659 assert!(ContractProvenance::OperatorSupplied.is_trusted());
660 assert!(ContractProvenance::HumanConfirmed.is_trusted());
661 assert!(ContractProvenance::RuntimeGenerated.is_trusted());
662 assert!(
663 !ContractProvenance::ModelDerived.is_trusted(),
664 "a contract the session's own model wrote is never trusted — widening \
665 this is how the gate stops working"
666 );
667 }
668}