1use crate::area::{areas_conserved, twice_area_fp2};
20use crate::overlap::convex_parts_overlap;
21use crate::topology::validate_multipart_topology;
22use crate::types::{ProtocolConfig, TopologyError};
23use crate::validation::validate_part;
24use serde::{Deserialize, Serialize};
25
26#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct ValidationCheck {
29 pub name: String,
31 pub passed: bool,
33 pub detail: String,
35 pub severity: String,
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize)]
41pub struct ValidationReport {
42 pub checks: Vec<ValidationCheck>,
44 pub valid: bool,
46 pub error_count: usize,
48 pub warn_count: usize,
50 pub original_twice_area: String,
52 pub parts_twice_area_sum: String,
54 pub part_areas: Vec<String>,
56}
57
58fn check_ok(name: impl Into<String>, detail: impl Into<String>) -> ValidationCheck {
59 ValidationCheck {
60 name: name.into(),
61 passed: true,
62 detail: detail.into(),
63 severity: "ok".into(),
64 }
65}
66
67fn check_err(name: impl Into<String>, detail: impl Into<String>) -> ValidationCheck {
68 ValidationCheck {
69 name: name.into(),
70 passed: false,
71 detail: detail.into(),
72 severity: "error".into(),
73 }
74}
75
76fn check_warn(name: impl Into<String>, detail: impl Into<String>) -> ValidationCheck {
77 ValidationCheck {
78 name: name.into(),
79 passed: true,
80 detail: detail.into(),
81 severity: "warn".into(),
82 }
83}
84
85pub fn validate_decomposition(
93 ring: &[[i64; 2]],
94 parts: &[Vec<[i64; 2]>],
95 config: &ProtocolConfig,
96) -> ValidationReport {
97 let mut checks = Vec::new();
98 let mut error_count = 0usize;
99 let mut warn_count = 0usize;
100
101 let mut push = |c: ValidationCheck| {
102 if c.severity == "error" && !c.passed {
103 error_count += 1;
104 } else if c.severity == "warn" {
105 warn_count += 1;
106 }
107 checks.push(c);
108 };
109
110 let n = parts.len();
112 if n == 0 {
113 push(check_err("part_count", "no parts"));
114 } else if n > config.max_parts {
115 push(check_err(
116 "part_count",
117 format!("{n} parts exceeds max {}", config.max_parts),
118 ));
119 } else {
120 push(check_ok(
121 "part_count",
122 format!("{n} (max {})", config.max_parts),
123 ));
124 }
125
126 let mut part_areas: Vec<u128> = Vec::with_capacity(n);
128 let mut total_vertices = 0usize;
129
130 for (i, part) in parts.iter().enumerate() {
131 total_vertices += part.len();
132
133 if part.len() < 3 {
135 push(check_err(
136 format!("part_{i}_vertices"),
137 format!("{} vertices (min 3)", part.len()),
138 ));
139 } else if part.len() > config.max_vertices_per_part {
140 push(check_err(
141 format!("part_{i}_vertices"),
142 format!(
143 "{} vertices (max {})",
144 part.len(),
145 config.max_vertices_per_part
146 ),
147 ));
148 } else {
149 push(check_ok(
150 format!("part_{i}_vertices"),
151 format!("{} (max {})", part.len(), config.max_vertices_per_part),
152 ));
153 }
154
155 match validate_part(part, config) {
158 None => {
159 push(check_ok(format!("part_{i}_valid"), "OK"));
160 }
161 Some(err) => {
162 push(check_err(format!("part_{i}_valid"), err));
163 }
164 }
165
166 let area = twice_area_fp2(part);
168 part_areas.push(area);
169 if area == 0 {
170 push(check_err(format!("part_{i}_area"), "zero area"));
171 } else {
172 push(check_ok(format!("part_{i}_area"), format!("2A = {area}")));
173 }
174
175 let max_world = crate::constants::MAX_WORLD as i64;
177 let out_of_range = part
178 .iter()
179 .flat_map(|vertex| vertex.iter())
180 .any(|&c| c < 0 || c > max_world);
181 if out_of_range {
182 push(check_warn(
185 format!("part_{i}_coords"),
186 "coordinates outside [0, MAX_WORLD] — will fail on-chain",
187 ));
188 }
189 }
190
191 let max_total = config.max_vertices_per_part.saturating_mul(n.max(1));
193 if total_vertices > max_total {
194 push(check_err(
195 "total_vertices",
196 format!("{total_vertices} > max {max_total}"),
197 ));
198 } else {
199 push(check_ok(
200 "total_vertices",
201 format!("{total_vertices} (max {max_total})"),
202 ));
203 }
204
205 let parts_area_sum: u128 = part_areas.iter().sum();
207 if parts_area_sum == 0 {
208 push(check_err("total_area", "zero total area"));
209 } else {
210 push(check_ok("total_area", format!("2A = {parts_area_sum}")));
211 }
212
213 let original_area = twice_area_fp2(ring);
215 if areas_conserved(original_area, &part_areas) {
216 push(check_ok(
217 "area_conservation",
218 format!("original={original_area}, sum={parts_area_sum}"),
219 ));
220 } else {
221 let diff = if original_area > parts_area_sum {
222 original_area - parts_area_sum
223 } else {
224 parts_area_sum - original_area
225 };
226 push(check_err(
227 "area_conservation",
228 format!("MISMATCH: original={original_area}, sum={parts_area_sum}, diff={diff}"),
229 ));
230 }
231
232 if n > 1 {
234 let mut has_overlap = false;
235 for i in 0..n {
236 for j in (i + 1)..n {
237 if convex_parts_overlap(&parts[i], &parts[j]) {
238 push(check_err(
239 format!("parts_overlap_{i}_{j}"),
240 format!("parts {i} and {j} overlap"),
241 ));
242 has_overlap = true;
243 }
244 }
245 }
246 if !has_overlap {
247 push(check_ok("parts_no_overlap", "no pairwise overlap"));
248 }
249 }
250
251 if n > 0 {
253 match validate_multipart_topology(parts, false, config) {
254 Ok(()) => {
255 push(check_ok("topology", "valid"));
256 }
257 Err(topo_err) => {
258 push(check_err("topology", format_topology_error(&topo_err)));
259 }
260 }
261 }
262
263 let valid = error_count == 0;
264 ValidationReport {
265 checks,
266 valid,
267 error_count,
268 warn_count,
269 original_twice_area: original_area.to_string(),
270 parts_twice_area_sum: parts_area_sum.to_string(),
271 part_areas: part_areas.iter().map(|a| a.to_string()).collect(),
272 }
273}
274
275fn format_topology_error(err: &TopologyError) -> String {
276 match err {
277 TopologyError::NotConnected { disconnected_parts } => {
278 format!("not connected (disconnected: {disconnected_parts:?})")
279 }
280 TopologyError::HasHoles {
281 boundary_components,
282 } => {
283 format!("has holes ({boundary_components} boundary components)")
284 }
285 TopologyError::VertexOnlyContact { part_a, part_b } => {
286 format!("vertex-only contact between parts {part_a} and {part_b}")
287 }
288 TopologyError::UnsupportedContact {
289 part_a,
290 part_b,
291 reason,
292 } => {
293 format!("unsupported contact {part_a}↔{part_b}: {reason}")
294 }
295 TopologyError::TooManyParts { count, max } => {
296 format!("too many parts ({count} > {max})")
297 }
298 TopologyError::NotCompact {
299 compactness_ppm,
300 min_ppm,
301 } => {
302 format!("not compact ({compactness_ppm} ppm < {min_ppm} ppm)")
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use crate::types::ProtocolConfig;
311
312 const M: i64 = 1_000_000;
313
314 fn merca() -> ProtocolConfig {
315 ProtocolConfig::merca()
316 }
317
318 fn permissive() -> ProtocolConfig {
319 ProtocolConfig::permissive()
320 }
321
322 #[test]
323 fn valid_single_convex_part() {
324 let ring = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
325 let parts = vec![ring.clone()];
326 let report = validate_decomposition(&ring, &parts, &merca());
327 assert!(
328 report.valid,
329 "errors: {:?}",
330 report
331 .checks
332 .iter()
333 .filter(|c| !c.passed)
334 .collect::<Vec<_>>()
335 );
336 }
337
338 #[test]
339 fn valid_two_part_l_shape() {
340 let ring = vec![
341 [0, 0],
342 [20 * M, 0],
343 [20 * M, 10 * M],
344 [10 * M, 10 * M],
345 [10 * M, 20 * M],
346 [0, 20 * M],
347 ];
348 let parts = vec![
349 vec![
350 [0, 0],
351 [20 * M, 0],
352 [20 * M, 10 * M],
353 [10 * M, 10 * M],
354 [0, 10 * M],
355 ],
356 vec![[0, 10 * M], [10 * M, 10 * M], [10 * M, 20 * M], [0, 20 * M]],
357 ];
358 let report = validate_decomposition(&ring, &parts, &merca());
359 assert!(
360 report.valid,
361 "errors: {:?}",
362 report
363 .checks
364 .iter()
365 .filter(|c| !c.passed)
366 .collect::<Vec<_>>()
367 );
368 }
369
370 #[test]
371 fn valid_two_part_l_shape_permissive_config() {
372 let ring = vec![
373 [0, 0],
374 [20 * M, 0],
375 [20 * M, 10 * M],
376 [10 * M, 10 * M],
377 [10 * M, 20 * M],
378 [0, 20 * M],
379 ];
380 let parts = vec![
381 vec![
382 [0, 0],
383 [20 * M, 0],
384 [20 * M, 10 * M],
385 [10 * M, 10 * M],
386 [0, 10 * M],
387 ],
388 vec![[0, 10 * M], [10 * M, 10 * M], [10 * M, 20 * M], [0, 20 * M]],
389 ];
390 let report = validate_decomposition(&ring, &parts, &permissive());
391 assert!(
392 report.valid,
393 "errors: {:?}",
394 report
395 .checks
396 .iter()
397 .filter(|c| !c.passed)
398 .collect::<Vec<_>>()
399 );
400 }
401
402 #[test]
403 fn rejects_overlapping_parts() {
404 let ring = vec![[0, 0], [20 * M, 0], [20 * M, 20 * M], [0, 20 * M]];
405 let parts = vec![
407 vec![[0, 0], [15 * M, 0], [15 * M, 15 * M], [0, 15 * M]],
408 vec![
409 [5 * M, 5 * M],
410 [20 * M, 5 * M],
411 [20 * M, 20 * M],
412 [5 * M, 20 * M],
413 ],
414 ];
415 let report = validate_decomposition(&ring, &parts, &merca());
416 assert!(!report.valid);
417 assert!(report
418 .checks
419 .iter()
420 .any(|c| c.name.starts_with("parts_overlap")));
421 }
422
423 #[test]
424 fn rejects_overlapping_parts_permissive_config() {
425 let ring = vec![[0, 0], [20 * M, 0], [20 * M, 20 * M], [0, 20 * M]];
426 let parts = vec![
427 vec![[0, 0], [15 * M, 0], [15 * M, 15 * M], [0, 15 * M]],
428 vec![
429 [5 * M, 5 * M],
430 [20 * M, 5 * M],
431 [20 * M, 20 * M],
432 [5 * M, 20 * M],
433 ],
434 ];
435 let report = validate_decomposition(&ring, &parts, &permissive());
436 assert!(!report.valid);
437 assert!(report
438 .checks
439 .iter()
440 .any(|c| c.name == "parts_overlap_0_1" && !c.passed));
441 }
442
443 #[test]
444 fn rejects_area_mismatch() {
445 let ring = vec![[0, 0], [10 * M, 0], [10 * M, 10 * M], [0, 10 * M]];
446 let parts = vec![vec![[0, 0], [5 * M, 0], [5 * M, 5 * M], [0, 5 * M]]];
448 let report = validate_decomposition(&ring, &parts, &merca());
449 assert!(!report.valid);
450 assert!(report
451 .checks
452 .iter()
453 .any(|c| c.name == "area_conservation" && !c.passed));
454 }
455
456 #[test]
457 fn rejects_area_mismatch_with_connected_parts() {
458 let ring = vec![[0, 0], [20 * M, 0], [20 * M, 20 * M], [0, 20 * M]];
459 let parts = vec![
460 vec![[0, 0], [20 * M, 0], [20 * M, 10 * M], [0, 10 * M]],
461 vec![[0, 10 * M], [10 * M, 10 * M], [10 * M, 20 * M], [0, 20 * M]],
462 ];
463 let report = validate_decomposition(&ring, &parts, &permissive());
464 assert!(!report.valid);
465 assert!(report
466 .checks
467 .iter()
468 .any(|c| c.name == "area_conservation" && !c.passed));
469 }
470
471 #[test]
472 fn warns_negative_coords() {
473 let ring = vec![
474 [-5 * M, -5 * M],
475 [5 * M, -5 * M],
476 [5 * M, 5 * M],
477 [-5 * M, 5 * M],
478 ];
479 let parts = vec![ring.clone()];
480 let report = validate_decomposition(&ring, &parts, &merca());
481 assert!(report
482 .checks
483 .iter()
484 .any(|c| c.name.contains("coords") && c.severity == "warn"));
485 }
486}