car-inference 0.56.1

Local model inference for CAR — Candle backend with Qwen3 models
Documentation
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
//! Portfolio assessment — what each installed local model is doing for this
//! machine, and whether keeping it is worth its disk.
//!
//! Pure over its inputs: the engine gathers the facts (usage stamps, outcome
//! profiles, fit, curated upgrades, retire plans) and this module turns them
//! into one verdict per model with the evidence behind it. Nothing here reads
//! a clock, the disk, or the network, so every rule is a unit test.
//!
//! Rules, in priority order — the first that applies decides:
//! 1. **Protected** — a lane default, a configured default, a speech default,
//!    the local tool model. Never retired, whatever else is true.
//! 2. **Never runs here** — the platform cannot run it, or its cold-load peak
//!    exceeds this machine's memory with nothing else running. Judged against
//!    the hardware, never the resource policy: the policy moves, the RAM
//!    doesn't. Both are estimates, so observation outranks them: a model that
//!    has succeeded here, or been used since tracking began, is not this.
//! 3. **Cannot load** — it has failed at least [`PortfolioPolicy::cannot_load_failures`]
//!    times and never once succeeded here. A model that worked and now fails
//!    is not this: re-fetching gigabytes because something else moved is the
//!    churn this module exists to remove.
//! 4. **Superseded** — a curated replacement is installed and runnable here,
//!    and this one has gone unused for [`PortfolioPolicy::superseded_after_secs`].
//! 5. **Idle** — unused for [`PortfolioPolicy::idle_after_secs`].
//! 6. Otherwise **active**, or **unknown** while there is not yet enough
//!    history to tell silence from disuse.
//!
//! Silence is only evidence once it has been observable. Usage stamps start
//! when tracking does, so until a model has been unstamped for a full idle
//! period *after tracking began*, an absent stamp says nothing. The outcome
//! ledger reaches further back but only records this state root's text
//! generation — not a second daemon's, not an in-process FFI consumer's, not
//! a path that writes no receipt — so it can show a text model *was* used,
//! and never shortens the wait.
//!
//! What never stamps is invisible: a CAR consumer older than stamping that
//! loads local models in process reads as silent here.

use serde::Serialize;

/// Thresholds the verdicts are judged against. Quoted in every assessment's
/// evidence, so a reader never needs to know the constants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct PortfolioPolicy {
    pub now: u64,
    /// Since when usage has been observable: the later of when tracking
    /// began and when the current run of a usage-stamping daemon began (a
    /// stopped daemon, or an older one that does not stamp, breaks the run;
    /// sleep does not). `None` means no silence is evidence yet.
    pub tracking_since: Option<u64>,
    pub idle_after_secs: u64,
    pub superseded_after_secs: u64,
    pub cannot_load_failures: u64,
}

impl PortfolioPolicy {
    pub const DEFAULT_IDLE_AFTER_SECS: u64 = 30 * 24 * 60 * 60;
    pub const DEFAULT_SUPERSEDED_AFTER_SECS: u64 = 7 * 24 * 60 * 60;
    pub const DEFAULT_CANNOT_LOAD_FAILURES: u64 = 3;

    pub fn new(now: u64, tracking_since: Option<u64>) -> Self {
        Self {
            now,
            tracking_since,
            idle_after_secs: Self::DEFAULT_IDLE_AFTER_SECS,
            superseded_after_secs: Self::DEFAULT_SUPERSEDED_AFTER_SECS,
            cannot_load_failures: Self::DEFAULT_CANNOT_LOAD_FAILURES,
        }
    }
}

/// What retiring the model would do, from its dry-run plan.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RetireSummary {
    pub freed_bytes: u64,
    /// The plan's digest: pass it as `expect` to execute exactly this plan.
    pub digest: String,
    /// Why the plan cannot execute now (`in_use`, `downloading`, …); empty
    /// when it can.
    pub refusals: Vec<String>,
    /// Rows that would retire with this one (they share its repo).
    pub also_retires: Vec<String>,
}

