Skip to main content

keel_cli/
effective.rs

1//! `keel doctor --effective-policy` — show the composed policy the core is
2//! given (the defaults/E005 CCR: front ends and CLI compose
3//! `defaults < packs < user` *before* `keel_configure`; the core layers no
4//! pack underneath, `contracts/core-ffi.h`).
5//!
6//! [`effective_policy`] is the Rust twin of Python's
7//! `keel._defaults.apply_pack_defaults` and Node's
8//! `defaults.mjs::applyPackDefaults`: merge granularity is per KEY of
9//! `defaults.outbound` / `defaults.llm` (a higher layer replaces a key it sets
10//! wholesale, fills in the rest), and target tables pass through untouched —
11//! the engine resolves target → defaults precedence per key at execute time.
12//! The three implementations must agree byte-for-byte; the cross-language
13//! parity test in `tests/cli.rs` runs all three over the same fixture.
14//!
15//! The Level 0 layer is the frozen smart-defaults pack itself,
16//! `contracts/defaults.toml`, embedded with `include_str!` so the binary and
17//! the contract can never drift ("this document ships compiled into the
18//! binary"). The printed policy is the PRE-resolution merge — `cache = { mode
19//! = "dev" }` stays symbolic (the front ends resolve it against `KEEL_ENV` at
20//! run time), keeping the `--json` twin byte-deterministic.
21
22use std::collections::{BTreeMap, BTreeSet};
23use std::path::Path;
24use std::sync::OnceLock;
25
26use keel_core_api::policy::Policy;
27use serde::Serialize;
28use serde_json::{Map, Value};
29
30use crate::render::to_json;
31use crate::{EXIT_OK, EXIT_USAGE, Rendered, evidence, scan};
32
33/// The frozen smart-defaults pack, compiled in (contracts/defaults.toml) via
34/// the crate-local vendored copy (`build.rs` asserts it stays byte-identical
35/// to the repo original) — the same copy `init.rs`/`explain.rs` embed, so
36/// `cargo package`/`cargo publish` (which cannot ship files outside the crate
37/// directory) still build correctly from a standalone crates.io tarball.
38const DEFAULTS_TOML: &str = include_str!("../contract/defaults.toml");
39
40/// Canonical key order inside a policy layer table (the order the contract
41/// file and `keel init` write); keys outside this list follow, sorted.
42const LAYER_KEY_ORDER: [&str; 5] = ["timeout", "retry", "breaker", "rate", "cache"];
43
44/// The run-time meaning of a `mode = "dev"` cache, stated so the printed
45/// pre-resolution merge cannot mislead (dx-spec honesty).
46const DEV_CACHE_NOTE: &str = "cache mode \"dev\" is resolved by the front end at run time \
47     (a concrete ttl off-prod; dropped when KEEL_ENV=prod) \u{2014} the policy shown is the \
48     pre-resolution merge.";
49
50/// One pack the CLI can detect statically (via the scan's import evidence),
51/// with the policy fragment it folds into the `packs` merge layer. Fragments
52/// mirror the front ends: the provider packs (`openai`, `anthropic`) and the
53/// AI-SDK middleware emit the generic `[defaults.llm]` layer; `mcp:` targets
54/// inherit `[defaults.outbound]` and contribute no fragment of their own.
55struct Pack {
56    lib: &'static str,
57    fragment: fn() -> Value,
58}
59
60/// Registration order = report order (matches the front ends' provider packs
61/// first, then the Node-only packs) — stable, deterministic.
62const PACKS: &[Pack] = &[
63    Pack {
64        lib: "openai",
65        fragment: llm_pack_fragment,
66    },
67    Pack {
68        lib: "anthropic",
69        fragment: llm_pack_fragment,
70    },
71    Pack {
72        lib: "ai-sdk",
73        fragment: llm_pack_fragment,
74    },
75    Pack {
76        lib: "mcp",
77        fragment: empty_fragment,
78    },
79];
80
81/// The `[defaults]` table of the embedded contract, parsed once.
82fn level0() -> &'static Map<String, Value> {
83    static LEVEL0: OnceLock<Map<String, Value>> = OnceLock::new();
84    LEVEL0.get_or_init(|| {
85        let toml_value: toml::Value = DEFAULTS_TOML
86            .parse()
87            .expect("contracts/defaults.toml is valid TOML");
88        let json = serde_json::to_value(&toml_value).expect("defaults normalize to JSON");
89        json.get("defaults")
90            .and_then(Value::as_object)
91            .cloned()
92            .expect("contracts/defaults.toml has a [defaults] table")
93    })
94}
95
96/// One embedded Level 0 layer (`outbound` or `llm`) as an owned table.
97fn level0_layer(layer: &str) -> Map<String, Value> {
98    level0()
99        .get(layer)
100        .and_then(Value::as_object)
101        .cloned()
102        .unwrap_or_default()
103}
104
105/// The generic `[defaults.llm]` pack fragment — what the provider packs and
106/// the AI-SDK middleware contribute (Python `provider_defaults()`, Node
107/// `llmPack.defaults()`). Folding it is the identity over the embedded
108/// defaults, by construction.
109pub fn llm_pack_fragment() -> Value {
110    let mut defaults = Map::new();
111    defaults.insert("llm".to_owned(), Value::Object(level0_layer("llm")));
112    let mut frag = Map::new();
113    frag.insert("defaults".to_owned(), Value::Object(defaults));
114    Value::Object(frag)
115}
116
117/// The empty fragment (`mcp:` targets inherit `[defaults.outbound]`).
118fn empty_fragment() -> Value {
119    Value::Object(Map::new())
120}
121
122/// A JSON value's table view, or empty — the twin of Python's `_table` /
123/// Node's `isTable` guards.
124fn table(v: Option<&Value>) -> Map<String, Value> {
125    v.and_then(Value::as_object).cloned().unwrap_or_default()
126}
127
128/// Fold the pack fragments' `defaults.outbound` / `defaults.llm` tables, later
129/// fragments overriding earlier per key (Python's `pack_outbound.update(...)`).
130fn pack_layers(fragments: &[Value]) -> (Map<String, Value>, Map<String, Value>) {
131    let mut outbound = Map::new();
132    let mut llm = Map::new();
133    for frag in fragments {
134        let frag_defaults = table(frag.get("defaults"));
135        outbound.extend(table(frag_defaults.get("outbound")));
136        llm.extend(table(frag_defaults.get("llm")));
137    }
138    (outbound, llm)
139}
140
141/// Compose the effective policy: `defaults < packs < user`, per-key wholesale
142/// on the `defaults.outbound` / `defaults.llm` tables, target tables untouched.
143/// Returns a NEW value; the input is never borrowed mutably. Idempotent on the
144/// embedded Level 0 policy. Byte-identical (as sorted JSON) to Python
145/// `apply_pack_defaults` and Node `applyPackDefaults` over the same inputs.
146pub fn effective_policy(user: &Value, fragments: &[Value]) -> Value {
147    let mut out = user.as_object().cloned().unwrap_or_default();
148    let user_defaults = table(out.get("defaults"));
149    let (pack_outbound, pack_llm) = pack_layers(fragments);
150
151    let mut outbound = level0_layer("outbound");
152    outbound.extend(pack_outbound);
153    outbound.extend(table(user_defaults.get("outbound")));
154
155    let mut llm = level0_layer("llm");
156    llm.extend(pack_llm);
157    llm.extend(table(user_defaults.get("llm")));
158
159    let mut defaults = user_defaults;
160    defaults.insert("outbound".to_owned(), Value::Object(outbound));
161    defaults.insert("llm".to_owned(), Value::Object(llm));
162    out.insert("defaults".to_owned(), Value::Object(defaults));
163    Value::Object(out)
164}
165
166/// Per-key provenance for the two merged layers: which layer WON each
167/// `defaults.outbound.*` / `defaults.llm.*` key — `"user"` if the user set it,
168/// else `"pack"` if a detected pack fragment set it, else `"defaults"`.
169/// (A pack that re-affirms a default still wins the key, exactly as the merge
170/// itself resolves it.)
171fn layer_sources(user: &Value, fragments: &[Value]) -> BTreeMap<String, &'static str> {
172    let user_defaults = table(user.get("defaults"));
173    let (pack_outbound, pack_llm) = pack_layers(fragments);
174    let mut sources = BTreeMap::new();
175    for (layer, pack_keys) in [("outbound", &pack_outbound), ("llm", &pack_llm)] {
176        let user_keys = table(user_defaults.get(layer));
177        let mut merged = level0_layer(layer);
178        merged.extend(pack_keys.clone());
179        merged.extend(user_keys.clone());
180        for key in merged.keys() {
181            let source = if user_keys.contains_key(key) {
182                "user"
183            } else if pack_keys.contains_key(key) {
184                "pack"
185            } else {
186                "defaults"
187            };
188            sources.insert(format!("defaults.{layer}.{key}"), source);
189        }
190    }
191    sources
192}
193
194/// Whether any layer of the composed policy still carries the symbolic
195/// dev-loop cache (`cache = { mode = "dev" }`) — the layers the front ends'
196/// dev-cache resolution walks: `defaults.llm`, `defaults.outbound`, targets.
197fn has_dev_cache(policy: &Value) -> bool {
198    let defaults = table(policy.get("defaults"));
199    if cache_is_dev(defaults.get("llm")) || cache_is_dev(defaults.get("outbound")) {
200        return true;
201    }
202    table(policy.get("target"))
203        .values()
204        .any(|t| cache_is_dev(Some(t)))
205}
206
207fn cache_is_dev(layer: Option<&Value>) -> bool {
208    layer
209        .and_then(|l| l.get("cache"))
210        .and_then(|c| c.get("mode"))
211        .and_then(Value::as_str)
212        == Some("dev")
213}
214
215/// The whole `--effective-policy` report. `policy` is THE effective policy —
216/// the object handed to `keel_configure`, and the object the cross-language
217/// parity golden pins.
218#[derive(Debug, Serialize)]
219struct EffectiveReport {
220    notes: Vec<&'static str>,
221    packs: Vec<&'static str>,
222    policy: Value,
223    sources: BTreeMap<String, &'static str>,
224    user_policy_present: bool,
225}
226
227/// Run `keel doctor --effective-policy` for `project`.
228pub fn run(project: &Path) -> Rendered {
229    let user = match load_user_policy(&evidence::keel_toml(project)) {
230        UserPolicy::Invalid { field, message } => {
231            return invalid_rendered(field.as_deref(), &message);
232        }
233        UserPolicy::Absent => None,
234        UserPolicy::Valid(v) => Some(v),
235    };
236    let scanned = scan::scan(project);
237    let report = build_report(&scanned.libs, user);
238    let human = human(&report);
239    Rendered::ok(human, to_json(&report)).with_exit(EXIT_OK)
240}
241
242/// Assemble the report from the detected import evidence and the (validated)
243/// user policy. Pure, so tests pin it without a filesystem.
244fn build_report(libs: &BTreeSet<String>, user: Option<Value>) -> EffectiveReport {
245    let detected: Vec<&Pack> = PACKS.iter().filter(|p| libs.contains(p.lib)).collect();
246    let fragments: Vec<Value> = detected.iter().map(|p| (p.fragment)()).collect();
247    let user_policy_present = user.is_some();
248    let user = user.unwrap_or(Value::Object(Map::new()));
249
250    let policy = effective_policy(&user, &fragments);
251    let sources = layer_sources(&user, &fragments);
252    let notes = if has_dev_cache(&policy) {
253        vec![DEV_CACHE_NOTE]
254    } else {
255        Vec::new()
256    };
257    EffectiveReport {
258        notes,
259        packs: detected.iter().map(|p| p.lib).collect(),
260        policy,
261        sources,
262        user_policy_present,
263    }
264}
265
266/// The user's `keel.toml`, loaded and validated against the typed
267/// [`Policy`] model (exact field path on error, via `serde_path_to_error`).
268enum UserPolicy {
269    Absent,
270    Valid(Value),
271    Invalid {
272        field: Option<String>,
273        message: String,
274    },
275}
276
277fn load_user_policy(path: &Path) -> UserPolicy {
278    if !path.exists() {
279        return UserPolicy::Absent;
280    }
281    let Ok(text) = std::fs::read_to_string(path) else {
282        return UserPolicy::Invalid {
283            field: None,
284            message: "keel.toml exists but could not be read".to_owned(),
285        };
286    };
287    let toml_value: toml::Value = match text.parse() {
288        Ok(v) => v,
289        Err(e) => {
290            return UserPolicy::Invalid {
291                field: None,
292                message: format!("keel.toml is not valid TOML: {e}"),
293            };
294        }
295    };
296    let json_value = match serde_json::to_value(&toml_value) {
297        Ok(v) => v,
298        Err(e) => {
299            return UserPolicy::Invalid {
300                field: None,
301                message: format!("keel.toml could not be normalized: {e}"),
302            };
303        }
304    };
305    match serde_path_to_error::deserialize::<_, Policy>(&json_value) {
306        Ok(_) => UserPolicy::Valid(json_value),
307        Err(e) => UserPolicy::Invalid {
308            field: Some(e.path().to_string()),
309            message: e.inner().to_string(),
310        },
311    }
312}
313
314/// The error result for an invalid `keel.toml`: no effective policy exists to
315/// compose, so this is a usage error (KEEL-E001, the frozen policy-invalid
316/// code) on stderr.
317fn invalid_rendered(field: Option<&str>, message: &str) -> Rendered {
318    let at = field.unwrap_or("(document)");
319    let human = format!(
320        "keel \u{25b8} doctor --effective-policy: keel.toml is invalid (KEEL-E001), so there is \
321         no effective policy to compose.\n  at `{at}`: {message}\n  \u{2192} Fix the field and \
322         re-run; `keel explain KEEL-E001` has the contract, `keel doctor` the full report.\n"
323    );
324    let json = to_json(&serde_json::json!({
325        "code": "KEEL-E001",
326        "error": message,
327        "field": field,
328    }));
329    Rendered::ok(human, json).with_exit(EXIT_USAGE).on_stderr()
330}
331
332// ---- human rendering: the merged policy as annotated TOML ----------------
333
334/// A TOML basic string.
335fn toml_string(s: &str) -> String {
336    format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
337}
338
339/// A TOML key: bare when possible, quoted otherwise.
340fn toml_key(k: &str) -> String {
341    let bare = !k.is_empty()
342        && k.bytes()
343            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-');
344    if bare { k.to_owned() } else { toml_string(k) }
345}
346
347/// Render a JSON value as a TOML value (inline tables, sorted keys — the JSON
348/// map is a `BTreeMap`, so the output is deterministic). `Null` cannot reach
349/// here: every merge input is TOML-parsed or contract-built.
350fn toml_value(v: &Value) -> String {
351    match v {
352        Value::String(s) => toml_string(s),
353        Value::Bool(b) => b.to_string(),
354        Value::Number(n) => n.to_string(),
355        Value::Null => "null".to_owned(),
356        Value::Array(items) => {
357            let inner: Vec<String> = items.iter().map(toml_value).collect();
358            format!("[{}]", inner.join(", "))
359        }
360        Value::Object(m) => {
361            if m.is_empty() {
362                "{}".to_owned()
363            } else {
364                let inner: Vec<String> = m
365                    .iter()
366                    .map(|(k, v)| format!("{} = {}", toml_key(k), toml_value(v)))
367                    .collect();
368                format!("{{ {} }}", inner.join(", "))
369            }
370        }
371    }
372}
373
374/// Layer keys in canonical order (timeout, retry, breaker, rate, cache), then
375/// anything else sorted.
376fn ordered_keys(m: &Map<String, Value>) -> Vec<&String> {
377    let mut keys: Vec<&String> = LAYER_KEY_ORDER
378        .iter()
379        .filter_map(|k| m.get_key_value(*k).map(|(k, _)| k))
380        .collect();
381    keys.extend(m.keys().filter(|k| !LAYER_KEY_ORDER.contains(&k.as_str())));
382    keys
383}
384
385/// One `[section]` of the annotated TOML: `header_line` is printed verbatim,
386/// each key gets the annotation `annotate` returns (aligned on the key column).
387fn render_table(
388    out: &mut String,
389    header_line: &str,
390    m: &Map<String, Value>,
391    annotate: &dyn Fn(&str) -> Option<&'static str>,
392) {
393    let header = format!("\n{header_line}\n");
394    out.push_str(&header);
395    let width = m.keys().map(String::len).max().unwrap_or(0);
396    for key in ordered_keys(m) {
397        let value = toml_value(&m[key.as_str()]);
398        let line = match annotate(key) {
399            Some(source) => format!("{key:<width$} = {value}  # {source}\n"),
400            None => format!("{key:<width$} = {value}\n"),
401        };
402        out.push_str(&line);
403    }
404}
405
406/// The human report: the merged policy as TOML, every `defaults` key annotated
407/// with the layer that won it — so no fact escapes the JSON twin.
408fn human(r: &EffectiveReport) -> String {
409    let mut out = String::from("keel \u{25b8} doctor --effective-policy\n\n");
410    out.push_str(
411        "The composed policy `keel_configure` receives \u{2014} defaults < packs < user,\n\
412         merged per key; the core layers no pack underneath.\n",
413    );
414    out.push_str("  defaults  embedded contracts/defaults.toml (Level 0)\n");
415    let packs = if r.packs.is_empty() {
416        "(none detected)".to_owned()
417    } else {
418        r.packs.join(", ")
419    };
420    let packs_line = format!("  packs     {packs}\n");
421    out.push_str(&packs_line);
422    let user_line = if r.user_policy_present {
423        "keel.toml"
424    } else {
425        "(no keel.toml \u{2014} Level 0 applies)"
426    };
427    let user_row = format!("  user      {user_line}\n");
428    out.push_str(&user_row);
429
430    let policy = table(Some(&r.policy));
431    let defaults = table(policy.get("defaults"));
432
433    // The two merged layers, each key annotated with its winning layer.
434    for layer in ["outbound", "llm"] {
435        if let Some(Value::Object(m)) = defaults.get(layer) {
436            render_table(&mut out, &format!("[defaults.{layer}]"), m, &|key| {
437                r.sources.get(&format!("defaults.{layer}.{key}")).copied()
438            });
439        }
440    }
441    // Any other `defaults` table is user-only (the typed model admits none, so
442    // this renders nothing after validation; kept for merge-shape honesty).
443    for (key, v) in &defaults {
444        if key != "outbound"
445            && key != "llm"
446            && let Value::Object(m) = v
447        {
448            render_table(
449                &mut out,
450                &format!("[defaults.{}]  # user", toml_key(key)),
451                m,
452                &|_| None,
453            );
454        }
455    }
456    // Remaining top-level tables (flows / journal / telemetry), pure user.
457    for (key, v) in &policy {
458        if key == "defaults" || key == "target" {
459            continue;
460        }
461        if let Value::Object(m) = v {
462            render_table(
463                &mut out,
464                &format!("[{}]  # user", toml_key(key)),
465                m,
466                &|_| None,
467            );
468        } else {
469            let line = format!("\n{} = {}  # user\n", toml_key(key), toml_value(v));
470            out.push_str(&line);
471        }
472    }
473    // Target tables pass through the merge untouched.
474    if let Some(Value::Object(targets)) = policy.get("target") {
475        for (name, t) in targets {
476            let header = format!(
477                "[target.{}]  # user \u{2014} pass-through (engine resolves target \u{2192} \
478                 defaults per key)",
479                toml_string(name)
480            );
481            if let Value::Object(m) = t {
482                render_table(&mut out, &header, m, &|_| None);
483            }
484        }
485    }
486    for note in &r.notes {
487        let line = format!("\nnote: {note}\n");
488        out.push_str(&line);
489    }
490    out
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use serde_json::json;
497
498    /// The embedded contract parses to the documented Level 0 values (the
499    /// same sync-test the Python/Node front ends run against the repo file).
500    #[test]
501    fn embedded_defaults_match_contract_values() {
502        let outbound = level0_layer("outbound");
503        assert_eq!(outbound["timeout"], json!("30s"));
504        assert_eq!(
505            outbound["retry"],
506            json!({
507                "attempts": 3,
508                "schedule": "exp(200ms, x2, max 30s, jitter)",
509                "on": ["conn", "timeout", "429", "5xx"],
510            })
511        );
512        assert_eq!(
513            outbound["breaker"],
514            json!({ "failures": 5, "cooldown": "15s" })
515        );
516
517        let llm = level0_layer("llm");
518        assert_eq!(llm["timeout"], json!("120s"));
519        assert_eq!(llm["retry"]["attempts"], json!(6));
520        assert_eq!(llm["breaker"], json!({ "failures": 5, "cooldown": "30s" }));
521        assert_eq!(llm["cache"], json!({ "mode": "dev" }));
522    }
523
524    /// Mirror of Python `test_empty_policy_gets_full_pack_layers` / the Node
525    /// twin in llm-pack.test.mjs.
526    #[test]
527    fn empty_policy_gets_full_pack_layers() {
528        let merged = effective_policy(&json!({}), &[]);
529        assert_eq!(
530            merged["defaults"]["outbound"],
531            Value::Object(level0_layer("outbound"))
532        );
533        assert_eq!(
534            merged["defaults"]["llm"],
535            Value::Object(level0_layer("llm"))
536        );
537    }
538
539    /// Mirror of Python `test_user_key_replaces_pack_key_wholesale`.
540    #[test]
541    fn user_key_replaces_layer_key_wholesale_and_targets_pass_through() {
542        let user = json!({
543            "defaults": { "llm": { "retry": { "attempts": 2 } } },
544            "target": { "x": {} },
545        });
546        let merged = effective_policy(&user, &[]);
547        assert_eq!(
548            merged["defaults"]["llm"]["retry"],
549            json!({ "attempts": 2 }),
550            "user retry wins wholesale"
551        );
552        assert_eq!(
553            merged["defaults"]["llm"]["cache"],
554            json!({ "mode": "dev" }),
555            "pack cache kept"
556        );
557        assert_eq!(
558            merged["defaults"]["llm"]["breaker"],
559            json!({ "failures": 5, "cooldown": "30s" }),
560            "pack breaker kept"
561        );
562        assert_eq!(
563            merged["target"],
564            json!({ "x": {} }),
565            "target tables untouched"
566        );
567    }
568
569    /// Mirror of Python `test_idempotent_on_level0`.
570    #[test]
571    fn idempotent_on_level0() {
572        let level0_policy = Value::Object(
573            [("defaults".to_owned(), Value::Object(level0().clone()))]
574                .into_iter()
575                .collect(),
576        );
577        assert_eq!(effective_policy(&level0_policy, &[]), level0_policy);
578    }
579
580    /// Mirror of Python `test_provider_fragments_fold_as_identity`.
581    #[test]
582    fn provider_fragments_fold_as_identity() {
583        let with = effective_policy(&json!({}), &[llm_pack_fragment(), llm_pack_fragment()]);
584        let without = effective_policy(&json!({}), &[]);
585        assert_eq!(with, without);
586    }
587
588    #[test]
589    fn sources_classify_user_pack_and_defaults() {
590        let user = json!({ "defaults": { "llm": { "retry": { "attempts": 2 } } } });
591        let sources = layer_sources(&user, &[llm_pack_fragment()]);
592        assert_eq!(sources["defaults.llm.retry"], "user");
593        assert_eq!(sources["defaults.llm.cache"], "pack");
594        assert_eq!(sources["defaults.llm.timeout"], "pack");
595        assert_eq!(sources["defaults.outbound.timeout"], "defaults");
596        assert_eq!(sources["defaults.outbound.retry"], "defaults");
597    }
598
599    #[test]
600    fn dev_cache_note_tracks_the_composed_policy() {
601        let with_default_cache = build_report(&BTreeSet::new(), None);
602        assert_eq!(with_default_cache.notes, vec![DEV_CACHE_NOTE]);
603
604        // The user replaces the llm cache wholesale with a concrete ttl → the
605        // symbolic dev cache is gone and the note with it.
606        let user = json!({ "defaults": { "llm": { "cache": { "ttl": "1h" } } } });
607        let report = build_report(&BTreeSet::new(), Some(user));
608        assert!(report.notes.is_empty());
609    }
610
611    #[test]
612    fn packs_are_detected_from_import_evidence_in_registration_order() {
613        let libs: BTreeSet<String> = ["anthropic", "openai", "requests"]
614            .into_iter()
615            .map(str::to_owned)
616            .collect();
617        let report = build_report(&libs, None);
618        assert_eq!(report.packs, vec!["openai", "anthropic"]);
619        // Fragments are identity over the embedded defaults; provenance says
620        // the packs re-affirmed the llm layer.
621        assert_eq!(report.sources["defaults.llm.cache"], "pack");
622        assert_eq!(report.sources["defaults.outbound.timeout"], "defaults");
623    }
624
625    #[test]
626    fn toml_rendering_quotes_and_sorts() {
627        assert_eq!(toml_value(&json!("a\"b")), "\"a\\\"b\"");
628        assert_eq!(toml_value(&json!([1, "x"])), "[1, \"x\"]");
629        assert_eq!(
630            toml_value(&json!({ "z": 1, "a": { "mode": "dev" } })),
631            "{ a = { mode = \"dev\" }, z = 1 }"
632        );
633        assert_eq!(toml_key("api.example.com"), "\"api.example.com\"");
634        assert_eq!(toml_key("timeout"), "timeout");
635    }
636
637    #[test]
638    fn invalid_policy_is_keel_e001_usage_error() {
639        let dir = tempfile::TempDir::new().unwrap();
640        std::fs::write(
641            dir.path().join("keel.toml"),
642            "[target.\"x\"]\nretry = { attempts = 0 }\n",
643        )
644        .unwrap();
645        let r = run(dir.path());
646        assert_eq!(r.exit, EXIT_USAGE);
647        assert!(r.to_stderr);
648        assert_eq!(r.json["code"], json!("KEEL-E001"));
649        assert_eq!(r.json["field"], json!("target.x.retry.attempts"));
650        assert!(r.human.contains("KEEL-E001"));
651        assert!(r.human.contains("target.x.retry.attempts"));
652    }
653
654    #[test]
655    fn absent_policy_composes_pure_level0() {
656        let dir = tempfile::TempDir::new().unwrap();
657        let r = run(dir.path());
658        assert_eq!(r.exit, EXIT_OK);
659        assert_eq!(r.json["user_policy_present"], json!(false));
660        assert_eq!(
661            r.json["policy"]["defaults"]["llm"]["timeout"],
662            json!("120s")
663        );
664        assert!(r.human.contains("(no keel.toml \u{2014} Level 0 applies)"));
665    }
666
667    /// Every fact in the JSON twin surfaces in the human report: packs, the
668    /// winning-layer annotations, the note.
669    #[test]
670    fn human_carries_the_json_facts() {
671        let libs: BTreeSet<String> = ["openai".to_owned()].into_iter().collect();
672        let user = json!({
673            "defaults": { "llm": { "retry": { "attempts": 2 } } },
674            "target": { "api.example.com": { "retry": { "attempts": 5 } } },
675        });
676        let report = build_report(&libs, Some(user));
677        let text = human(&report);
678        assert!(text.contains("openai"));
679        assert!(text.contains("# user"));
680        assert!(text.contains("# pack"));
681        assert!(text.contains("# defaults"));
682        assert!(text.contains("[target.\"api.example.com\"]"));
683        assert!(text.contains("note: "));
684    }
685}