1use std::collections::HashSet;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use serde::Deserialize;
10use thiserror::Error;
11
12pub const ALLOWED_CATEGORIES: &[&str] = &[
17 "python_namespace",
18 "cli_option",
19 "protocol",
20 "cipher",
21 "composition",
22 "process",
23 "failure",
24];
25
26pub const ALLOWED_STATUSES: &[&str] = &[
27 "gap",
28 "drop_in",
29 "structural",
30 "known_upstream_defect",
31 "platform_constraint",
32 "not_applicable",
33 "intentional_non_parity",
34];
35
36pub const ALLOWED_COMPARATORS: &[&str] = &[
37 "async_callable_signature",
38 "module_existence",
39 "constant_value",
40 "enum_membership",
41 "method_signature",
42 "property_existence",
43 "class_hierarchy",
44 "cli_flag_parse",
45 "cli_flag_rejection",
46 "protocol_wire",
47 "cipher_roundtrip",
48 "cipher_kat",
49 "process_lifecycle",
50 "failure_class",
51 "composition_validity",
52 "composition_rejection",
53];
54
55pub const ALLOWED_OWNERS: &[&str] = &["track-a", "track-b", "track-c"];
56
57pub const ALLOWED_MILESTONES: &[&str] = &["A", "B", "C", "D", "E", "F"];
58
59pub const ALLOWED_EVIDENCE_LEVELS: &[&str] = &[
60 "paired_oracle",
61 "bidirectional_interop",
62 "oracle_only_baseline",
63 "candidate_only",
64 "structural_only",
65 "none",
66];
67
68pub const ALLOWED_IMPLEMENTATION_STATES: &[&str] =
69 &["functional", "structural", "partial", "absent"];
70
71pub const ALLOWED_CERTIFICATION_SCOPES: &[&str] =
72 &["structural", "behavioral", "interop", "process", "platform"];
73
74const MILESTONE_ORDER: &[&str] = &["A", "B", "C", "D", "E", "F"];
76
77const CURRENT_MILESTONE: &str = "C";
81
82const TERMINAL_STATUSES: &[&str] = &[
84 "drop_in",
85 "not_applicable",
86 "known_upstream_defect",
87 "platform_constraint",
88 "intentional_non_parity",
89 "structural",
90];
91
92#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
98pub struct StrictManifestMeta {
99 pub manifest_version: String,
100 pub pproxy_version: String,
101 pub schema: String,
102 #[serde(default)]
103 pub policy_ref: String,
104 #[serde(default)]
105 pub oracle_ref: String,
106 #[serde(default)]
107 pub closure_through: String,
108}
109
110#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
112pub struct StrictRecord {
113 pub id: String,
114 pub category: String,
115 #[serde(default)]
116 pub kind: String,
117 #[serde(default)]
118 pub module: String,
119 #[serde(default)]
120 pub name: String,
121 #[serde(default)]
122 pub oracle_probe: String,
123 #[serde(default)]
124 pub candidate_probe: String,
125 #[serde(default)]
126 pub comparator: String,
127 pub status: String,
128 #[serde(default)]
129 pub owner: String,
130 #[serde(default)]
131 pub milestone: String,
132 #[serde(default)]
133 pub platforms: Vec<String>,
134 #[serde(default)]
135 pub python_versions: Vec<String>,
136 #[serde(default)]
137 pub depends_on: Vec<String>,
138 #[serde(default)]
139 pub test_refs: Vec<String>,
140 #[serde(default)]
141 pub evidence_refs: Vec<String>,
142 #[serde(default)]
143 pub notes: String,
144 #[serde(default)]
145 pub evidence_level: String,
146 #[serde(default)]
147 pub implementation_state: String,
148 #[serde(default = "default_certification_scope")]
149 pub certification_scope: String,
150 #[serde(default = "default_closure_required")]
151 pub closure_required: bool,
152 #[serde(default)]
153 pub behavior_record: Option<String>,
154 #[serde(default)]
155 pub inventory_only: bool,
156}
157
158#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
160pub struct StrictManifest {
161 pub meta: StrictManifestMeta,
162 pub record: Vec<StrictRecord>,
163}
164
165#[derive(Debug, Clone, Error, PartialEq, Eq)]
171pub enum StrictValidationError {
172 #[error("TOML parse error: {message}")]
173 TomlParse { message: String },
174
175 #[error("file I/O error: {message}")]
176 Io { message: String },
177
178 #[error("unknown category \"{value}\" (valid: {allowed:?})")]
180 UnknownCategory { value: String, allowed: Vec<String> },
181
182 #[error("unknown status \"{value}\" (valid: {allowed:?})")]
183 UnknownStatus { value: String, allowed: Vec<String> },
184
185 #[error("unknown comparator \"{value}\" (valid: {allowed:?})")]
186 UnknownComparator { value: String, allowed: Vec<String> },
187
188 #[error("unknown owner \"{value}\" (valid: {allowed:?})")]
189 UnknownOwner { value: String, allowed: Vec<String> },
190
191 #[error("unknown milestone \"{value}\" (valid: {allowed:?})")]
192 UnknownMilestone { value: String, allowed: Vec<String> },
193
194 #[error("duplicate record id: \"{id}\"")]
196 DuplicateId { id: String },
197
198 #[error("record has empty id")]
200 EmptyId,
201
202 #[error("drop_in record \"{id}\" has empty evidence_refs and test_refs")]
204 DropInWithoutEvidence { id: String },
205
206 #[error("drop_in record \"{id}\" has empty oracle_probe")]
208 DropInWithoutOracleProbe { id: String },
209
210 #[error(
212 "record \"{id}\" has non-terminal status \"{status}\" at milestone \"{milestone}\" \
213 (at or below current milestone {current})"
214 )]
215 UnresolvedProgress {
216 id: String,
217 status: String,
218 milestone: String,
219 current: String,
220 },
221
222 #[error(
224 "drop_in record \"{id}\" has evidence_level \"{evidence_level}\" \
225 (must be paired_oracle or bidirectional_interop)"
226 )]
227 DropInRequiresStrongEvidence { id: String, evidence_level: String },
228
229 #[error(
231 "drop_in record \"{id}\" with comparator \"{comparator}\" has \
232 evidence_level \"{evidence_level}\" (must not be structural_only or none)"
233 )]
234 StructuralComparatorDropInRequiresEvidence {
235 id: String,
236 comparator: String,
237 evidence_level: String,
238 },
239
240 #[error(
242 "record \"{id}\" has evidence_level \"structural_only\" but status \"drop_in\" \
243 (structural_only evidence is incompatible with drop_in)"
244 )]
245 StructuralOnlyIncompatibleWithDropIn { id: String },
246
247 #[error(
249 "record \"{id}\" has status \"drop_in\" with structural comparator \"{comparator}\" \
250 but certification_scope is \"behavioral\" (structural evidence cannot certify behavior)"
251 )]
252 StructuralComparatorBehavioralScopeMismatch { id: String, comparator: String },
253
254 #[error(
256 "record \"{id}\" has closure_required = true but empty evidence_refs \
257 (closure-required records must have evidence)"
258 )]
259 ClosureRequiredWithoutEvidence { id: String },
260
261 #[error(
263 "record \"{id}\" has certification_scope = \"structural\" but missing behavior_record \
264 (structural records must reference a behavior record)"
265 )]
266 StructuralMissingBehaviorRecord { id: String },
267
268 #[error("unknown certification_scope \"{value}\" (valid: {allowed:?})")]
270 UnknownCertificationScope { value: String, allowed: Vec<String> },
271 #[error("unknown evidence_level \"{value}\" (valid: {allowed:?})")]
272 UnknownEvidenceLevel { value: String, allowed: Vec<String> },
273
274 #[error("unknown implementation_state \"{value}\" (valid: {allowed:?})")]
275 UnknownImplementationState { value: String, allowed: Vec<String> },
276}
277
278#[derive(Debug, Clone, Error, PartialEq, Eq)]
280#[error("{errors:#?}")]
281pub struct StrictValidationErrors {
282 pub errors: Vec<StrictValidationError>,
283}
284
285impl StrictValidationErrors {
286 pub fn new() -> Self {
287 Self { errors: Vec::new() }
288 }
289
290 pub fn push(&mut self, err: StrictValidationError) {
291 self.errors.push(err);
292 }
293
294 pub fn is_empty(&self) -> bool {
295 self.errors.is_empty()
296 }
297
298 pub fn len(&self) -> usize {
299 self.errors.len()
300 }
301}
302
303impl Default for StrictValidationErrors {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309fn default_certification_scope() -> String {
314 "behavioral".to_string()
315}
316
317fn default_closure_required() -> bool {
318 true
319}
320
321fn milestone_index(milestone: &str) -> Option<usize> {
322 MILESTONE_ORDER.iter().position(|&m| m == milestone)
323}
324
325pub fn find_strict_manifest_path() -> Option<PathBuf> {
327 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
328 let candidate = PathBuf::from(&manifest_dir)
329 .join("../../docs/parity/pproxy_2_7_9_strict_manifest.toml");
330 if candidate.exists() {
331 return Some(candidate);
332 }
333 }
334
335 let cwd = std::env::current_dir().ok()?;
336 let mut dir = cwd.as_path();
337 loop {
338 let candidate = dir.join("docs/parity/pproxy_2_7_9_strict_manifest.toml");
339 if candidate.exists() {
340 return Some(candidate);
341 }
342 dir = dir.parent()?;
343 }
344}
345
346#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
352pub struct OracleHashes {
353 pub package: OracleHashPackage,
354}
355
356#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
357pub struct OracleHashPackage {
358 pub version: String,
359 pub source: String,
360 pub sha256_sdist: Option<String>,
361 pub sha256_wheel: Option<String>,
362}
363
364#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
366pub struct OracleProvenance {
367 pub oracle: OracleProvenanceInner,
368}
369
370#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
371pub struct OracleProvenanceInner {
372 pub package: String,
373 pub version: String,
374 pub source: String,
375 pub license: String,
376 pub retrieval_date: String,
377}
378
379#[derive(Debug, Clone, Error, PartialEq, Eq)]
381pub enum OracleError {
382 #[error("file I/O error: {message}")]
383 Io { message: String },
384
385 #[error("TOML parse error: {message}")]
386 TomlParse { message: String },
387
388 #[error("expected pproxy version \"{expected}\" but found \"{found}\"")]
389 VersionMismatch { expected: String, found: String },
390
391 #[error("expected pproxy package \"{expected}\" but found \"{found}\"")]
392 PackageMismatch { expected: String, found: String },
393
394 #[error("wheel hash mismatch: expected \"{expected}\" but computed \"{found}\"")]
395 WheelHashMismatch { expected: String, found: String },
396
397 #[error("no wheel hash recorded in hashes.toml")]
398 MissingWheelHash,
399
400 #[error("hash computation failed: {message}")]
401 HashComputationFailed { message: String },
402}
403
404pub fn load_oracle_hashes(manifest_dir: &Path) -> Result<OracleHashes, OracleError> {
406 let path = manifest_dir.join("compat/pproxy-2.7.9/hashes.toml");
407 let content = fs::read_to_string(&path).map_err(|e| OracleError::Io {
408 message: format!("failed to read {}: {}", path.display(), e),
409 })?;
410 let hashes: OracleHashes = toml::from_str(&content).map_err(|e| OracleError::TomlParse {
411 message: e.to_string(),
412 })?;
413 Ok(hashes)
414}
415
416pub fn load_oracle_provenance(manifest_dir: &Path) -> Result<OracleProvenance, OracleError> {
418 let path = manifest_dir.join("compat/pproxy-2.7.9/provenance.toml");
419 let content = fs::read_to_string(&path).map_err(|e| OracleError::Io {
420 message: format!("failed to read {}: {}", path.display(), e),
421 })?;
422 let prov: OracleProvenance = toml::from_str(&content).map_err(|e| OracleError::TomlParse {
423 message: e.to_string(),
424 })?;
425 Ok(prov)
426}
427
428pub fn verify_manifest_oracle_version(
430 manifest: &StrictManifest,
431 provenance: &OracleProvenance,
432) -> Result<(), OracleError> {
433 if manifest.meta.pproxy_version != provenance.oracle.version {
434 return Err(OracleError::VersionMismatch {
435 expected: provenance.oracle.version.clone(),
436 found: manifest.meta.pproxy_version.clone(),
437 });
438 }
439 if provenance.oracle.package != "pproxy" {
440 return Err(OracleError::PackageMismatch {
441 expected: "pproxy".to_string(),
442 found: provenance.oracle.package.clone(),
443 });
444 }
445 Ok(())
446}
447
448pub fn verify_wheel_hash(_wheel_path: &Path, _expected_hash: &str) -> Result<(), OracleError> {
453 Err(OracleError::HashComputationFailed {
458 message: "sha2 feature required for hash verification; use oracle runner at runtime"
459 .to_string(),
460 })
461}
462
463pub fn validate_strict_manifest(manifest: &StrictManifest) -> Result<(), StrictValidationErrors> {
472 let mut errs = StrictValidationErrors::new();
473
474 let mut seen_ids = HashSet::new();
476 for rec in &manifest.record {
477 if !seen_ids.insert(rec.id.clone()) {
478 errs.push(StrictValidationError::DuplicateId { id: rec.id.clone() });
479 }
480 }
481
482 for rec in &manifest.record {
484 if rec.id.is_empty() {
486 errs.push(StrictValidationError::EmptyId);
487 continue;
488 }
489
490 if !rec.category.is_empty() && !ALLOWED_CATEGORIES.contains(&rec.category.as_str()) {
492 errs.push(StrictValidationError::UnknownCategory {
493 value: rec.category.clone(),
494 allowed: ALLOWED_CATEGORIES.iter().map(|s| s.to_string()).collect(),
495 });
496 }
497
498 if !rec.status.is_empty() && !ALLOWED_STATUSES.contains(&rec.status.as_str()) {
500 errs.push(StrictValidationError::UnknownStatus {
501 value: rec.status.clone(),
502 allowed: ALLOWED_STATUSES.iter().map(|s| s.to_string()).collect(),
503 });
504 }
505
506 if !rec.comparator.is_empty() && !ALLOWED_COMPARATORS.contains(&rec.comparator.as_str()) {
508 errs.push(StrictValidationError::UnknownComparator {
509 value: rec.comparator.clone(),
510 allowed: ALLOWED_COMPARATORS.iter().map(|s| s.to_string()).collect(),
511 });
512 }
513
514 if !rec.owner.is_empty() && !ALLOWED_OWNERS.contains(&rec.owner.as_str()) {
516 errs.push(StrictValidationError::UnknownOwner {
517 value: rec.owner.clone(),
518 allowed: ALLOWED_OWNERS.iter().map(|s| s.to_string()).collect(),
519 });
520 }
521
522 if !rec.milestone.is_empty() && !ALLOWED_MILESTONES.contains(&rec.milestone.as_str()) {
524 errs.push(StrictValidationError::UnknownMilestone {
525 value: rec.milestone.clone(),
526 allowed: ALLOWED_MILESTONES.iter().map(|s| s.to_string()).collect(),
527 });
528 }
529
530 if !rec.evidence_level.is_empty()
532 && !ALLOWED_EVIDENCE_LEVELS.contains(&rec.evidence_level.as_str())
533 {
534 errs.push(StrictValidationError::UnknownEvidenceLevel {
535 value: rec.evidence_level.clone(),
536 allowed: ALLOWED_EVIDENCE_LEVELS
537 .iter()
538 .map(|s| s.to_string())
539 .collect(),
540 });
541 }
542
543 if !rec.implementation_state.is_empty()
545 && !ALLOWED_IMPLEMENTATION_STATES.contains(&rec.implementation_state.as_str())
546 {
547 errs.push(StrictValidationError::UnknownImplementationState {
548 value: rec.implementation_state.clone(),
549 allowed: ALLOWED_IMPLEMENTATION_STATES
550 .iter()
551 .map(|s| s.to_string())
552 .collect(),
553 });
554 }
555
556 if rec.status == "drop_in" && rec.evidence_refs.is_empty() && rec.test_refs.is_empty() {
558 errs.push(StrictValidationError::DropInWithoutEvidence { id: rec.id.clone() });
559 }
560
561 if rec.status == "drop_in" && rec.oracle_probe.is_empty() {
563 errs.push(StrictValidationError::DropInWithoutOracleProbe { id: rec.id.clone() });
564 }
565
566 if let Some(rec_idx) = milestone_index(&rec.milestone) {
568 if let Some(cur_idx) = milestone_index(CURRENT_MILESTONE) {
569 if rec_idx <= cur_idx && !TERMINAL_STATUSES.contains(&rec.status.as_str()) {
570 errs.push(StrictValidationError::UnresolvedProgress {
571 id: rec.id.clone(),
572 status: rec.status.clone(),
573 milestone: rec.milestone.clone(),
574 current: CURRENT_MILESTONE.to_string(),
575 });
576 }
577 }
578 }
579
580 if rec.status == "drop_in"
582 && !rec.evidence_level.is_empty()
583 && rec.evidence_level != "paired_oracle"
584 && rec.evidence_level != "bidirectional_interop"
585 {
586 errs.push(StrictValidationError::DropInRequiresStrongEvidence {
587 id: rec.id.clone(),
588 evidence_level: rec.evidence_level.clone(),
589 });
590 }
591
592 let is_structural_comparator = matches!(
597 rec.comparator.as_str(),
598 "module_existence"
599 | "constant_value"
600 | "enum_membership"
601 | "method_signature"
602 | "property_existence"
603 | "class_hierarchy"
604 );
605 if rec.status == "drop_in"
606 && is_structural_comparator
607 && (rec.evidence_level == "structural_only" || rec.evidence_level == "none")
608 {
609 errs.push(
610 StrictValidationError::StructuralComparatorDropInRequiresEvidence {
611 id: rec.id.clone(),
612 comparator: rec.comparator.clone(),
613 evidence_level: rec.evidence_level.clone(),
614 },
615 );
616 }
617
618 if rec.evidence_level == "structural_only" && rec.status == "drop_in" {
620 errs.push(
621 StrictValidationError::StructuralOnlyIncompatibleWithDropIn { id: rec.id.clone() },
622 );
623 }
624
625 if !rec.certification_scope.is_empty()
627 && !ALLOWED_CERTIFICATION_SCOPES.contains(&rec.certification_scope.as_str())
628 {
629 errs.push(StrictValidationError::UnknownCertificationScope {
630 value: rec.certification_scope.clone(),
631 allowed: ALLOWED_CERTIFICATION_SCOPES
632 .iter()
633 .map(|s| s.to_string())
634 .collect(),
635 });
636 }
637
638 if rec.status == "drop_in"
640 && rec.certification_scope == "behavioral"
641 && is_structural_comparator
642 {
643 errs.push(
644 StrictValidationError::StructuralComparatorBehavioralScopeMismatch {
645 id: rec.id.clone(),
646 comparator: rec.comparator.clone(),
647 },
648 );
649 }
650
651 if rec.closure_required && rec.evidence_refs.is_empty() {
653 errs.push(StrictValidationError::ClosureRequiredWithoutEvidence { id: rec.id.clone() });
654 }
655
656 if rec.closure_required
660 && rec.certification_scope == "structural"
661 && rec.behavior_record.is_none()
662 && !rec.inventory_only
663 {
664 errs.push(StrictValidationError::StructuralMissingBehaviorRecord {
665 id: rec.id.clone(),
666 });
667 }
668 }
669
670 if errs.is_empty() {
671 Ok(())
672 } else {
673 Err(errs)
674 }
675}
676
677pub fn validate_strict_manifest_file(
679 path: &Path,
680) -> Result<StrictManifest, StrictValidationErrors> {
681 let content = fs::read_to_string(path).map_err(|e| {
682 let mut errs = StrictValidationErrors::new();
683 errs.push(StrictValidationError::Io {
684 message: format!("failed to read {}: {}", path.display(), e),
685 });
686 errs
687 })?;
688
689 let manifest: StrictManifest = toml::from_str(&content).map_err(|e| {
690 let mut errs = StrictValidationErrors::new();
691 errs.push(StrictValidationError::TomlParse {
692 message: e.to_string(),
693 });
694 errs
695 })?;
696
697 validate_strict_manifest(&manifest)?;
698 Ok(manifest)
699}
700
701#[derive(Debug, Clone)]
707pub struct StrictManifestSummary {
708 pub total: usize,
709 pub by_status: Vec<(String, usize)>,
710 pub by_category: Vec<(String, usize)>,
711 pub by_owner: Vec<(String, usize)>,
712 pub by_milestone: Vec<(String, usize)>,
713 pub by_certification_scope: Vec<(String, usize)>,
714 pub terminal_count: usize,
715 pub gap_count: usize,
716}
717
718pub fn summarize_manifest(manifest: &StrictManifest) -> StrictManifestSummary {
720 use std::collections::HashMap;
721
722 let mut status_counts: HashMap<String, usize> = HashMap::new();
723 let mut category_counts: HashMap<String, usize> = HashMap::new();
724 let mut owner_counts: HashMap<String, usize> = HashMap::new();
725 let mut milestone_counts: HashMap<String, usize> = HashMap::new();
726 let mut scope_counts: HashMap<String, usize> = HashMap::new();
727 let mut terminal = 0;
728 let mut gaps = 0;
729
730 for rec in &manifest.record {
731 *status_counts.entry(rec.status.clone()).or_insert(0) += 1;
732 *category_counts.entry(rec.category.clone()).or_insert(0) += 1;
733 if !rec.owner.is_empty() {
734 *owner_counts.entry(rec.owner.clone()).or_insert(0) += 1;
735 }
736 if !rec.milestone.is_empty() {
737 *milestone_counts.entry(rec.milestone.clone()).or_insert(0) += 1;
738 }
739 *scope_counts
740 .entry(rec.certification_scope.clone())
741 .or_insert(0) += 1;
742 if TERMINAL_STATUSES.contains(&rec.status.as_str()) {
743 terminal += 1;
744 } else {
745 gaps += 1;
746 }
747 }
748
749 let mut by_status: Vec<_> = status_counts.into_iter().collect();
750 by_status.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
751 let mut by_category: Vec<_> = category_counts.into_iter().collect();
752 by_category.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
753 let mut by_owner: Vec<_> = owner_counts.into_iter().collect();
754 by_owner.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
755 let mut by_milestone: Vec<_> = milestone_counts.into_iter().collect();
756 by_milestone.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
757 let mut by_certification_scope: Vec<_> = scope_counts.into_iter().collect();
758 by_certification_scope.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
759
760 StrictManifestSummary {
761 total: manifest.record.len(),
762 by_status,
763 by_category,
764 by_owner,
765 by_milestone,
766 by_certification_scope,
767 terminal_count: terminal,
768 gap_count: gaps,
769 }
770}
771
772pub fn generate_strict_report(manifest: &StrictManifest) -> String {
774 let summary = summarize_manifest(manifest);
775 let mut out = String::with_capacity(4096);
776
777 out.push_str("# pproxy 2.7.9 Strict Compatibility Report\n\n");
778 out.push_str(&format!(
779 "**Oracle version:** pproxy=={}\n",
780 manifest.meta.pproxy_version
781 ));
782 out.push_str(&format!("**Manifest schema:** {}\n", manifest.meta.schema));
783 out.push_str(&format!("**Policy:** {}\n", manifest.meta.policy_ref));
784 out.push_str(&format!("**Oracle ref:** {}\n\n", manifest.meta.oracle_ref));
785
786 out.push_str("## Summary\n\n");
787 out.push_str("| Metric | Count |\n");
788 out.push_str("|--------|-------|\n");
789 out.push_str(&format!("| Total records | {} |\n", summary.total));
790 out.push_str(&format!(
791 "| Terminal (resolved) | {} |\n",
792 summary.terminal_count
793 ));
794 out.push_str(&format!("| Gap (unresolved) | {} |\n", summary.gap_count));
795 out.push_str(&format!(
796 "| Certification readiness | {:.0}% |\n\n",
797 if summary.total > 0 {
798 (summary.terminal_count as f64 / summary.total as f64) * 100.0
799 } else {
800 0.0
801 }
802 ));
803
804 out.push_str("### By Status\n\n");
805 out.push_str("| Status | Count |\n");
806 out.push_str("|--------|-------|\n");
807 for (status, count) in &summary.by_status {
808 out.push_str(&format!("| {} | {} |\n", status, count));
809 }
810 out.push('\n');
811
812 out.push_str("### By Category\n\n");
813 out.push_str("| Category | Count |\n");
814 out.push_str("|----------|-------|\n");
815 for (category, count) in &summary.by_category {
816 out.push_str(&format!("| {} | {} |\n", category, count));
817 }
818 out.push('\n');
819
820 out.push_str("### By Owner\n\n");
821 out.push_str("| Owner | Count |\n");
822 out.push_str("|-------|-------|\n");
823 for (owner, count) in &summary.by_owner {
824 out.push_str(&format!("| {} | {} |\n", owner, count));
825 }
826 out.push('\n');
827
828 out.push_str("### By Milestone\n\n");
829 out.push_str("| Milestone | Count |\n");
830 out.push_str("|-----------|-------|\n");
831 for (milestone, count) in &summary.by_milestone {
832 out.push_str(&format!("| {} | {} |\n", milestone, count));
833 }
834 out.push('\n');
835
836 out.push_str("### By Certification Scope\n\n");
837 out.push_str("| Scope | Count |\n");
838 out.push_str("|-------|-------|\n");
839 for (scope, count) in &summary.by_certification_scope {
840 out.push_str(&format!("| {} | {} |\n", scope, count));
841 }
842 out.push('\n');
843
844 out.push_str("## Gap Records\n\n");
845 out.push_str("Records with non-terminal status requiring resolution:\n\n");
846 let gaps: Vec<_> = manifest
847 .record
848 .iter()
849 .filter(|r| !TERMINAL_STATUSES.contains(&r.status.as_str()))
850 .collect();
851 if gaps.is_empty() {
852 out.push_str("_No unresolved gaps._\n\n");
853 } else {
854 out.push_str("| ID | Status | Category | Owner | Milestone |\n");
855 out.push_str("|----|--------|----------|-------|----------|\n");
856 for rec in &gaps {
857 out.push_str(&format!(
858 "| {} | {} | {} | {} | {} |\n",
859 rec.id, rec.status, rec.category, rec.owner, rec.milestone
860 ));
861 }
862 out.push('\n');
863 }
864
865 out.push_str("## Terminal Records\n\n");
866 let terminals: Vec<_> = manifest
867 .record
868 .iter()
869 .filter(|r| TERMINAL_STATUSES.contains(&r.status.as_str()))
870 .collect();
871 if terminals.is_empty() {
872 out.push_str("_No terminal records._\n\n");
873 } else {
874 out.push_str("| ID | Status | Category | Notes |\n");
875 out.push_str("|----|--------|----------|-------|\n");
876 for rec in &terminals {
877 let notes = if rec.notes.is_empty() {
878 "-".to_string()
879 } else if rec.notes.len() > 80 {
880 format!("{}...", &rec.notes[..77])
881 } else {
882 rec.notes.clone()
883 };
884 out.push_str(&format!(
885 "| {} | {} | {} | {} |\n",
886 rec.id, rec.status, rec.category, notes
887 ));
888 }
889 out.push('\n');
890 }
891
892 out.push_str("## Structural Inventory Records\n\n");
894 let structural: Vec<_> = manifest
895 .record
896 .iter()
897 .filter(|r| r.certification_scope == "structural")
898 .collect();
899 if structural.is_empty() {
900 out.push_str("_No structural inventory records._\n\n");
901 } else {
902 out.push_str("| ID | Status | Comparator | Evidence Level | Behavior Record |\n");
903 out.push_str("|----|--------|------------|----------------|------------------|\n");
904 for rec in &structural {
905 let behavior = rec.behavior_record.as_deref().unwrap_or("-");
906 out.push_str(&format!(
907 "| {} | {} | {} | {} | {} |\n",
908 rec.id, rec.status, rec.comparator, rec.evidence_level, behavior
909 ));
910 }
911 out.push('\n');
912 }
913
914 out.push_str("## Behaviorally Certified Records\n\n");
916 let behavioral: Vec<_> = manifest
917 .record
918 .iter()
919 .filter(|r| r.certification_scope == "behavioral")
920 .collect();
921 if behavioral.is_empty() {
922 out.push_str("_No behaviorally certified records._\n\n");
923 } else {
924 out.push_str("| ID | Status | Comparator | Evidence Level |\n");
925 out.push_str("|----|--------|------------|----------------|\n");
926 for rec in &behavioral {
927 out.push_str(&format!(
928 "| {} | {} | {} | {} |\n",
929 rec.id, rec.status, rec.comparator, rec.evidence_level
930 ));
931 }
932 out.push('\n');
933 }
934
935 out.push_str("## Intentional Non-Parity Records\n\n");
937 let non_parity: Vec<_> = manifest
938 .record
939 .iter()
940 .filter(|r| r.status == "intentional_non_parity")
941 .collect();
942 if non_parity.is_empty() {
943 out.push_str("_No intentional non-parity records._\n\n");
944 } else {
945 out.push_str("| ID | Category | Notes |\n");
946 out.push_str("|----|----------|-------|\n");
947 for rec in &non_parity {
948 let notes = if rec.notes.is_empty() {
949 "-".to_string()
950 } else if rec.notes.len() > 80 {
951 format!("{}...", &rec.notes[..77])
952 } else {
953 rec.notes.clone()
954 };
955 out.push_str(&format!("| {} | {} | {} |\n", rec.id, rec.category, notes));
956 }
957 out.push('\n');
958 }
959
960 out.push_str("## Platform-Constrained Records\n\n");
962 let platform: Vec<_> = manifest
963 .record
964 .iter()
965 .filter(|r| r.status == "platform_constraint" || r.certification_scope == "platform")
966 .collect();
967 if platform.is_empty() {
968 out.push_str("_No platform-constrained records._\n\n");
969 } else {
970 out.push_str("| ID | Status | Platforms | Notes |\n");
971 out.push_str("|----|--------|-----------|-------|\n");
972 for rec in &platform {
973 let platforms = if rec.platforms.is_empty() {
974 "-".to_string()
975 } else {
976 rec.platforms.join(", ")
977 };
978 let notes = if rec.notes.is_empty() {
979 "-".to_string()
980 } else if rec.notes.len() > 60 {
981 format!("{}...", &rec.notes[..57])
982 } else {
983 rec.notes.clone()
984 };
985 out.push_str(&format!(
986 "| {} | {} | {} | {} |\n",
987 rec.id, rec.status, platforms, notes
988 ));
989 }
990 out.push('\n');
991 }
992
993 out.push_str("## Unresolved Behavior Gaps\n\n");
995 let behavior_gaps: Vec<_> = manifest
996 .record
997 .iter()
998 .filter(|r| {
999 !TERMINAL_STATUSES.contains(&r.status.as_str()) && r.certification_scope == "behavioral"
1000 })
1001 .collect();
1002 if behavior_gaps.is_empty() {
1003 out.push_str("_No unresolved behavior gaps._\n\n");
1004 } else {
1005 out.push_str("| ID | Status | Category | Owner | Milestone |\n");
1006 out.push_str("|----|--------|----------|-------|----------|\n");
1007 for rec in &behavior_gaps {
1008 out.push_str(&format!(
1009 "| {} | {} | {} | {} | {} |\n",
1010 rec.id, rec.status, rec.category, rec.owner, rec.milestone
1011 ));
1012 }
1013 out.push('\n');
1014 }
1015
1016 out.push_str("## Missing or Stale Evidence\n\n");
1018 let missing_evidence: Vec<_> = manifest
1019 .record
1020 .iter()
1021 .filter(|r| r.closure_required && r.evidence_refs.is_empty())
1022 .collect();
1023 if missing_evidence.is_empty() {
1024 out.push_str("_No records with missing required evidence._\n\n");
1025 } else {
1026 out.push_str("| ID | Status | Certification Scope | Closure Required |\n");
1027 out.push_str("|----|--------|---------------------|------------------|\n");
1028 for rec in &missing_evidence {
1029 out.push_str(&format!(
1030 "| {} | {} | {} | {} |\n",
1031 rec.id, rec.status, rec.certification_scope, rec.closure_required
1032 ));
1033 }
1034 out.push('\n');
1035 }
1036
1037 out
1038}
1039
1040pub fn write_strict_report(
1042 manifest: &StrictManifest,
1043 output_path: &Path,
1044) -> Result<(), std::io::Error> {
1045 let report = generate_strict_report(manifest);
1046 if let Some(parent) = output_path.parent() {
1047 fs::create_dir_all(parent)?;
1048 }
1049 fs::write(output_path, report)
1050}
1051
1052#[cfg(test)]
1053#[allow(clippy::all)]
1054mod tests {
1055 use super::*;
1056
1057 fn make_meta() -> StrictManifestMeta {
1058 StrictManifestMeta {
1059 manifest_version: "1".to_string(),
1060 pproxy_version: "2.7.9".to_string(),
1061 schema: "strict_1".to_string(),
1062 policy_ref: String::new(),
1063 oracle_ref: String::new(),
1064 closure_through: "C".to_string(),
1065 }
1066 }
1067
1068 fn make_manifest(records: Vec<StrictRecord>) -> StrictManifest {
1069 StrictManifest {
1070 meta: make_meta(),
1071 record: records,
1072 }
1073 }
1074
1075 fn default_record(id: &str) -> StrictRecord {
1076 StrictRecord {
1077 id: id.to_string(),
1078 category: "protocol".to_string(),
1079 kind: "role".to_string(),
1080 module: "http".to_string(),
1081 name: format!("Test {id}"),
1082 oracle_probe: "test.probe".to_string(),
1083 candidate_probe: "test.probe".to_string(),
1084 comparator: "protocol_wire".to_string(),
1085 status: "drop_in".to_string(),
1086 owner: "track-b".to_string(),
1087 milestone: "B".to_string(),
1088 platforms: vec!["linux".to_string()],
1089 python_versions: vec![],
1090 depends_on: vec![],
1091 test_refs: vec!["test_ref".to_string()],
1092 evidence_refs: vec!["evidence_ref".to_string()],
1093 notes: String::new(),
1094 evidence_level: "paired_oracle".to_string(),
1095 implementation_state: "functional".to_string(),
1096 certification_scope: "behavioral".to_string(),
1097 closure_required: true,
1098 behavior_record: None,
1099 inventory_only: false,
1100 }
1101 }
1102
1103 #[test]
1104 fn valid_manifest_passes() {
1105 let rec = default_record("test.ok");
1106 let manifest = make_manifest(vec![rec]);
1107 let result = validate_strict_manifest(&manifest);
1108 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1109 }
1110
1111 #[test]
1112 fn duplicate_ids_fail() {
1113 let manifest = make_manifest(vec![default_record("dup"), default_record("dup")]);
1114 let errs = validate_strict_manifest(&manifest).unwrap_err();
1115 assert!(
1116 errs.errors
1117 .iter()
1118 .any(|e| matches!(e, StrictValidationError::DuplicateId { id, .. } if id == "dup")),
1119 "expected DuplicateId"
1120 );
1121 }
1122
1123 #[test]
1124 fn empty_id_fails() {
1125 let rec = StrictRecord {
1126 id: String::new(),
1127 ..default_record("unused")
1128 };
1129 let manifest = make_manifest(vec![rec]);
1130 let errs = validate_strict_manifest(&manifest).unwrap_err();
1131 assert!(
1132 errs.errors
1133 .iter()
1134 .any(|e| matches!(e, StrictValidationError::EmptyId)),
1135 "expected EmptyId"
1136 );
1137 }
1138
1139 #[test]
1140 fn unknown_category_fails() {
1141 let mut rec = default_record("bad_cat");
1142 rec.category = "bogus".to_string();
1143 let manifest = make_manifest(vec![rec]);
1144 let errs = validate_strict_manifest(&manifest).unwrap_err();
1145 assert!(
1146 errs.errors.iter().any(|e| matches!(
1147 e,
1148 StrictValidationError::UnknownCategory { value, .. } if value == "bogus"
1149 )),
1150 "expected UnknownCategory"
1151 );
1152 }
1153
1154 #[test]
1155 fn unknown_status_fails() {
1156 let mut rec = default_record("bad_status");
1157 rec.status = "bogus".to_string();
1158 let manifest = make_manifest(vec![rec]);
1159 let errs = validate_strict_manifest(&manifest).unwrap_err();
1160 assert!(
1161 errs.errors.iter().any(|e| matches!(
1162 e,
1163 StrictValidationError::UnknownStatus { value, .. } if value == "bogus"
1164 )),
1165 "expected UnknownStatus"
1166 );
1167 }
1168
1169 #[test]
1170 fn unknown_comparator_fails() {
1171 let mut rec = default_record("bad_comp");
1172 rec.comparator = "bogus".to_string();
1173 let manifest = make_manifest(vec![rec]);
1174 let errs = validate_strict_manifest(&manifest).unwrap_err();
1175 assert!(
1176 errs.errors.iter().any(|e| matches!(
1177 e,
1178 StrictValidationError::UnknownComparator { value, .. } if value == "bogus"
1179 )),
1180 "expected UnknownComparator"
1181 );
1182 }
1183
1184 #[test]
1185 fn unknown_owner_fails() {
1186 let mut rec = default_record("bad_owner");
1187 rec.owner = "bogus".to_string();
1188 let manifest = make_manifest(vec![rec]);
1189 let errs = validate_strict_manifest(&manifest).unwrap_err();
1190 assert!(
1191 errs.errors.iter().any(|e| matches!(
1192 e,
1193 StrictValidationError::UnknownOwner { value, .. } if value == "bogus"
1194 )),
1195 "expected UnknownOwner"
1196 );
1197 }
1198
1199 #[test]
1200 fn unknown_milestone_fails() {
1201 let mut rec = default_record("bad_ms");
1202 rec.milestone = "Z".to_string();
1203 let manifest = make_manifest(vec![rec]);
1204 let errs = validate_strict_manifest(&manifest).unwrap_err();
1205 assert!(
1206 errs.errors.iter().any(|e| matches!(
1207 e,
1208 StrictValidationError::UnknownMilestone { value, .. } if value == "Z"
1209 )),
1210 "expected UnknownMilestone"
1211 );
1212 }
1213
1214 #[test]
1215 fn drop_in_without_evidence_fails() {
1216 let mut rec = default_record("no_ev");
1217 rec.status = "drop_in".to_string();
1218 rec.evidence_refs = vec![];
1219 rec.test_refs = vec![];
1220 let manifest = make_manifest(vec![rec]);
1221 let errs = validate_strict_manifest(&manifest).unwrap_err();
1222 assert!(
1223 errs.errors.iter().any(|e| matches!(
1224 e,
1225 StrictValidationError::DropInWithoutEvidence { id, .. } if id == "no_ev"
1226 )),
1227 "expected DropInWithoutEvidence"
1228 );
1229 }
1230
1231 #[test]
1232 fn drop_in_without_oracle_probe_fails() {
1233 let mut rec = default_record("no_probe");
1234 rec.status = "drop_in".to_string();
1235 rec.oracle_probe = String::new();
1236 let manifest = make_manifest(vec![rec]);
1237 let errs = validate_strict_manifest(&manifest).unwrap_err();
1238 assert!(
1239 errs.errors.iter().any(|e| matches!(
1240 e,
1241 StrictValidationError::DropInWithoutOracleProbe { id, .. } if id == "no_probe"
1242 )),
1243 "expected DropInWithoutOracleProbe"
1244 );
1245 }
1246
1247 #[test]
1248 fn drop_in_with_evidence_passes() {
1249 let mut rec = default_record("with_ev");
1250 rec.status = "drop_in".to_string();
1251 rec.evidence_refs = vec!["some_evidence".to_string()];
1252 let manifest = make_manifest(vec![rec]);
1253 let result = validate_strict_manifest(&manifest);
1254 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1255 }
1256
1257 #[test]
1258 fn drop_in_with_tests_passes() {
1259 let mut rec = default_record("with_tests");
1260 rec.status = "drop_in".to_string();
1261 rec.test_refs = vec!["some_test".to_string()];
1262 rec.evidence_refs = vec![];
1263 rec.closure_required = false; let manifest = make_manifest(vec![rec]);
1265 let result = validate_strict_manifest(&manifest);
1266 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1267 }
1268
1269 #[test]
1270 fn gap_at_current_milestone_fails() {
1271 let mut rec = default_record("gap_ms");
1272 rec.status = "gap".to_string();
1273 rec.milestone = "A".to_string(); let manifest = make_manifest(vec![rec]);
1275 let errs = validate_strict_manifest(&manifest).unwrap_err();
1276 assert!(
1277 errs.errors.iter().any(|e| matches!(
1278 e,
1279 StrictValidationError::UnresolvedProgress { id, status, .. }
1280 if id == "gap_ms" && status == "gap"
1281 )),
1282 "expected UnresolvedProgress for gap at milestone A"
1283 );
1284 }
1285
1286 #[test]
1287 fn drop_in_at_current_milestone_passes() {
1288 let mut rec = default_record("di_ms");
1289 rec.status = "drop_in".to_string();
1290 rec.milestone = "A".to_string();
1291 let manifest = make_manifest(vec![rec]);
1292 let result = validate_strict_manifest(&manifest);
1293 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1294 }
1295
1296 #[test]
1297 fn gap_at_future_milestone_passes() {
1298 let mut rec = default_record("gap_future");
1299 rec.status = "gap".to_string();
1300 rec.milestone = "E".to_string(); let manifest = make_manifest(vec![rec]);
1302 let result = validate_strict_manifest(&manifest);
1303 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1304 }
1305
1306 #[test]
1307 fn multiple_errors_collected() {
1308 let mut rec = default_record("dup");
1309 rec.category = "bogus".to_string();
1310 let manifest = make_manifest(vec![rec, default_record("dup")]);
1311 let errs = validate_strict_manifest(&manifest).unwrap_err();
1312 assert!(
1313 errs.len() >= 2,
1314 "expected at least 2 errors, got {}",
1315 errs.len()
1316 );
1317 assert!(errs
1318 .errors
1319 .iter()
1320 .any(|e| matches!(e, StrictValidationError::DuplicateId { .. })));
1321 assert!(errs
1322 .errors
1323 .iter()
1324 .any(|e| matches!(e, StrictValidationError::UnknownCategory { .. })));
1325 }
1326
1327 #[test]
1328 fn all_allowed_categories_are_valid() {
1329 for cat in ALLOWED_CATEGORIES {
1330 let mut rec = default_record(&format!("cat_{cat}"));
1331 rec.category = cat.to_string();
1332 rec.status = "gap".to_string();
1333 rec.milestone = "F".to_string(); let manifest = make_manifest(vec![rec]);
1335 let errs = validate_strict_manifest(&manifest);
1336 if let Err(ref e) = errs {
1337 assert!(
1338 !e.errors
1339 .iter()
1340 .any(|e| matches!(e, StrictValidationError::UnknownCategory { .. })),
1341 "category \"{cat}\" should be valid"
1342 );
1343 }
1344 }
1345 }
1346
1347 #[test]
1348 fn all_allowed_statuses_are_valid() {
1349 for status in ALLOWED_STATUSES {
1350 let mut rec = default_record(&format!("status_{status}"));
1351 rec.status = status.to_string();
1352 rec.milestone = "F".to_string();
1353 let manifest = make_manifest(vec![rec]);
1354 let errs = validate_strict_manifest(&manifest);
1355 if let Err(ref e) = errs {
1356 assert!(
1357 !e.errors
1358 .iter()
1359 .any(|e| matches!(e, StrictValidationError::UnknownStatus { .. })),
1360 "status \"{status}\" should be valid"
1361 );
1362 }
1363 }
1364 }
1365
1366 #[test]
1367 fn all_allowed_comparators_are_valid() {
1368 for comp in ALLOWED_COMPARATORS {
1369 let mut rec = default_record(&format!("comp_{comp}"));
1370 rec.comparator = comp.to_string();
1371 rec.status = "gap".to_string();
1372 rec.milestone = "F".to_string();
1373 let manifest = make_manifest(vec![rec]);
1374 let errs = validate_strict_manifest(&manifest);
1375 if let Err(ref e) = errs {
1376 assert!(
1377 !e.errors
1378 .iter()
1379 .any(|e| matches!(e, StrictValidationError::UnknownComparator { .. })),
1380 "comparator \"{comp}\" should be valid"
1381 );
1382 }
1383 }
1384 }
1385
1386 #[test]
1387 fn all_allowed_owners_are_valid() {
1388 for owner in ALLOWED_OWNERS {
1389 let mut rec = default_record(&format!("owner_{owner}"));
1390 rec.owner = owner.to_string();
1391 rec.status = "gap".to_string();
1392 rec.milestone = "F".to_string();
1393 let manifest = make_manifest(vec![rec]);
1394 let errs = validate_strict_manifest(&manifest);
1395 if let Err(ref e) = errs {
1396 assert!(
1397 !e.errors
1398 .iter()
1399 .any(|e| matches!(e, StrictValidationError::UnknownOwner { .. })),
1400 "owner \"{owner}\" should be valid"
1401 );
1402 }
1403 }
1404 }
1405
1406 #[test]
1407 fn all_allowed_milestones_are_valid() {
1408 for ms in ALLOWED_MILESTONES {
1409 let mut rec = default_record(&format!("ms_{ms}"));
1410 rec.milestone = ms.to_string();
1411 rec.status = "gap".to_string();
1412 let manifest = make_manifest(vec![rec]);
1413 let errs = validate_strict_manifest(&manifest);
1414 if let Err(ref e) = errs {
1415 assert!(
1416 !e.errors
1417 .iter()
1418 .any(|e| matches!(e, StrictValidationError::UnknownMilestone { .. })),
1419 "milestone \"{ms}\" should be valid"
1420 );
1421 }
1422 }
1423 }
1424
1425 #[test]
1426 fn validation_errors_collection() {
1427 let mut errs = StrictValidationErrors::new();
1428 assert!(errs.is_empty());
1429 assert_eq!(errs.len(), 0);
1430 errs.push(StrictValidationError::EmptyId);
1431 errs.push(StrictValidationError::DuplicateId {
1432 id: "a".to_string(),
1433 });
1434 assert!(!errs.is_empty());
1435 assert_eq!(errs.len(), 2);
1436 }
1437
1438 #[test]
1439 fn validate_strict_manifest_file_missing_path() {
1440 let path = Path::new("/nonexistent/path/strict_manifest.toml");
1441 let result = validate_strict_manifest_file(path);
1442 assert!(result.is_err());
1443 let errs = result.unwrap_err();
1444 assert!(errs
1445 .errors
1446 .iter()
1447 .any(|e| matches!(e, StrictValidationError::Io { .. })));
1448 }
1449
1450 #[test]
1451 fn toml_parse_error() {
1452 let bad_toml = "this is not [valid toml {{{{";
1453 let result: Result<StrictManifest, _> = toml::from_str(bad_toml);
1454 assert!(result.is_err());
1455 }
1456
1457 #[test]
1458 fn validate_real_strict_manifest() {
1459 let path = match find_strict_manifest_path() {
1460 Some(p) => p,
1461 None => {
1462 eprintln!("strict manifest not found, skipping");
1463 return;
1464 }
1465 };
1466 eprintln!("Validating strict manifest at: {}", path.display());
1467 match validate_strict_manifest_file(&path) {
1468 Ok(manifest) => {
1469 eprintln!(
1470 "Strict manifest OK: {} records, meta.schema={}",
1471 manifest.record.len(),
1472 manifest.meta.schema
1473 );
1474 }
1475 Err(errs) => {
1476 eprintln!(
1477 "Strict manifest validation FAILED with {} errors:",
1478 errs.len()
1479 );
1480 for (i, err) in errs.errors.iter().enumerate() {
1481 eprintln!(" ERROR {}: {}", i + 1, err);
1482 }
1483 panic!(
1484 "strict manifest validation failed with {} errors (see above)",
1485 errs.len()
1486 );
1487 }
1488 }
1489 }
1490
1491 #[test]
1492 fn milestone_index_ordering() {
1493 assert!(milestone_index("A") < milestone_index("B"));
1494 assert!(milestone_index("B") < milestone_index("C"));
1495 assert!(milestone_index("C") < milestone_index("D"));
1496 assert!(milestone_index("D") < milestone_index("E"));
1497 assert!(milestone_index("E") < milestone_index("F"));
1498 }
1499
1500 #[test]
1501 fn terminal_statuses_not_flagged_at_current_milestone() {
1502 for status in TERMINAL_STATUSES {
1503 let mut rec = default_record(&format!("term_{status}"));
1504 rec.status = status.to_string();
1505 rec.milestone = "B".to_string();
1506 let manifest = make_manifest(vec![rec]);
1507 let errs = validate_strict_manifest(&manifest);
1508 if let Err(ref e) = errs {
1509 assert!(
1510 !e.errors
1511 .iter()
1512 .any(|e| matches!(e, StrictValidationError::UnresolvedProgress { .. })),
1513 "terminal status \"{status}\" should not be flagged at current milestone"
1514 );
1515 }
1516 }
1517 }
1518
1519 #[test]
1520 fn gap_at_milestone_a_also_flagged() {
1521 let mut rec = default_record("gap_a");
1522 rec.status = "gap".to_string();
1523 rec.milestone = "A".to_string();
1524 let manifest = make_manifest(vec![rec]);
1525 let errs = validate_strict_manifest(&manifest).unwrap_err();
1526 assert!(
1527 errs.errors
1528 .iter()
1529 .any(|e| matches!(e, StrictValidationError::UnresolvedProgress { .. })),
1530 "gap at milestone A should be flagged"
1531 );
1532 }
1533
1534 #[test]
1537 fn verify_manifest_oracle_version_matches() {
1538 let manifest = make_manifest(vec![]);
1539 let provenance = OracleProvenance {
1540 oracle: OracleProvenanceInner {
1541 package: "pproxy".to_string(),
1542 version: "2.7.9".to_string(),
1543 source: "pypi".to_string(),
1544 license: "MIT".to_string(),
1545 retrieval_date: "2026-07-16".to_string(),
1546 },
1547 };
1548 assert!(verify_manifest_oracle_version(&manifest, &provenance).is_ok());
1549 }
1550
1551 #[test]
1552 fn verify_manifest_oracle_version_mismatch() {
1553 let mut meta = make_meta();
1554 meta.pproxy_version = "2.7.8".to_string();
1555 let manifest = StrictManifest {
1556 meta,
1557 record: vec![],
1558 };
1559 let provenance = OracleProvenance {
1560 oracle: OracleProvenanceInner {
1561 package: "pproxy".to_string(),
1562 version: "2.7.9".to_string(),
1563 source: "pypi".to_string(),
1564 license: "MIT".to_string(),
1565 retrieval_date: "2026-07-16".to_string(),
1566 },
1567 };
1568 let err = verify_manifest_oracle_version(&manifest, &provenance).unwrap_err();
1569 assert!(matches!(err, OracleError::VersionMismatch { .. }));
1570 }
1571
1572 #[test]
1573 fn verify_manifest_oracle_package_mismatch() {
1574 let manifest = make_manifest(vec![]);
1575 let provenance = OracleProvenance {
1576 oracle: OracleProvenanceInner {
1577 package: "not_pproxy".to_string(),
1578 version: "2.7.9".to_string(),
1579 source: "pypi".to_string(),
1580 license: "MIT".to_string(),
1581 retrieval_date: "2026-07-16".to_string(),
1582 },
1583 };
1584 let err = verify_manifest_oracle_version(&manifest, &provenance).unwrap_err();
1585 assert!(matches!(err, OracleError::PackageMismatch { .. }));
1586 }
1587
1588 #[test]
1589 fn load_oracle_hashes_from_workspace() {
1590 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1591 let path = PathBuf::from(&manifest_dir);
1592 match load_oracle_hashes(&path) {
1593 Ok(hashes) => {
1594 assert_eq!(hashes.package.version, "2.7.9");
1595 assert!(hashes.package.sha256_wheel.is_some());
1596 let wheel_hash = hashes.package.sha256_wheel.unwrap();
1597 assert_ne!(wheel_hash, "placeholder_update_on_first_run");
1598 assert_eq!(wheel_hash.len(), 64); }
1600 Err(e) => {
1601 eprintln!("load_oracle_hashes: {}", e);
1602 }
1603 }
1604 }
1605 }
1606
1607 #[test]
1608 fn load_oracle_provenance_from_workspace() {
1609 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1610 let path = PathBuf::from(&manifest_dir);
1611 match load_oracle_provenance(&path) {
1612 Ok(prov) => {
1613 assert_eq!(prov.oracle.package, "pproxy");
1614 assert_eq!(prov.oracle.version, "2.7.9");
1615 assert_eq!(prov.oracle.source, "pypi");
1616 }
1617 Err(e) => {
1618 eprintln!("load_oracle_provenance: {}", e);
1619 }
1620 }
1621 }
1622 }
1623
1624 #[test]
1625 fn manifest_version_matches_provenance() {
1626 if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1627 let path = PathBuf::from(&manifest_dir);
1628 let hashes = load_oracle_hashes(&path);
1629 let provenance = load_oracle_provenance(&path);
1630 if let (Ok(h), Ok(p)) = (hashes, provenance) {
1631 assert_eq!(h.package.version, p.oracle.version);
1632 }
1633 }
1634 }
1635
1636 #[test]
1639 fn summarize_manifest_basic() {
1640 let mut r1 = default_record("a");
1641 r1.status = "drop_in".to_string();
1642 r1.category = "protocol".to_string();
1643 r1.owner = "track-b".to_string();
1644 let mut r2 = default_record("b");
1645 r2.status = "gap".to_string();
1646 r2.category = "python_namespace".to_string();
1647 r2.owner = "track-a".to_string();
1648 r2.milestone = "F".to_string(); let manifest = make_manifest(vec![r1, r2]);
1650 let summary = summarize_manifest(&manifest);
1651 assert_eq!(summary.total, 2);
1652 assert_eq!(summary.terminal_count, 1);
1653 assert_eq!(summary.gap_count, 1);
1654 assert!(!summary.by_certification_scope.is_empty());
1655 }
1656
1657 #[test]
1658 fn summarize_manifest_all_terminal() {
1659 let mut r1 = default_record("a");
1660 r1.status = "drop_in".to_string();
1661 let mut r2 = default_record("b");
1662 r2.status = "not_applicable".to_string();
1663 let manifest = make_manifest(vec![r1, r2]);
1664 let summary = summarize_manifest(&manifest);
1665 assert_eq!(summary.total, 2);
1666 assert_eq!(summary.terminal_count, 2);
1667 assert_eq!(summary.gap_count, 0);
1668 }
1669
1670 #[test]
1671 fn generate_strict_report_basic() {
1672 let mut r1 = default_record("a");
1673 r1.status = "drop_in".to_string();
1674 r1.category = "protocol".to_string();
1675 r1.notes = "Test note".to_string();
1676 let mut r2 = default_record("b");
1677 r2.status = "gap".to_string();
1678 r2.category = "python_namespace".to_string();
1679 let manifest = make_manifest(vec![r1, r2]);
1680 let report = generate_strict_report(&manifest);
1681 assert!(report.contains("# pproxy 2.7.9 Strict Compatibility Report"));
1682 assert!(report.contains("pproxy==2.7.9"));
1683 assert!(report.contains("Total records"));
1684 assert!(report.contains("Gap Records"));
1685 assert!(report.contains("Terminal Records"));
1686 }
1687
1688 #[test]
1689 fn generate_strict_report_empty() {
1690 let manifest = make_manifest(vec![]);
1691 let report = generate_strict_report(&manifest);
1692 assert!(report.contains("Total records"));
1693 assert!(report.contains("_No unresolved gaps._"));
1694 assert!(report.contains("_No terminal records._"));
1695 }
1696
1697 #[test]
1698 fn write_strict_report_to_temp() {
1699 let manifest = make_manifest(vec![]);
1700 let dir = std::env::temp_dir().join("eggress_strict_report_test");
1701 let path = dir.join("report.md");
1702 let result = write_strict_report(&manifest, &path);
1703 assert!(result.is_ok());
1704 assert!(path.exists());
1705 let content = fs::read_to_string(&path).unwrap();
1706 assert!(content.contains("pproxy 2.7.9 Strict Compatibility Report"));
1707 let _ = fs::remove_file(&path);
1709 let _ = fs::remove_dir(&dir);
1710 }
1711
1712 #[test]
1713 fn generate_strict_report_truncates_long_notes() {
1714 let mut rec = default_record("long_notes");
1715 rec.notes = "A".repeat(200);
1716 rec.status = "drop_in".to_string();
1717 let manifest = make_manifest(vec![rec]);
1718 let report = generate_strict_report(&manifest);
1719 assert!(report.contains("..."));
1720 }
1721
1722 #[test]
1725 fn module_existence_drop_in_structural_only_rejected() {
1726 let mut rec = default_record("me_drop_struct");
1727 rec.comparator = "module_existence".to_string();
1728 rec.status = "drop_in".to_string();
1729 rec.evidence_level = "structural_only".to_string();
1730 let manifest = make_manifest(vec![rec]);
1731 let errs = validate_strict_manifest(&manifest).unwrap_err();
1732 assert!(
1733 errs.errors.iter().any(|e| matches!(
1734 e,
1735 StrictValidationError::StructuralComparatorDropInRequiresEvidence { id, .. }
1736 if id == "me_drop_struct"
1737 )),
1738 "expected StructuralComparatorDropInRequiresEvidence for module_existence + drop_in + structural_only"
1739 );
1740 }
1741
1742 #[test]
1743 fn structural_only_with_drop_in_rejected() {
1744 let mut rec = default_record("struct_drop");
1745 rec.comparator = "protocol_wire".to_string();
1746 rec.status = "drop_in".to_string();
1747 rec.evidence_level = "structural_only".to_string();
1748 let manifest = make_manifest(vec![rec]);
1749 let errs = validate_strict_manifest(&manifest).unwrap_err();
1750 assert!(
1751 errs.errors.iter().any(|e| matches!(
1752 e,
1753 StrictValidationError::StructuralOnlyIncompatibleWithDropIn { id, .. }
1754 if id == "struct_drop"
1755 )),
1756 "expected StructuralOnlyIncompatibleWithDropIn for structural_only + drop_in"
1757 );
1758 }
1759
1760 #[test]
1761 fn drop_in_with_candidate_only_rejected() {
1762 let mut rec = default_record("drop_candidate");
1763 rec.comparator = "protocol_wire".to_string();
1764 rec.status = "drop_in".to_string();
1765 rec.evidence_level = "candidate_only".to_string();
1766 let manifest = make_manifest(vec![rec]);
1767 let errs = validate_strict_manifest(&manifest).unwrap_err();
1768 assert!(
1769 errs.errors.iter().any(|e| matches!(
1770 e,
1771 StrictValidationError::DropInRequiresStrongEvidence { id, .. }
1772 if id == "drop_candidate"
1773 )),
1774 "expected DropInRequiresStrongEvidence for drop_in + candidate_only"
1775 );
1776 }
1777
1778 #[test]
1779 fn drop_in_with_paired_oracle_passes() {
1780 let mut rec = default_record("drop_paired");
1781 rec.status = "drop_in".to_string();
1782 rec.evidence_level = "paired_oracle".to_string();
1783 let manifest = make_manifest(vec![rec]);
1784 let result = validate_strict_manifest(&manifest);
1785 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1786 }
1787
1788 #[test]
1789 fn drop_in_with_bidirectional_interop_passes() {
1790 let mut rec = default_record("drop_bidi");
1791 rec.status = "drop_in".to_string();
1792 rec.evidence_level = "bidirectional_interop".to_string();
1793 let manifest = make_manifest(vec![rec]);
1794 let result = validate_strict_manifest(&manifest);
1795 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1796 }
1797
1798 #[test]
1799 fn all_allowed_evidence_levels_are_valid() {
1800 for ev in ALLOWED_EVIDENCE_LEVELS {
1801 let mut rec = default_record(&format!("ev_{ev}"));
1802 rec.evidence_level = ev.to_string();
1803 rec.status = "gap".to_string();
1804 rec.milestone = "F".to_string();
1805 let manifest = make_manifest(vec![rec]);
1806 let errs = validate_strict_manifest(&manifest);
1807 if let Err(ref e) = errs {
1808 assert!(
1809 !e.errors
1810 .iter()
1811 .any(|e| matches!(e, StrictValidationError::UnknownEvidenceLevel { .. })),
1812 "evidence_level \"{ev}\" should be valid"
1813 );
1814 }
1815 }
1816 }
1817
1818 #[test]
1819 fn all_allowed_implementation_states_are_valid() {
1820 for ist in ALLOWED_IMPLEMENTATION_STATES {
1821 let mut rec = default_record(&format!("ist_{ist}"));
1822 rec.implementation_state = ist.to_string();
1823 rec.status = "gap".to_string();
1824 rec.milestone = "F".to_string();
1825 let manifest = make_manifest(vec![rec]);
1826 let errs = validate_strict_manifest(&manifest);
1827 if let Err(ref e) = errs {
1828 assert!(
1829 !e.errors.iter().any(|e| matches!(
1830 e,
1831 StrictValidationError::UnknownImplementationState { .. }
1832 )),
1833 "implementation_state \"{ist}\" should be valid"
1834 );
1835 }
1836 }
1837 }
1838
1839 #[test]
1840 fn unknown_evidence_level_fails() {
1841 let mut rec = default_record("bad_ev");
1842 rec.evidence_level = "bogus".to_string();
1843 let manifest = make_manifest(vec![rec]);
1844 let errs = validate_strict_manifest(&manifest).unwrap_err();
1845 assert!(
1846 errs.errors.iter().any(|e| matches!(
1847 e,
1848 StrictValidationError::UnknownEvidenceLevel { value, .. } if value == "bogus"
1849 )),
1850 "expected UnknownEvidenceLevel"
1851 );
1852 }
1853
1854 #[test]
1855 fn unknown_implementation_state_fails() {
1856 let mut rec = default_record("bad_ist");
1857 rec.implementation_state = "bogus".to_string();
1858 let manifest = make_manifest(vec![rec]);
1859 let errs = validate_strict_manifest(&manifest).unwrap_err();
1860 assert!(
1861 errs.errors.iter().any(|e| matches!(
1862 e,
1863 StrictValidationError::UnknownImplementationState { value, .. } if value == "bogus"
1864 )),
1865 "expected UnknownImplementationState"
1866 );
1867 }
1868
1869 #[test]
1870 fn constant_value_drop_in_structural_only_rejected() {
1871 let mut rec = default_record("cv_drop_struct");
1872 rec.comparator = "constant_value".to_string();
1873 rec.status = "drop_in".to_string();
1874 rec.evidence_level = "structural_only".to_string();
1875 let manifest = make_manifest(vec![rec]);
1876 let errs = validate_strict_manifest(&manifest).unwrap_err();
1877 assert!(
1878 errs.errors.iter().any(|e| matches!(
1879 e,
1880 StrictValidationError::StructuralComparatorDropInRequiresEvidence { id, .. }
1881 if id == "cv_drop_struct"
1882 )),
1883 "expected StructuralComparatorDropInRequiresEvidence for constant_value + drop_in + structural_only"
1884 );
1885 }
1886
1887 #[test]
1888 fn gap_with_structural_only_passes() {
1889 let mut rec = default_record("gap_struct");
1890 rec.status = "gap".to_string();
1891 rec.milestone = "F".to_string();
1892 rec.evidence_level = "structural_only".to_string();
1893 let manifest = make_manifest(vec![rec]);
1894 let result = validate_strict_manifest(&manifest);
1895 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1896 }
1897
1898 #[test]
1901 fn rule_10a_structural_comparator_behavioral_scope_rejected() {
1902 let mut rec = default_record("r10a_reject");
1903 rec.comparator = "module_existence".to_string();
1904 rec.status = "drop_in".to_string();
1905 rec.certification_scope = "behavioral".to_string();
1906 let manifest = make_manifest(vec![rec]);
1907 let errs = validate_strict_manifest(&manifest).unwrap_err();
1908 assert!(
1909 errs.errors.iter().any(|e| matches!(
1910 e,
1911 StrictValidationError::StructuralComparatorBehavioralScopeMismatch { id, .. }
1912 if id == "r10a_reject"
1913 )),
1914 "expected StructuralComparatorBehavioralScopeMismatch for module_existence + drop_in + behavioral"
1915 );
1916 }
1917
1918 #[test]
1919 fn rule_10a_structural_comparator_structural_scope_passes() {
1920 let mut rec = default_record("r10a_pass");
1921 rec.comparator = "module_existence".to_string();
1922 rec.status = "structural".to_string();
1923 rec.certification_scope = "structural".to_string();
1924 rec.behavior_record = Some("python.pproxy.server.Connection".to_string());
1925 let manifest = make_manifest(vec![rec]);
1926 let result = validate_strict_manifest(&manifest);
1927 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1928 }
1929
1930 #[test]
1931 fn rule_10b_closure_required_without_evidence_rejected() {
1932 let mut rec = default_record("r10b_reject");
1933 rec.closure_required = true;
1934 rec.evidence_refs = vec![];
1935 let manifest = make_manifest(vec![rec]);
1936 let errs = validate_strict_manifest(&manifest).unwrap_err();
1937 assert!(
1938 errs.errors.iter().any(|e| matches!(
1939 e,
1940 StrictValidationError::ClosureRequiredWithoutEvidence { id, .. }
1941 if id == "r10b_reject"
1942 )),
1943 "expected ClosureRequiredWithoutEvidence"
1944 );
1945 }
1946
1947 #[test]
1948 fn rule_10b_closure_not_required_without_evidence_passes() {
1949 let mut rec = default_record("r10b_pass");
1950 rec.closure_required = false;
1951 rec.evidence_refs = vec![];
1952 let manifest = make_manifest(vec![rec]);
1953 let result = validate_strict_manifest(&manifest);
1954 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1955 }
1956
1957 #[test]
1958 fn rule_10c_structural_missing_behavior_record_fails() {
1959 let mut rec = default_record("r10c_reject");
1962 rec.certification_scope = "structural".to_string();
1963 rec.behavior_record = None;
1964 rec.inventory_only = false;
1965 let manifest = make_manifest(vec![rec]);
1966 let errs = validate_strict_manifest(&manifest).unwrap_err();
1967 assert!(
1968 errs.errors.iter().any(|e| matches!(
1969 e,
1970 StrictValidationError::StructuralMissingBehaviorRecord { id, .. }
1971 if id == "r10c_reject"
1972 )),
1973 "expected StructuralMissingBehaviorRecord"
1974 );
1975 }
1976
1977 #[test]
1978 fn rule_10c_structural_inventory_only_passes() {
1979 let mut rec = default_record("r10c_inventory_only");
1981 rec.certification_scope = "structural".to_string();
1982 rec.behavior_record = None;
1983 rec.inventory_only = true;
1984 let manifest = make_manifest(vec![rec]);
1985 let result = validate_strict_manifest(&manifest);
1986 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1987 }
1988
1989 #[test]
1990 fn rule_10c_structural_with_behavior_record_passes() {
1991 let mut rec = default_record("r10c_pass");
1992 rec.certification_scope = "structural".to_string();
1993 rec.behavior_record = Some("some.behavior.record".to_string());
1994 let manifest = make_manifest(vec![rec]);
1995 let result = validate_strict_manifest(&manifest);
1996 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
1997 }
1998
1999 #[test]
2000 fn rule_10c_behavioral_scope_no_behavior_record_passes() {
2001 let mut rec = default_record("r10c_behavioral_pass");
2002 rec.certification_scope = "behavioral".to_string();
2003 rec.behavior_record = None;
2004 let manifest = make_manifest(vec![rec]);
2005 let result = validate_strict_manifest(&manifest);
2006 assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
2007 }
2008
2009 #[test]
2010 fn unknown_certification_scope_fails() {
2011 let mut rec = default_record("bad_scope");
2012 rec.certification_scope = "bogus".to_string();
2013 let manifest = make_manifest(vec![rec]);
2014 let errs = validate_strict_manifest(&manifest).unwrap_err();
2015 assert!(
2016 errs.errors.iter().any(|e| matches!(
2017 e,
2018 StrictValidationError::UnknownCertificationScope { value, .. } if value == "bogus"
2019 )),
2020 "expected UnknownCertificationScope"
2021 );
2022 }
2023
2024 #[test]
2025 fn all_allowed_certification_scopes_are_valid() {
2026 for scope in ALLOWED_CERTIFICATION_SCOPES {
2027 let mut rec = default_record(&format!("scope_{scope}"));
2028 rec.certification_scope = scope.to_string();
2029 rec.status = "gap".to_string();
2030 rec.milestone = "F".to_string();
2031 let manifest = make_manifest(vec![rec]);
2032 let errs = validate_strict_manifest(&manifest);
2033 if let Err(ref e) = errs {
2034 assert!(
2035 !e.errors.iter().any(|e| matches!(
2036 e,
2037 StrictValidationError::UnknownCertificationScope { .. }
2038 )),
2039 "certification_scope \"{scope}\" should be valid"
2040 );
2041 }
2042 }
2043 }
2044}