/// Everything the engine knows about one installed local model.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ModelFacts {
    pub model_id: String,
    pub name: String,
    /// It can generate text, so the outcome ledger covers its use.
    pub generates_text: bool,
    pub platform_compatible: bool,
    /// Its cold-load peak exceeds this machine's memory budget with no
    /// policy applied — it can never fit here.
    pub never_fits: bool,
    pub estimated_peak_mb: Option<u64>,
    pub hardware_budget_mb: u64,
    /// Why it must stay, if it must.
    pub protected: Option<String>,
    /// The user asked to keep it (a `retire:<id>` dismissal in force).
    pub kept: bool,
    /// Last use from the usage stamp (every modality).
    pub last_used: Option<u64>,
    /// Last receipt in the outcome ledger (text generation only).
    pub ledger_last_used: Option<u64>,
    pub success_count: u64,
    /// Failures the model was blamed for since its weights last landed.
    pub fail_count: u64,
    /// An installed, runnable, fitting curated replacement.
    pub superseded_by: Option<String>,
    pub retire: Option<RetireSummary>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
    Protected,
    NeverRunsHere,
    CannotLoad,
    Superseded,
    Idle,
    Active,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortfolioAction {
    Keep,
    Retire,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Assessment {
    pub model_id: String,
    pub name: String,
    pub verdict: Verdict,
    pub action: PortfolioAction,
    /// The facts and thresholds the verdict rests on, in plain language.
    pub evidence: Vec<String>,
    /// The user asked to keep it: never recommended for retirement.
    pub kept: bool,
    /// Most recent use known: the stamp, or a ledger receipt for a text model.
    pub last_used: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub retire: Option<RetireSummary>,
}

fn days(secs: u64) -> u64 {
    secs / (24 * 60 * 60)
}

/// The moment from which this model's silence counts: its last known use,
/// floored at when tracking began — before that, nothing was stamped, so an
/// absent stamp says nothing. `None` (no silence is evidence) until tracking
/// has begun. The ledger can prove a text model was used; it never shortens
/// the silence, because a text path that writes no receipt would make a model
/// used last week look a month idle.
fn silent_since(facts: &ModelFacts, policy: &PortfolioPolicy) -> Option<u64> {
    let since = policy.tracking_since?;
    let ledger = facts.ledger_last_used.filter(|_| facts.generates_text);
    Some(
        facts
            .last_used
            .max(ledger)
            .map_or(since, |last| last.max(since)),
    )
}

/// Assess one model.
pub fn assess(facts: &ModelFacts, policy: &PortfolioPolicy) -> Assessment {
    let last_used = facts
        .last_used
        .max(facts.ledger_last_used.filter(|_| facts.generates_text));
    let mut evidence = Vec::new();
    if let Some(last) = last_used {
        evidence.push(format!(
            "last used {} days ago",
            days(policy.now.saturating_sub(last))
        ));
    }
    let retire_freed = facts.retire.as_ref().map_or(0, |r| r.freed_bytes);
    let verdict = 'verdict: {
        if let Some(why) = &facts.protected {
            evidence.push(format!("protected: {why}"));
            break 'verdict Verdict::Protected;
        }
        // It ran here: the estimate is what's wrong.
        let observed_running = facts.success_count > 0
            || facts
                .last_used
                .zip(policy.tracking_since)
                .is_some_and(|(used, since)| used >= since);
        if observed_running && (!facts.platform_compatible || facts.never_fits) {
            evidence.push("estimated not to run here, but it has".into());
        } else if !facts.platform_compatible {
            evidence.push("this machine's platform cannot run it".into());
            break 'verdict Verdict::NeverRunsHere;
        } else if facts.never_fits {
            evidence.push(match facts.estimated_peak_mb {
                Some(peak) => format!(
                    "needs ~{peak} MB to load; this machine has {} MB for models",
                    facts.hardware_budget_mb
                ),
                None => "too large for this machine's memory".into(),
            });
            break 'verdict Verdict::NeverRunsHere;
        }
        if facts.success_count == 0 && facts.fail_count >= policy.cannot_load_failures {
            evidence.push(format!(
                "failed {} times since it was installed and never succeeded here (threshold {})",
                facts.fail_count, policy.cannot_load_failures
            ));
            break 'verdict Verdict::CannotLoad;
        }
        let silent = silent_since(facts, policy).map(|since| policy.now.saturating_sub(since));
        if let (Some(by), Some(silent)) = (&facts.superseded_by, silent) {
            if silent >= policy.superseded_after_secs {
                evidence.push(format!(
                    "replaced by {by}; unused {} days (threshold {})",
                    days(silent),
                    days(policy.superseded_after_secs)
                ));
                break 'verdict Verdict::Superseded;
            }
        }
        match silent {
            Some(silent) if silent >= policy.idle_after_secs => {
                evidence.push(format!(
                    "unused {} days (threshold {})",
                    days(silent),
                    days(policy.idle_after_secs)
                ));
                Verdict::Idle
            }
            Some(_) if last_used.is_some() => Verdict::Active,
            _ => {
                evidence.push(match policy.tracking_since {
                    Some(since) => format!(
                        "usage tracked for {} days; {} needed before silence counts",
                        days(policy.now.saturating_sub(since)),
                        days(policy.idle_after_secs)
                    ),
                    None => "usage is not tracked yet".into(),
                });
                Verdict::Unknown
            }
        }
    };
    let retirable = matches!(
        verdict,
        Verdict::NeverRunsHere | Verdict::CannotLoad | Verdict::Superseded | Verdict::Idle
    );
    // A retirement that frees nothing, or whose plan is refused for a reason
    // waiting will not change, is not an action worth taking. `in_use` and
    // `downloading` pass with time; the executor re-plans before acting.
    let permanently_refused = facts.retire.as_ref().is_some_and(|r| {
        r.refusals
            .iter()
            .any(|reason| !matches!(reason.as_str(), "in_use" | "downloading"))
    });
    if retirable && permanently_refused {
        evidence.push(format!(
            "retirement refused: {}",
            facts
                .retire
                .as_ref()
                .map(|r| r.refusals.join(", "))
                .unwrap_or_default()
        ));
    }
    if retirable && facts.kept {
        evidence.push("you asked to keep it".into());
    }
    let action = if retirable && retire_freed > 0 && !permanently_refused && !facts.kept {
        PortfolioAction::Retire
    } else {
        if retirable && retire_freed == 0 && facts.retire.is_some() && !facts.kept {
            evidence.push("retiring it would free nothing CAR owns".into());
        }
        PortfolioAction::Keep
    };
    Assessment {
        model_id: facts.model_id.clone(),
        name: facts.name.clone(),
        verdict,
        action,
        evidence,
        kept: facts.kept,
        last_used,
        retire: facts.retire.clone(),
    }
}

