Skip to content

Commit a502562

Browse files
authored
Merge pull request #265 from egohygiene/copilot/implement-quality-propagation
feat: multiplicative quality normalization and propagation across transform chains
2 parents 913f760 + e697e4b commit a502562

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

src/graph/pathfinding.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,106 @@ impl TransformPath {
4444
Self { steps, total_cost, total_quality }
4545
}
4646
}
47+
48+
#[cfg(test)]
49+
mod tests {
50+
use super::*;
51+
use crate::graph::{Format, TransformEdge};
52+
53+
fn edge(from: Format, to: Format, cost: f32, quality: f32) -> TransformEdge {
54+
TransformEdge::new(from, to, cost, quality)
55+
}
56+
57+
// ── single-step paths ─────────────────────────────────────────────────────
58+
59+
#[test]
60+
fn test_single_step_quality_preserved() {
61+
let steps = vec![edge(Format::Markdown, Format::Html, 1.0, 0.9)];
62+
let path = TransformPath::from_steps(steps);
63+
assert!((path.total_quality - 0.9).abs() < 1e-5,
64+
"single-step quality must equal the edge quality");
65+
}
66+
67+
#[test]
68+
fn test_single_step_cost_preserved() {
69+
let steps = vec![edge(Format::Markdown, Format::Html, 2.5, 1.0)];
70+
let path = TransformPath::from_steps(steps);
71+
assert!((path.total_cost - 2.5).abs() < 1e-5);
72+
}
73+
74+
// ── multi-step quality is multiplicative ──────────────────────────────────
75+
76+
#[test]
77+
fn test_two_step_quality_is_product() {
78+
// 0.9 * 0.8 = 0.72
79+
let steps = vec![
80+
edge(Format::Markdown, Format::Html, 1.0, 0.9),
81+
edge(Format::Html, Format::Pdf, 1.0, 0.8),
82+
];
83+
let path = TransformPath::from_steps(steps);
84+
assert!((path.total_quality - 0.72).abs() < 1e-5,
85+
"two-step quality must be the product of both edge qualities");
86+
}
87+
88+
#[test]
89+
fn test_two_step_cost_is_sum() {
90+
let steps = vec![
91+
edge(Format::Markdown, Format::Html, 0.5, 1.0),
92+
edge(Format::Html, Format::Pdf, 0.8, 0.85),
93+
];
94+
let path = TransformPath::from_steps(steps);
95+
assert!((path.total_cost - 1.3).abs() < 1e-5);
96+
}
97+
98+
#[test]
99+
fn test_three_step_quality_propagates_multiplicatively() {
100+
// 0.9 * 0.8 * 0.7 = 0.504
101+
let steps = vec![
102+
edge(Format::Markdown, Format::Html, 1.0, 0.9),
103+
edge(Format::Html, Format::Rst, 1.0, 0.8),
104+
edge(Format::Rst, Format::Pdf, 1.0, 0.7),
105+
];
106+
let path = TransformPath::from_steps(steps);
107+
assert!((path.total_quality - 0.504_f32).abs() < 1e-5,
108+
"three-step quality must be the product of all three edge qualities");
109+
}
110+
111+
// ── perfect quality path ──────────────────────────────────────────────────
112+
113+
#[test]
114+
fn test_all_quality_one_yields_product_one() {
115+
let steps = vec![
116+
edge(Format::Markdown, Format::Html, 1.0, 1.0),
117+
edge(Format::Html, Format::Pdf, 1.0, 1.0),
118+
];
119+
let path = TransformPath::from_steps(steps);
120+
assert!((path.total_quality - 1.0).abs() < 1e-5,
121+
"product of 1.0 values must equal 1.0");
122+
}
123+
124+
// ── zero quality collapses path quality ───────────────────────────────────
125+
126+
#[test]
127+
fn test_zero_quality_edge_collapses_path_quality() {
128+
let steps = vec![
129+
edge(Format::Markdown, Format::Html, 1.0, 0.9),
130+
edge(Format::Html, Format::Pdf, 1.0, 0.0),
131+
];
132+
let path = TransformPath::from_steps(steps);
133+
assert!((path.total_quality - 0.0).abs() < 1e-5,
134+
"a zero-quality edge must bring total path quality to 0.0");
135+
}
136+
137+
// ── step count ────────────────────────────────────────────────────────────
138+
139+
#[test]
140+
fn test_steps_count_matches_input() {
141+
let steps = vec![
142+
edge(Format::Markdown, Format::Html, 1.0, 1.0),
143+
edge(Format::Html, Format::Pdf, 1.0, 1.0),
144+
edge(Format::Pdf, Format::Epub, 1.0, 1.0),
145+
];
146+
let path = TransformPath::from_steps(steps);
147+
assert_eq!(path.steps.len(), 3);
148+
}
149+
}

src/graph/transform_edge.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ impl TransformEdge {
2626
/// * `from` – source [`Format`]
2727
/// * `to` – target [`Format`]
2828
/// * `cost` – relative execution cost (lower is cheaper)
29-
/// * `quality` – expected output quality in the range `[0.0, 1.0]`
29+
/// * `quality` – expected output quality in the range `[0.0, 1.0]`; values
30+
/// outside this range are clamped automatically.
3031
pub fn new(from: Format, to: Format, cost: f32, quality: f32) -> Self {
31-
Self { from, to, cost, quality }
32+
Self { from, to, cost, quality: quality.clamp(0.0, 1.0) }
3233
}
3334
}
3435

@@ -65,4 +66,24 @@ mod tests {
6566
let b = TransformEdge::new(Format::Markdown, Format::Pdf, 0.5, 1.0);
6667
assert_ne!(a, b);
6768
}
69+
70+
#[test]
71+
fn test_quality_clamped_above_one() {
72+
let edge = TransformEdge::new(Format::Markdown, Format::Html, 1.0, 1.5);
73+
assert!((edge.quality - 1.0).abs() < 1e-5, "quality above 1.0 must be clamped to 1.0");
74+
}
75+
76+
#[test]
77+
fn test_quality_clamped_below_zero() {
78+
let edge = TransformEdge::new(Format::Markdown, Format::Html, 1.0, -0.5);
79+
assert!((edge.quality - 0.0).abs() < 1e-5, "quality below 0.0 must be clamped to 0.0");
80+
}
81+
82+
#[test]
83+
fn test_quality_at_boundaries_not_clamped() {
84+
let edge_zero = TransformEdge::new(Format::Markdown, Format::Html, 1.0, 0.0);
85+
let edge_one = TransformEdge::new(Format::Html, Format::Pdf, 1.0, 1.0);
86+
assert!((edge_zero.quality - 0.0).abs() < 1e-5);
87+
assert!((edge_one.quality - 1.0).abs() < 1e-5);
88+
}
6889
}

0 commit comments

Comments
 (0)