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
//! Shared frontmatter key/value line parser.
//!
//! Why: Two independent copies of a `split_once(':')` frontmatter parser
//! existed in `agent_builder` and `delegation_authority`; both silently
//! corrupted or hard-errored on values that legitimately contain a colon
//! (e.g. URLs, ISO-8601 timestamps, model ids like
//! `bedrock/us.anthropic.claude-sonnet-4-6`). A single shared function
//! eliminates the divergence and fixes the bug for both call sites.
//! What: [`parse_kv_line`] splits a frontmatter line on the *first* colon
//! only, trims the key and value, strips optional surrounding quotes from the
//! value, and returns `Some((key, value))`; it returns `None` for blank lines,
//! comment lines, and YAML fence markers (`---`).
//! Test: `cargo test -p trusty-mpm -- frontmatter` covers URL values,
//! timestamp values, model-id values, normal key/value pairs, trailing
//! whitespace, and malformed/comment/empty lines.
/// Parse one frontmatter line into a `(key, value)` pair.
///
/// Why: values in YAML-ish frontmatter often contain colons — URLs, timestamps,
/// model ids — so splitting on the first colon and preserving the rest is the
/// only correct interpretation.
/// What: splits `line` on the first `':'`, lower-cases and trims the key, trims
/// the value and strips one layer of surrounding `"` or `'` quotes. Returns
/// `None` when the line is blank, a comment (`#`), a fence (`---`), or has no
/// colon at all.
/// Test: `frontmatter::tests::parse_kv_*` in this file.
pub fn parse_kv_line(line: &str) -> Option<(String, String)> {
let trimmed = line.trim();
// Skip fences, blank lines, and YAML comments.
if trimmed.is_empty() || trimmed == "---" || trimmed.starts_with('#') {
return None;
}
// Split on the FIRST colon only; everything after belongs to the value.
let (raw_key, raw_value) = trimmed.split_once(':')?;
let key = raw_key.trim().to_ascii_lowercase();
// A key must be a non-empty identifier; guard against lines like
// `https://...` that have a colon in an unexpected position.
if key.is_empty() {
return None;
}
let value = raw_value
.trim()
.trim_matches(|c| c == '"' || c == '\'')
.to_string();
Some((key, value))
}
#[cfg(test)]
mod tests {
use super::*;
// ── happy-path cases ─────────────────────────────────────────────────────
#[test]
fn parse_kv_normal() {
// A plain `key: value` must round-trip cleanly.
let (k, v) = parse_kv_line("name: engineer").unwrap();
assert_eq!(k, "name");
assert_eq!(v, "engineer");
}
#[test]
fn parse_kv_url_value() {
// A URL value contains multiple colons; only the first should be the
// key/value separator — the rest must be preserved verbatim.
let (k, v) = parse_kv_line("repo: https://github.com/x/y").unwrap();
assert_eq!(k, "repo");
assert_eq!(v, "https://github.com/x/y");
}
#[test]
fn parse_kv_timestamp_value() {
// ISO-8601 timestamps contain colons in the time component.
let (k, v) = parse_kv_line("created: 2026-06-05T14:31:34").unwrap();
assert_eq!(k, "created");
assert_eq!(v, "2026-06-05T14:31:34");
}
#[test]
fn parse_kv_model_id_value() {
// Model ids like `bedrock/us.anthropic.claude-sonnet-4-6` contain
// slashes and dots but no colon in the value — still must parse cleanly.
let (k, v) = parse_kv_line("model: bedrock/us.anthropic.claude-sonnet-4-6").unwrap();
assert_eq!(k, "model");
assert_eq!(v, "bedrock/us.anthropic.claude-sonnet-4-6");
}
#[test]
fn parse_kv_model_id_with_colon_in_value() {
// A hypothetical model id that actually contains a colon (e.g.
// `openrouter:gpt-4`) must preserve everything after the first colon.
let (k, v) = parse_kv_line("model: openrouter:gpt-4").unwrap();
assert_eq!(k, "model");
assert_eq!(v, "openrouter:gpt-4");
}
#[test]
fn parse_kv_strips_trailing_whitespace() {
// Trailing spaces on both key and value must be trimmed.
let (k, v) = parse_kv_line(" role : engineer ").unwrap();
assert_eq!(k, "role");
assert_eq!(v, "engineer");
}
#[test]
fn parse_kv_strips_double_quotes() {
// Values wrapped in double-quotes must have the outer quotes removed.
let (k, v) = parse_kv_line(r#"description: "Implements features.""#).unwrap();
assert_eq!(k, "description");
assert_eq!(v, "Implements features.");
}
#[test]
fn parse_kv_strips_single_quotes() {
// Values wrapped in single-quotes must have the outer quotes removed.
let (k, v) = parse_kv_line("description: 'Implements features.'").unwrap();
assert_eq!(k, "description");
assert_eq!(v, "Implements features.");
}
#[test]
fn parse_kv_key_is_lower_cased() {
// Keys are normalised to lower-case so lookups are case-insensitive.
let (k, _) = parse_kv_line("Name: foo").unwrap();
assert_eq!(k, "name");
}
// ── none / skip cases ─────────────────────────────────────────────────────
#[test]
fn parse_kv_empty_line_returns_none() {
// Blank lines must be skipped — they carry no key/value data.
assert!(parse_kv_line("").is_none());
assert!(parse_kv_line(" ").is_none());
}
#[test]
fn parse_kv_fence_returns_none() {
// YAML fence markers must be skipped, not parsed as key/value pairs.
assert!(parse_kv_line("---").is_none());
assert!(parse_kv_line(" --- ").is_none());
}
#[test]
fn parse_kv_comment_returns_none() {
// YAML comment lines must be skipped.
assert!(parse_kv_line("# this is a comment").is_none());
assert!(parse_kv_line(" # indented comment").is_none());
}
#[test]
fn parse_kv_malformed_no_colon_returns_none() {
// A line with no colon at all cannot be split and must return None.
assert!(parse_kv_line("not_a_kv_line").is_none());
}
}