/// The whole portfolio: one assessment per model, and what retiring every
/// recommended one would free — each shared repo counted once.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Portfolio {
    pub policy: PortfolioPolicy,
    pub models: Vec<Assessment>,
    /// What retiring every model recommended for retirement would free.
    pub reclaimable_bytes: u64,
    /// Hub repos no registry row references. `car` ones are CAR's (it
    /// recorded fetching them); `user` ones are reported and never touched.
    pub orphans: Vec<crate::retire::HubOrphan>,
    /// What the concierge did on its own in the last 30 days, newest first,
    /// with how to undo each — so a deletion nobody watched is never silent.
    pub recent_actions: Vec<RecentAction>,
    /// Agents still pinning a model the signed catalog revoked. Nothing
    /// re-points an agent on its own, so this stays until the agent is
    /// changed. A revoked signed row has left the registry, so routing to it
    /// fails meanwhile; a revoked builtin still runs, deprecated.
    pub revoked_pins: Vec<RevokedPin>,
    /// Directories named for revoked models that still hold real files. Never
    /// counted in `reclaimable_bytes` and never removed by maintenance: a
    /// revocation does not order real bytes deleted.
    pub revoked_copies: Vec<crate::retire::RevokedCopy>,
}

/// A model declarative agents pin that the signed catalog revoked.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RevokedPin {
    /// What the agents name (`inference.model`): an id or a model name.
    pub pin: String,
    /// The revoked model it resolves to.
    pub model_id: String,
    /// The agents that pin it, sorted.
    pub agents: Vec<String>,
}

