kranz_engine/routing.rs
1//! The backend routing table (ticket `backend-routing-abstraction`, KRZ-331):
2//! deterministic resolution of a ticket's task class to an executor
3//! CAPABILITY CLASS ([`ExecutorTier`]) under the operator-configured table
4//! ([`RoutingConfig`]).
5//!
6//! This module is the floor of the routing abstraction: local endpoints,
7//! hosted frontier models, and hosted fine-tunes are peers behind one
8//! interface because the table routes to capability classes only — NEVER to
9//! model ids (docs/reviews/local-llm-and-triumvirate.md §1; the hardcoded-id
10//! trap is "dead on arrival on anyone else's machine"). Which concrete
11//! endpoint a `local` route resolves to is ordinary local-backend role
12//! config (`baseUrl` + `model` + `contextBudget`): a hosted fine-tune behind
13//! an OpenAI-compatible endpoint is exactly that config shape, not a new
14//! backend kind and not routing-table content. The grep-style test below
15//! pins the no-model-id rule on this file's logic.
16//!
17//! Determinism is the contract the rest of the engine relies on: the SAME
18//! (table, task class) inputs ALWAYS produce the same route — the first
19//! matching exact `taskClassRules` entry wins; only when none matches are the
20//! ordered `patternRules` consulted (first match wins); no match anywhere
21//! falls through to [`ExecutorTier::Frontier`] (the safe default, identical
22//! to the hardcoded floor's fallthrough). An EMPTY table is not resolved
23//! here at all: [`crate::config::route_task_class_executor`] keeps the
24//! legacy literal floor for it byte-for-byte, so configuring no table is a
25//! perfect regression of today's behavior. Anything LLM-judged is
26//! deliberately above this floor (the worker self-escalation layered on
27//! top), never inside it. The tracked, base-branch-owned rules FILE that
28//! populates the table lives in [`crate::routing_rules`] (ticket
29//! `routing-rules-config`); the seed-time route record derived here
30//! ([`seed_executor_route`]) rides `worker.spawned`.
31
32use crate::types::{ExecutorRoute, ExecutorTier, RoutingConfig};
33
34/// Normalize a task-class string for comparison: trimmed and ASCII-lowercased
35/// — the SAME normalization the hardcoded floor
36/// ([`crate::config::task_class_to_tier`]) applies, so a table rule matches
37/// exactly the strings the literal floor would have.
38pub fn normalize_task_class(task_class: &str) -> String {
39 task_class.trim().to_ascii_lowercase()
40}
41
42/// Which table entry decided a route — the provenance half of the decision,
43/// recorded per worker session so the effective route is never a hidden
44/// implementation detail (ticket `routing-rules-config`).
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum RuleMatch {
47 /// `taskClassRules[i]` matched the task class exactly (after
48 /// normalization).
49 TaskClass(usize),
50 /// `patternRules[i]` matched — consulted only when no exact rule did.
51 Pattern(usize),
52 /// No rule matched; the fall-through to [`ExecutorTier::Frontier`]
53 /// decided.
54 FallThrough,
55}
56
57impl RuleMatch {
58 /// The recorded form: the table key path + index, exactly the shape
59 /// [`validate_table`] errors name, so a session's route can be traced to
60 /// the same rule a validation failure would name.
61 pub fn describe(&self) -> String {
62 match self {
63 RuleMatch::TaskClass(i) => format!("taskClassRules[{i}]"),
64 RuleMatch::Pattern(i) => format!("patternRules[{i}]"),
65 RuleMatch::FallThrough => "fall-through".to_string(),
66 }
67 }
68}
69
70/// The full resolution of one task class against the table: the tier AND the
71/// rule that decided it. [`table_tier`] is the tier-only view for callers
72/// that predate provenance.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct RouteDecision {
75 pub tier: ExecutorTier,
76 pub rule: RuleMatch,
77}
78
79/// Resolve `task_class` against the routing table: the FIRST matching exact
80/// rule wins; with no exact match the FIRST matching pattern rule wins; no
81/// match anywhere (or no task class at all) falls through to
82/// [`ExecutorTier::Frontier`].
83///
84/// Pure and total: no I/O, no time, no randomness — repeated calls with the
85/// same inputs return the same decision, which is what makes the floor route
86/// auditable (`mission.created` records the routed config, and each
87/// `worker.spawned` can carry the recomputed decision). Callers handle the
88/// empty table separately (the legacy literal floor), so this function
89/// treats every rule present as intentional; [`validate_table`] has already
90/// failed closed on a malformed one.
91pub fn resolve(routing: &RoutingConfig, task_class: Option<&str>) -> RouteDecision {
92 let Some(normalized) = task_class.map(normalize_task_class) else {
93 return RouteDecision {
94 tier: ExecutorTier::Frontier,
95 rule: RuleMatch::FallThrough,
96 };
97 };
98 for (i, rule) in routing.task_class_rules.iter().enumerate() {
99 if normalize_task_class(&rule.task_class) == normalized {
100 return RouteDecision {
101 tier: rule.tier,
102 rule: RuleMatch::TaskClass(i),
103 };
104 }
105 }
106 for (i, rule) in routing.pattern_rules.iter().enumerate() {
107 if pattern_matches(&normalize_task_class(&rule.pattern), &normalized) {
108 return RouteDecision {
109 tier: rule.tier,
110 rule: RuleMatch::Pattern(i),
111 };
112 }
113 }
114 RouteDecision {
115 tier: ExecutorTier::Frontier,
116 rule: RuleMatch::FallThrough,
117 }
118}
119
120/// Resolve `task_class` against the routing table: the FIRST matching rule
121/// wins; no rule matching (or no task class at all) falls through to
122/// [`ExecutorTier::Frontier`]. Tier-only view of [`resolve`].
123pub fn table_tier(routing: &RoutingConfig, task_class: Option<&str>) -> ExecutorTier {
124 resolve(routing, task_class).tier
125}
126
127/// The pattern language for [`crate::types::PatternRoute`]: `*` matches any
128/// (possibly empty) run of characters, every other character is a literal
129/// byte. Both sides are already normalized (trim + lowercase) by the caller.
130/// Deliberately NOT regex: the rules file is an operator contract, and a
131/// two-wildcard-semantics glob keeps every rule auditable at a glance while
132/// the classic two-pointer backtracking scan stays total and deterministic.
133fn pattern_matches(pattern: &str, text: &str) -> bool {
134 let (p, t) = (pattern.as_bytes(), text.as_bytes());
135 let (mut pi, mut ti) = (0usize, 0usize);
136 // Last `*` position and the text index it had consumed up to — the
137 // backtrack point when a post-`*` literal fails to match.
138 let (mut star, mut star_ti) = (None, 0usize);
139 while ti < t.len() {
140 if pi < p.len() && p[pi] == t[ti] {
141 pi += 1;
142 ti += 1;
143 } else if pi < p.len() && p[pi] == b'*' {
144 star = Some(pi);
145 star_ti = ti;
146 pi += 1;
147 } else if let Some(sp) = star {
148 pi = sp + 1;
149 star_ti += 1;
150 ti = star_ti;
151 } else {
152 return false;
153 }
154 }
155 while pi < p.len() && p[pi] == b'*' {
156 pi += 1;
157 }
158 pi == p.len()
159}
160
161/// The seed-time route record (ticket `routing-rules-config`): the EFFECTIVE
162/// tier and the rule that decided it, folded once from `mission.created`
163/// (whose `goal` still carries the folded task class — `plan.approved`
164/// overwrites `state.mission.goal` with the plan's own goal, so the class
165/// exists ONLY on this event) onto [`crate::types::Mission`], then replayed
166/// onto every `worker.spawned`. Routing is provenance, not a hidden
167/// implementation detail — and because resolution is deterministic, the
168/// fold-time recomputation from the recorded (goal, config) equals the
169/// seed-time decision [`crate::config::route_task_class_executor`] made, so
170/// nothing new has to be persisted and a resumed engine records exactly what
171/// a fresh one would.
172///
173/// The recorded tier is the SEEDED effective tier (derived from the routed
174/// config exactly as [`crate::types::MissionState::executor_tier`] derives
175/// it): a later mid-mission `config.changed` backend flip moves the live
176/// tier, not this seed-time record — the flip is its own event.
177///
178/// `None` when the seed goal carried no task class at all: routing never
179/// decided anything for such a mission, and its `worker.spawned` payload
180/// stays byte-identical to the pre-provenance shape.
181pub fn seed_executor_route(
182 cfg: &crate::types::MissionConfig,
183 mission_goal: &str,
184) -> Option<ExecutorRoute> {
185 let task_class = crate::ticket::parse_task_class_from_goal(mission_goal)?;
186 let rule = if cfg.routing.is_empty() {
187 // The legacy literal floor decided; there is no table rule to name.
188 None
189 } else {
190 match resolve(&cfg.routing, Some(&task_class)).rule {
191 RuleMatch::FallThrough => None,
192 matched => Some(matched.describe()),
193 }
194 };
195 Some(ExecutorRoute {
196 tier: cfg.executor_tier(),
197 rule,
198 })
199}
200
201/// Fail-closed validation of the routing table, called by
202/// [`crate::config::validate`] so a malformed table never reaches a mission.
203/// Returns `Err` naming the offending rule (the config-file key path), never
204/// a silently-corrected table:
205///
206/// - a blank `taskClass` could never match honestly (routing matches on
207/// task-class text), and
208/// - a duplicate class after normalization is dead config under
209/// first-match-wins — almost always a mistake the operator meant to
210/// reorder or merge, so it is refused rather than quietly shadowed.
211///
212/// The same two checks apply to `patternRules` (ticket
213/// `routing-rules-config`): a blank pattern is meaningless (spell `*`), and
214/// a duplicate pattern is dead config under first-match-wins.
215pub fn validate_table(routing: &RoutingConfig) -> std::result::Result<(), String> {
216 let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
217 for (i, rule) in routing.task_class_rules.iter().enumerate() {
218 let normalized = normalize_task_class(&rule.task_class);
219 if normalized.is_empty() {
220 return Err(format!(
221 "routing.taskClassRules[{i}].taskClass must be non-empty: routing matches on \
222 task-class text, so a blank rule could never match honestly"
223 ));
224 }
225 if let Some(first) = seen.insert(normalized, i) {
226 return Err(format!(
227 "routing.taskClassRules[{i}].taskClass {:?} duplicates rule {first} after \
228 normalization (trim + case-insensitive); first-match-wins makes the later rule \
229 dead config — remove or reword one",
230 rule.task_class
231 ));
232 }
233 }
234 let mut seen_patterns: std::collections::HashMap<String, usize> =
235 std::collections::HashMap::new();
236 for (i, rule) in routing.pattern_rules.iter().enumerate() {
237 let normalized = normalize_task_class(&rule.pattern);
238 if normalized.is_empty() {
239 return Err(format!(
240 "routing.patternRules[{i}].pattern must be non-empty: routing matches on \
241 task-class text, so a blank pattern could never match honestly (spell \"*\" \
242 to match every class)"
243 ));
244 }
245 if let Some(first) = seen_patterns.insert(normalized, i) {
246 return Err(format!(
247 "routing.patternRules[{i}].pattern {:?} duplicates rule {first} after \
248 normalization (trim + case-insensitive); first-match-wins makes the later rule \
249 dead config — remove or reword one",
250 rule.pattern
251 ));
252 }
253 }
254 Ok(())
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::types::{PatternRoute, TaskClassRoute};
261
262 fn table(rules: &[(&str, ExecutorTier)]) -> RoutingConfig {
263 RoutingConfig {
264 task_class_rules: rules
265 .iter()
266 .map(|(task_class, tier)| TaskClassRoute {
267 task_class: task_class.to_string(),
268 tier: *tier,
269 })
270 .collect(),
271 pattern_rules: vec![],
272 }
273 }
274
275 fn pattern_table(rules: &[(&str, ExecutorTier)]) -> RoutingConfig {
276 RoutingConfig {
277 task_class_rules: vec![],
278 pattern_rules: rules
279 .iter()
280 .map(|(pattern, tier)| PatternRoute {
281 pattern: pattern.to_string(),
282 tier: *tier,
283 })
284 .collect(),
285 }
286 }
287
288 #[test]
289 fn routing_abstraction_first_matching_rule_wins() {
290 let routing = table(&[
291 ("execution-class", ExecutorTier::Local),
292 ("docs-class", ExecutorTier::Frontier),
293 ]);
294 assert_eq!(
295 table_tier(&routing, Some("execution-class")),
296 ExecutorTier::Local
297 );
298 assert_eq!(
299 table_tier(&routing, Some("docs-class")),
300 ExecutorTier::Frontier
301 );
302
303 // A class two rules could have claimed is decided by ORDER: the
304 // earlier rule wins, so table order is the only precedence knob.
305 let routing = table(&[
306 ("execution-class", ExecutorTier::Frontier),
307 ("execution-class", ExecutorTier::Local),
308 ]);
309 assert_eq!(
310 table_tier(&routing, Some("execution-class")),
311 ExecutorTier::Frontier,
312 "first match wins — the shadowed later rule never decides"
313 );
314 }
315
316 #[test]
317 fn routing_abstraction_matching_uses_the_floors_normalization() {
318 let routing = table(&[("Execution-Class", ExecutorTier::Local)]);
319 // Same case- and whitespace-insensitivity as the hardcoded floor.
320 assert_eq!(
321 table_tier(&routing, Some(" EXECUTION-CLASS ")),
322 ExecutorTier::Local
323 );
324 }
325
326 #[test]
327 fn routing_abstraction_no_matching_rule_stays_frontier() {
328 let routing = table(&[("execution-class", ExecutorTier::Local)]);
329 assert_eq!(
330 table_tier(&routing, Some("planning-class")),
331 ExecutorTier::Frontier
332 );
333 assert_eq!(table_tier(&routing, None), ExecutorTier::Frontier);
334 assert_eq!(table_tier(&routing, Some("")), ExecutorTier::Frontier);
335 }
336
337 #[test]
338 fn routing_abstraction_resolution_is_deterministic() {
339 // The floor's contract: same (table, task class) → same route, every
340 // time. Iterate enough to catch accidental nondeterminism (hash
341 // iteration, time, randomness) — the implementation is an ordered
342 // scan, so any deviation here is a regression.
343 let routing = table(&[
344 ("alpha-class", ExecutorTier::Local),
345 ("beta-class", ExecutorTier::Frontier),
346 ("gamma-class", ExecutorTier::Local),
347 ]);
348 for _ in 0..256 {
349 assert_eq!(
350 table_tier(&routing, Some("alpha-class")),
351 ExecutorTier::Local
352 );
353 assert_eq!(
354 table_tier(&routing, Some("beta-class")),
355 ExecutorTier::Frontier
356 );
357 assert_eq!(
358 table_tier(&routing, Some("gamma-class")),
359 ExecutorTier::Local
360 );
361 assert_eq!(
362 table_tier(&routing, Some("unlisted-class")),
363 ExecutorTier::Frontier
364 );
365 }
366 }
367
368 #[test]
369 fn routing_abstraction_validate_table_fails_closed_naming_the_rule() {
370 // Blank class: refused, naming the offending rule index.
371 let routing = table(&[
372 ("docs-class", ExecutorTier::Frontier),
373 (" ", ExecutorTier::Local),
374 ]);
375 let err = validate_table(&routing).expect_err("a blank class must be refused");
376 assert!(err.contains("routing.taskClassRules[1].taskClass"), "{err}");
377 assert!(err.contains("non-empty"), "{err}");
378
379 // Duplicate after normalization: refused, naming both rules — a
380 // shadowed rule is dead config under first-match-wins.
381 let routing = table(&[
382 ("execution-class", ExecutorTier::Local),
383 (" Execution-Class ", ExecutorTier::Frontier),
384 ]);
385 let err = validate_table(&routing).expect_err("a duplicate class must be refused");
386 assert!(err.contains("routing.taskClassRules[1].taskClass"), "{err}");
387 assert!(err.contains("duplicates rule 0"), "{err}");
388
389 // A clean table validates.
390 let routing = table(&[
391 ("execution-class", ExecutorTier::Local),
392 ("docs-class", ExecutorTier::Frontier),
393 ]);
394 assert!(validate_table(&routing).is_ok(), "a clean table must pass");
395 assert!(validate_table(&RoutingConfig::default()).is_ok());
396 }
397
398 #[test]
399 fn routing_rules_config_exact_rule_beats_pattern_and_order_decides() {
400 // An exact taskClassRules entry always beats a pattern that would
401 // also match — specific over general, documented precedence.
402 let mut routing = pattern_table(&[("execution-*", ExecutorTier::Frontier)]);
403 routing.task_class_rules.push(TaskClassRoute {
404 task_class: "execution-class".to_string(),
405 tier: ExecutorTier::Local,
406 });
407 assert_eq!(
408 resolve(&routing, Some("execution-class")),
409 RouteDecision {
410 tier: ExecutorTier::Local,
411 rule: RuleMatch::TaskClass(0),
412 }
413 );
414 // The same class WITHOUT the exact rule is the pattern's claim.
415 assert_eq!(
416 resolve(&routing, Some("execution-heavy-class")),
417 RouteDecision {
418 tier: ExecutorTier::Frontier,
419 rule: RuleMatch::Pattern(0),
420 }
421 );
422
423 // Within the pattern list, order is the only precedence knob.
424 let routing = pattern_table(&[
425 ("docs-*", ExecutorTier::Local),
426 ("*", ExecutorTier::Frontier),
427 ]);
428 assert_eq!(
429 resolve(&routing, Some("docs-class")).rule,
430 RuleMatch::Pattern(0),
431 "first matching pattern wins"
432 );
433 assert_eq!(
434 resolve(&routing, Some("anything-else")).rule,
435 RuleMatch::Pattern(1),
436 "the catch-all claims what the earlier pattern did not"
437 );
438 }
439
440 #[test]
441 fn routing_rules_config_pattern_glob_semantics() {
442 let routing = pattern_table(&[("execution-*-class", ExecutorTier::Local)]);
443 // `*` matches any run, empty included: the double dash of
444 // "execution--class" is "execution-" + "" + "-class".
445 for matching in [
446 "execution-heavy-class",
447 "execution--class",
448 "EXECUTION-A-B-CLASS ",
449 ] {
450 assert_eq!(
451 resolve(&routing, Some(matching)).tier,
452 ExecutorTier::Local,
453 "{matching:?} must match execution-*-class after normalization"
454 );
455 }
456 // Anchored at BOTH ends: "execution-class" has no second dash for the
457 // literal "-class" suffix, and prefixes/suffixes around the pattern
458 // never slide.
459 for non_matching in [
460 "execution-class",
461 "execution-class-extra",
462 "xexecution-heavy-class",
463 "docs-class",
464 ] {
465 assert_eq!(
466 resolve(&routing, Some(non_matching)),
467 RouteDecision {
468 tier: ExecutorTier::Frontier,
469 rule: RuleMatch::FallThrough,
470 },
471 "{non_matching:?} must NOT match execution-*-class"
472 );
473 }
474 // Bare `*` matches everything, including the empty class.
475 assert!(pattern_matches("*", ""));
476 assert!(pattern_matches("**", "anything"));
477 assert!(!pattern_matches("", "x"));
478 assert!(pattern_matches("", ""));
479 }
480
481 #[test]
482 fn routing_rules_config_resolution_is_deterministic() {
483 // Same (table, task class) → same decision, rule included, every
484 // time — the per-session provenance record recomputes this at spawn,
485 // so any nondeterminism would fork the audit trail.
486 let mut routing = pattern_table(&[
487 ("docs-*", ExecutorTier::Frontier),
488 ("execution-*", ExecutorTier::Local),
489 ]);
490 routing.task_class_rules.push(TaskClassRoute {
491 task_class: "docs-class".to_string(),
492 tier: ExecutorTier::Local,
493 });
494 for _ in 0..256 {
495 assert_eq!(
496 resolve(&routing, Some("docs-class")),
497 RouteDecision {
498 tier: ExecutorTier::Local,
499 rule: RuleMatch::TaskClass(0),
500 }
501 );
502 assert_eq!(
503 resolve(&routing, Some("docs-other")),
504 RouteDecision {
505 tier: ExecutorTier::Frontier,
506 rule: RuleMatch::Pattern(0),
507 }
508 );
509 assert_eq!(
510 resolve(&routing, Some("execution-class")),
511 RouteDecision {
512 tier: ExecutorTier::Local,
513 rule: RuleMatch::Pattern(1),
514 }
515 );
516 assert_eq!(
517 resolve(&routing, Some("unlisted")),
518 RouteDecision {
519 tier: ExecutorTier::Frontier,
520 rule: RuleMatch::FallThrough,
521 }
522 );
523 }
524 }
525
526 #[test]
527 fn routing_rules_config_validate_table_fails_closed_on_pattern_rules() {
528 // Blank pattern: refused, naming the rule index + field.
529 let routing = pattern_table(&[
530 ("docs-*", ExecutorTier::Frontier),
531 (" ", ExecutorTier::Local),
532 ]);
533 let err = validate_table(&routing).expect_err("a blank pattern must be refused");
534 assert!(err.contains("routing.patternRules[1].pattern"), "{err}");
535 assert!(err.contains("non-empty"), "{err}");
536
537 // Duplicate after normalization: refused, naming both rules.
538 let routing = pattern_table(&[
539 ("docs-*", ExecutorTier::Local),
540 (" DOCS-* ", ExecutorTier::Frontier),
541 ]);
542 let err = validate_table(&routing).expect_err("a duplicate pattern must be refused");
543 assert!(err.contains("routing.patternRules[1].pattern"), "{err}");
544 assert!(err.contains("duplicates rule 0"), "{err}");
545
546 // A clean mixed table validates.
547 let mut routing = pattern_table(&[("docs-*", ExecutorTier::Frontier)]);
548 routing.task_class_rules.push(TaskClassRoute {
549 task_class: "execution-class".to_string(),
550 tier: ExecutorTier::Local,
551 });
552 assert!(validate_table(&routing).is_ok(), "a clean table must pass");
553 }
554
555 #[test]
556 fn routing_rules_config_seed_executor_route_records_rule_and_effective_tier() {
557 // A folded ticket goal (the exact channel create parses) + a seeded,
558 // frozen config → the record folded from mission.created.
559 let ticket = crate::ticket::Ticket::parse(
560 "bump-dep",
561 "---\ntitle: Bump a dependency\ntask-class: execution-class\n---\n\n## Goal\nBump it.\n",
562 )
563 .expect("parse ticket");
564 let goal = ticket.mission_goal();
565
566 // Table routed the class local and the endpoint exists: the seeded
567 // config was rewritten to the local backend, so the record names the
568 // rule AND the effective local tier.
569 let mut table = pattern_table(&[]);
570 table.task_class_rules.push(TaskClassRoute {
571 task_class: "execution-class".to_string(),
572 tier: ExecutorTier::Local,
573 });
574 let mut cfg = crate::types::MissionConfig {
575 routing: table,
576 ..crate::types::MissionConfig::default()
577 };
578 cfg.worker.backend = Some("local".to_string());
579 let route = seed_executor_route(&cfg, &goal).expect("a task class routes");
580 assert_eq!(route.tier, ExecutorTier::Local);
581 assert_eq!(route.rule.as_deref(), Some("taskClassRules[0]"));
582
583 // Fall-through: the table exists but claims nothing — tier effective
584 // from the (unrouted) config, no rule named.
585 let cfg = crate::types::MissionConfig {
586 routing: pattern_table(&[("docs-*", ExecutorTier::Local)]),
587 ..crate::types::MissionConfig::default()
588 };
589 let route = seed_executor_route(&cfg, &goal).expect("a task class routes");
590 assert_eq!(route.tier, ExecutorTier::Frontier);
591 assert_eq!(route.rule, None, "the fall-through names no rule");
592
593 // No table at all: the legacy literal floor decided; no rule to name,
594 // and a goal WITHOUT a folded task class records nothing at all (the
595 // pre-provenance byte shape).
596 let cfg = crate::types::MissionConfig::default();
597 let route = seed_executor_route(&cfg, &goal).expect("a task class routes");
598 assert_eq!(route.tier, ExecutorTier::Frontier);
599 assert_eq!(route.rule, None);
600 assert!(seed_executor_route(&cfg, "ship the demo feature").is_none());
601 }
602
603 /// The no-hardcoded-model-ids rule (KRZ-331, citing
604 /// docs/reviews/local-llm-and-triumvirate.md §1), pinned as a grep over
605 /// this module's NON-TEST source: the routing table resolves capability
606 /// classes ([`ExecutorTier`]) only, so no model-id literal may appear in
607 /// the resolution logic or its docs. Model ids legitimately live
608 /// ELSEWHERE in core and are the documented allowlist, all outside the
609 /// routing table code paths: the per-role default model names in
610 /// `config.rs` (`role_default_model`) and `types.rs`
611 /// (`MissionConfig::default`), the backend default model constants in
612 /// `cost.rs`, tier classification in `config::model_tier`, and test
613 /// fixtures everywhere (including this file's own test module, which the
614 /// split below excludes — the needles themselves live there).
615 #[test]
616 fn routing_abstraction_routing_logic_carries_no_model_id_literals() {
617 let source = include_str!("routing.rs");
618 let logic = source
619 .split("#[cfg(test)]")
620 .next()
621 .expect("the test module marker exists");
622 // Quoted alias forms (what a literal in code would look like) and
623 // distinctive id substrings (provider-qualified or versioned ids).
624 for needle in [
625 "\"opus\"",
626 "\"sonnet\"",
627 "\"haiku\"",
628 "\"fable\"",
629 "gpt-",
630 "glm-",
631 "kimi-code",
632 "claude-",
633 "fireworks",
634 "qwen",
635 "mistral",
636 "deepseek",
637 ] {
638 assert!(
639 !logic.contains(needle),
640 "model-id literal {needle:?} must never appear in the routing \
641 table code paths — route capability classes (ExecutorTier), \
642 not models"
643 );
644 }
645 }
646}