/// One thing the concierge did on its own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RecentAction {
    pub at: u64,
    /// `retire`, `discard_partials`, or `resource_policy`.
    pub kind: String,
    /// The model id, `repo:<owner/name>`, or empty.
    pub subject: String,
    pub detail: String,
    /// The command that undoes it, where one exists — display text; act on
    /// the structured fields below, never on this.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub undo: Option<String>,
    /// For an upgrade that switched a lane: the lane (`assistant`, …), what
    /// `concierge.rollback` takes.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub use_case: Option<String>,
    /// What it replaced: the previous model of an upgrade, or the previous
    /// memory profile of a policy change (what `models.resource_policy.set`
    /// takes to undo it).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prior: Option<String>,
    /// A retirement that began and has no completion record: cut short,
    /// finished at the next daemon start.
    pub interrupted: bool,
}

/// Assess every model. Twins that retire together report the same plan;
/// only the first of a group is recommended, so the total and the action
/// list each count a shared repo once.
pub fn assess_all(facts: &[ModelFacts], policy: &PortfolioPolicy) -> Portfolio {
    let mut models: Vec<Assessment> = facts.iter().map(|f| assess(f, policy)).collect();
    models.sort_by(|a, b| a.model_id.cmp(&b.model_id));
    let mut covered = std::collections::BTreeSet::new();
    let mut reclaimable_bytes = 0;
    for assessment in &mut models {
        if covered.contains(&assessment.model_id) {
            if assessment.action == PortfolioAction::Retire {
                assessment.action = PortfolioAction::Keep;
                assessment
                    .evidence
                    .push("retires with the row that shares its files".into());
            }
            continue;
        }
        if assessment.action != PortfolioAction::Retire {
            continue;
        }
        let Some(retire) = &assessment.retire else {
            continue;
        };
        // A twin that must stay keeps the shared repo: retiring this row
        // would take the twin with it.
        let twin_kept = retire.also_retires.iter().any(|twin| {
            facts
                .iter()
                .find(|f| &f.model_id == twin)
                .is_some_and(|f| !retirable_alone(f, policy))
        });
        if twin_kept {
            assessment.action = PortfolioAction::Keep;
            assessment
                .evidence
                .push("a row sharing its files is still needed".into());
            continue;
        }
        covered.extend(retire.also_retires.iter().cloned());
        reclaimable_bytes += retire.freed_bytes;
    }
    Portfolio {
        policy: *policy,
        models,
        reclaimable_bytes,
        orphans: Vec::new(),
        recent_actions: Vec::new(),
        revoked_pins: Vec::new(),
        revoked_copies: Vec::new(),
    }
}

fn retirable_alone(facts: &ModelFacts, policy: &PortfolioPolicy) -> bool {
    matches!(
        assess(facts, policy).verdict,
        Verdict::NeverRunsHere | Verdict::CannotLoad | Verdict::Superseded | Verdict::Idle
    )
}

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

    const DAY: u64 = 24 * 60 * 60;
    const NOW: u64 = 1_000 * DAY;

    fn policy(tracking_days: u64) -> PortfolioPolicy {
        PortfolioPolicy::new(NOW, Some(NOW - tracking_days * DAY))
    }

    fn installed(id: &str) -> ModelFacts {
        ModelFacts {
            model_id: id.into(),
            name: id.into(),
            platform_compatible: true,
            hardware_budget_mb: 60_000,
            retire: Some(RetireSummary {
                freed_bytes: 1_000,
                digest: "d".into(),
                refusals: Vec::new(),
                also_retires: Vec::new(),
            }),
            ..ModelFacts::default()
        }
    }

    #[test]
    fn silence_is_not_evidence_until_tracking_has_seen_a_full_period() {
        let facts = installed("mlx/a");
        let fresh = assess(&facts, &policy(2));
        assert_eq!(fresh.verdict, Verdict::Unknown, "{fresh:?}");
        assert_eq!(fresh.action, PortfolioAction::Keep);
        let untracked = assess(&facts, &PortfolioPolicy::new(NOW, None));
        assert_eq!(untracked.verdict, Verdict::Unknown);
        // Positive control: the same facts, tracked past the threshold.
        let aged = assess(&facts, &policy(31));
        assert_eq!(aged.verdict, Verdict::Idle, "{aged:?}");
        assert_eq!(aged.action, PortfolioAction::Retire);
    }

    #[test]
    fn an_old_stamp_counts_from_when_tracking_began() {
        let mut facts = installed("mlx/a");
        facts.last_used = Some(NOW - 100 * DAY);
        // Tracking began 5 days ago: the stamp predates it, so 5 days of
        // silence is all that is known.
        assert_eq!(assess(&facts, &policy(5)).verdict, Verdict::Active);
        facts.last_used = Some(NOW - 2 * DAY);
        assert_eq!(assess(&facts, &policy(100)).verdict, Verdict::Active);
    }

    #[test]
    fn the_ledger_proves_text_use_but_never_shortens_the_wait() {
        let mut facts = installed("mlx/a");
        facts.ledger_last_used = Some(NOW - 45 * DAY);
        facts.generates_text = true;
        let early = assess(&facts, &policy(8));
        assert_eq!(early.verdict, Verdict::Active, "{early:?}");
        let text = assess(&facts, &policy(31));
        assert_eq!(text.verdict, Verdict::Idle, "{text:?}");
        assert!(text.evidence.iter().any(|e| e.contains("threshold 30")));
        // A recent receipt keeps it active even when nothing stamped.
        facts.ledger_last_used = Some(NOW - DAY);
        assert_eq!(assess(&facts, &policy(60)).verdict, Verdict::Active);
        // A speech model's generate receipt is not its use.
        facts.generates_text = false;
        facts.ledger_last_used = Some(NOW - 45 * DAY);
        assert_eq!(assess(&facts, &policy(8)).verdict, Verdict::Unknown);
    }

    #[test]
    fn a_permanently_refused_plan_is_not_an_action() {
        let mut facts = installed("mlx/a");
        facts.retire.as_mut().unwrap().refusals = vec!["unsafe_path".into()];
        let refused = assess(&facts, &policy(60));
        assert_eq!(refused.verdict, Verdict::Idle);
        assert_eq!(refused.action, PortfolioAction::Keep, "{refused:?}");
        // Busy right now passes with time.
        facts.retire.as_mut().unwrap().refusals = vec!["in_use".into()];
        assert_eq!(assess(&facts, &policy(60)).action, PortfolioAction::Retire);
    }

    #[test]
    fn running_here_outranks_the_estimate_that_it_cannot() {
        let mut facts = installed("mlx/moe");
        facts.never_fits = true;
        facts.success_count = 4;
        facts.last_used = Some(NOW - DAY);
        let assessment = assess(&facts, &policy(60));
        assert_eq!(assessment.verdict, Verdict::Active, "{assessment:?}");
        assert_eq!(assessment.action, PortfolioAction::Keep);
        // A stamp since tracking began is the same evidence.
        let mut stamped = installed("mlx/vlm");
        stamped.platform_compatible = false;
        stamped.last_used = Some(NOW - 2 * DAY);
        assert_eq!(assess(&stamped, &policy(10)).verdict, Verdict::Active);
        // A stamp from before tracking is not.
        stamped.last_used = Some(NOW - 20 * DAY);
        assert_eq!(
            assess(&stamped, &policy(10)).verdict,
            Verdict::NeverRunsHere
        );
    }

    #[test]
    fn a_protected_model_stays_whatever_else_is_true() {
        let mut facts = installed("mlx/a");
        facts.protected = Some("lane default for assistant".into());
        facts.platform_compatible = false;
        facts.never_fits = true;
        facts.fail_count = 50;
        facts.superseded_by = Some("mlx/b".into());
        let assessment = assess(&facts, &policy(365));
        assert_eq!(assessment.verdict, Verdict::Protected);
        assert_eq!(assessment.action, PortfolioAction::Keep);
    }

    #[test]
    fn never_runs_here_and_cannot_load_need_no_history() {
        let mut facts = installed("qwen/gguf");
        facts.platform_compatible = false;
        assert_eq!(assess(&facts, &policy(0)).verdict, Verdict::NeverRunsHere);
        let mut big = installed("mlx/huge");
        big.never_fits = true;
        big.estimated_peak_mb = Some(120_000);
        let verdict = assess(&big, &policy(0));
        assert_eq!(verdict.verdict, Verdict::NeverRunsHere);
        assert!(verdict.evidence[0].contains("120000"));
        let mut broken = installed("mlx/broken");
        broken.fail_count = 3;
        assert_eq!(assess(&broken, &policy(0)).verdict, Verdict::CannotLoad);
        // It worked once: a regression is not "cannot load".
        broken.success_count = 1;
        broken.last_used = Some(NOW);
        assert_eq!(assess(&broken, &policy(0)).verdict, Verdict::Active);
    }

    #[test]
    fn a_replaced_model_goes_after_a_week_not_a_month() {
        let mut facts = installed("mlx/old");
        facts.superseded_by = Some("mlx/new".into());
        facts.last_used = Some(NOW - 10 * DAY);
        let assessment = assess(&facts, &policy(60));
        assert_eq!(assessment.verdict, Verdict::Superseded, "{assessment:?}");
        facts.last_used = Some(NOW - 2 * DAY);
        assert_eq!(assess(&facts, &policy(60)).verdict, Verdict::Active);
    }

    #[test]
    fn a_kept_model_is_never_recommended_for_retirement() {
        let mut facts = installed("mlx/a");
        facts.kept = true;
        let assessment = assess(&facts, &policy(60));
        assert_eq!(
            assessment.verdict,
            Verdict::Idle,
            "the verdict stays honest"
        );
        assert_eq!(assessment.action, PortfolioAction::Keep);
        assert!(assessment.kept);
        let portfolio = assess_all(&[facts], &policy(60));
        assert_eq!(portfolio.reclaimable_bytes, 0);
    }

    #[test]
    fn nothing_to_free_is_not_an_action() {
        let mut facts = installed("mlx/a");
        facts.retire.as_mut().unwrap().freed_bytes = 0;
        let assessment = assess(&facts, &policy(60));
        assert_eq!(assessment.verdict, Verdict::Idle);
        assert_eq!(assessment.action, PortfolioAction::Keep);
    }

    #[test]
    fn twins_are_recommended_once_and_kept_when_either_is_needed() {
        let mut a = installed("mlx/gemma");
        let mut b = installed("vllm-mlx/gemma");
        a.retire.as_mut().unwrap().also_retires = vec![b.model_id.clone()];
        b.retire.as_mut().unwrap().also_retires = vec![a.model_id.clone()];
        let portfolio = assess_all(&[a.clone(), b.clone()], &policy(60));
        let retiring: Vec<_> = portfolio
            .models
            .iter()
            .filter(|m| m.action == PortfolioAction::Retire)
            .collect();
        assert_eq!(retiring.len(), 1, "{portfolio:?}");
        assert_eq!(
            portfolio.reclaimable_bytes, 1_000,
            "shared repo counted once"
        );

        b.last_used = Some(NOW);
        let portfolio = assess_all(&[a, b], &policy(60));
        assert!(
            portfolio
                .models
                .iter()
                .all(|m| m.action == PortfolioAction::Keep),
            "{portfolio:?}"
        );
        assert_eq!(portfolio.reclaimable_bytes, 0);
    }
}