pub use polymodel_ldraw_core::{ CanonicalRecord, Diagnostic, LdMeshSemantic, SceneRecord, SemanticRecord, Span, SyntaxRecord, TexmapEvent, TexmapKind, }; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::BTreeSet; pub const SCHEMA_VERSION: &str = "ldraw-authority-v1"; pub const CANDIDATE_OBSERVATION_SCHEMA_VERSION: &str = "ldraw-candidate-observation-v1"; pub const ORACLE_SCHEMA_VERSION: &str = "ldraw-oracle-v4"; pub const ORACLE_WRAPPER_SCHEMA_VERSION: &str = "wrapper-v4"; pub const ORACLE_PROJECTION_RULE_VERSION: &str = "pm-78-oracle-only-v4"; pub const ORACLE_PATCH_IDENTITY: &str = "ldparse-oracle-access.patch@pm-78-v2"; pub const LDRAW_LIMITS: &[(&str, u64)] = &[ ("resource_bytes", 100 * 1024 * 1024), ("line_bytes", 1024 * 1024), ("files", 16_384), ("include_depth", 256), ("commands", 5_000_000), ("instances", 5_000_000), ("triangles", 20_000_000), ("lines", 20_000_000), ("textures", 1_024), ("fetches", 8), ("diagnostics", 10_000), ]; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)] #[serde(rename_all = "snake_case")] pub enum Profile { Strict, Compatibility, Lossless, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AuthorityClass { RatifiedSpec, PolymodelSafety, HOwned, Observation, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExpectedOutcome { Accepted, Rejected, Preserved, Cancelled, Limited, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OraclePosition(pub Option, pub Option, pub Option); #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleMesh { pub vertex_count: usize, pub positions: Vec, pub normal_count: usize, pub normals: Vec, pub indices: Vec, pub bf_indices: Vec, pub triangle_count: usize, pub bf_triangle_count: usize, } /// Compare the LDParse mesh projection with the candidate's authoritative /// semantic mesh. Oracle coordinates are f32 because that is the pinned C++ /// projection; candidate f64 values must round to the same finite f32 bits. pub fn compare_mesh_semantic( model_name: &str, expected: &OracleMesh, expected_step_offsets: &[usize], actual: &LdMeshSemantic, ) -> Result<(), String> { fn rounded(value: f64) -> Option { if value.is_finite() { let value = value as f32; value.is_finite().then_some(value) } else { None } } fn position_matches(actual: &[f64; 3], expected: &OraclePosition) -> bool { [rounded(actual[0]), rounded(actual[1]), rounded(actual[2])] .into_iter() .zip([expected.0, expected.1, expected.2]) .all(|(actual, expected)| match (actual, expected) { (Some(actual), Some(expected)) => actual.to_bits() == expected.to_bits(), (None, None) => true, _ => false, }) } if actual.positions.len() != expected.vertex_count || actual.positions.len() != expected.positions.len() { return Err(format!( "{model_name} positions length mismatch: oracle_count={} oracle_values={} candidate={}", expected.vertex_count, expected.positions.len(), actual.positions.len() )); } for (index, (actual, expected)) in actual.positions.iter().zip(&expected.positions).enumerate() { if !position_matches(actual, expected) { return Err(format!( "{model_name} position mismatch at {index}: oracle={expected:?} candidate={actual:?}" )); } } let actual_normals = actual.normals.as_deref().unwrap_or(&[]); if actual_normals.len() != expected.normal_count || actual_normals.len() != expected.normals.len() { return Err(format!( "{model_name} normals length mismatch: oracle_count={} oracle_values={} candidate={}", expected.normal_count, expected.normals.len(), actual_normals.len() )); } for (index, (actual, expected)) in actual_normals.iter().zip(&expected.normals).enumerate() { if !position_matches(actual, expected) { return Err(format!( "{model_name} normal mismatch at {index}: oracle={expected:?} candidate={actual:?}" )); } } if actual.indices != expected.indices { return Err(format!( "{model_name} forward indices mismatch: oracle={:?} candidate={:?}", expected.indices, actual.indices )); } if actual.bf_indices != expected.bf_indices { return Err(format!( "{model_name} reverse indices mismatch: oracle={:?} candidate={:?}", expected.bf_indices, actual.bf_indices )); } if actual.step_offsets != expected_step_offsets { return Err(format!( "{model_name} step offsets mismatch: oracle={expected_step_offsets:?} candidate={:?}", actual.step_offsets )); } Ok(()) } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleTransform { pub translation: OraclePosition, pub matrix: Vec>, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleChild { pub index: usize, pub bfc: String, pub transform: OracleTransform, pub child_name: Option, pub child_path: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleModel { pub name: String, pub model_index: u32, pub path: String, pub certify: String, pub winding: String, pub default_color: u64, pub step_ends: Vec, pub local_colors: Vec, pub mesh: OracleMesh, pub children: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleColor { pub code: u16, pub slot: u16, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleToken { pub kind: String, pub kind_code: i32, pub value: serde_json::Value, pub line: i32, pub column: i32, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleTokenLine { pub index: usize, pub text: String, pub tokens: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleTokenizedModel { pub model_index: usize, pub name: String, pub lines: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleGeometry { pub triangles: usize, pub quads: usize, pub lines: usize, pub conditional_lines: usize, pub includes: usize, pub triangle_positions: Vec>, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleDiagnostic { pub message: String, pub token: String, pub fatal: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleError { pub kind: String, pub message: String, pub diagnostics: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleAuthorityRule { pub path: String, pub class: String, pub rule: String, pub evidence: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleAuthorityTable { pub version: String, pub classes: Vec, pub rules: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct OracleProjection { pub schema_version: String, pub oracle_revision: String, pub wrapper_schema_version: String, pub projection_rule_version: String, pub patch_identity: String, pub fixture_path: String, pub fixture_sha256: String, pub authority_rules: OracleAuthorityTable, pub status: String, pub model_names: Vec, pub root_model: Option, pub models: Vec, pub tokenized_models: Vec, pub tokenized_geometry: Option, pub resolved_geometry: Option, pub diagnostics: Vec, pub error: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum DerivationAuthority { OracleDirect, OracleMechanical, InputMechanical, PolymodelPolicy, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DerivationRef { pub field: String, pub authority: DerivationAuthority, pub source: String, pub revision: String, pub locator: String, pub basis: String, pub decision: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct GeneratedMetadata { pub authority: String, pub oracle_revision: String, pub wrapper_schema_version: String, pub projection_rule_version: String, pub patch_identity: String, pub fixture_path: String, pub fixture_sha256: String, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct PolicyExpected { pub limits: Vec, pub texmap_events: Vec, pub texmap_geometry: Vec, pub data_payloads: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ExpectedSyntaxRecord { pub line_type: u8, pub raw_tail: String, pub fields: Vec, } pub fn compare_syntax_records( fixture_id: &str, expected: &[ExpectedSyntaxRecord], actual: &[serde_json::Value], ) -> Result<(), String> { if expected.len() != actual.len() { return Err(format!( "{fixture_id} syntax record count mismatch: expected={} actual={}", expected.len(), actual.len() )); } for (index, (expected, actual)) in expected.iter().zip(actual).enumerate() { let actual_type = actual.get("line_type").and_then(serde_json::Value::as_u64); if actual_type != Some(u64::from(expected.line_type)) { return Err(format!( "{fixture_id} syntax[{index}] line type mismatch: expected={} actual={actual_type:?}", expected.line_type )); } let actual_raw_tail = actual .get("raw_tail") .and_then(serde_json::Value::as_str) .unwrap_or_default(); if actual_raw_tail != expected.raw_tail { return Err(format!( "{fixture_id} syntax[{index}] raw tail mismatch: expected={:?} actual={actual_raw_tail:?}", expected.raw_tail )); } let actual_fields = match actual.get("fields").and_then(serde_json::Value::as_array) { Some(fields) => fields .iter() .map(|value| { value .as_str() .ok_or_else(|| { format!("{fixture_id} syntax[{index}] field is not a string: {value:?}") }) .map(String::from) }) .collect::, _>>()?, None => Vec::new(), }; let expected_fields = expected .fields .iter() .map(String::as_str) .collect::>(); if actual_fields != expected_fields { return Err(format!( "{fixture_id} syntax[{index}] fields mismatch: expected={expected_fields:?} actual={actual_fields:?}" )); } } Ok(()) } #[derive(Debug, Clone, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct ExpectedRecord { pub outcome: ExpectedOutcome, pub profile: Profile, #[serde(default)] pub syntax_expected: Vec, pub oracle_expected: OracleProjection, pub polymodel_policy_expected: PolicyExpected, pub derivations: Vec, pub generated: GeneratedMetadata, } impl<'de> Deserialize<'de> for ExpectedRecord { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct Wire { outcome: ExpectedOutcome, profile: Profile, #[serde(default)] syntax_expected: Vec, oracle_expected: OracleProjection, polymodel_policy_expected: PolicyExpected, derivations: Vec, generated: GeneratedMetadata, } let wire = Wire::deserialize(deserializer)?; if wire.generated.authority != "ldparse_oracle_with_policy" { return Err(serde::de::Error::custom( "expected record is not an LDParse oracle projection", )); } Ok(Self { outcome: wire.outcome, profile: wire.profile, syntax_expected: wire.syntax_expected, oracle_expected: wire.oracle_expected, polymodel_policy_expected: wire.polymodel_policy_expected, derivations: wire.derivations, generated: wire.generated, }) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Provenance { pub id: String, pub source_url: Option, pub source_revision: String, pub sha256: String, pub license: String, pub original_test: String, pub modifications: String, pub authority: AuthorityClass, } #[derive(Debug, Clone, PartialEq)] pub struct FixtureCase { pub id: &'static str, pub relative_path: &'static str, pub bytes: &'static [u8], pub tags: &'static [&'static str], pub profiles: &'static [Profile], pub profile: Profile, pub outcome: ExpectedOutcome, pub authority: AuthorityClass, pub provenance: Provenance, pub expected: ExpectedRecord, } impl FixtureCase { pub fn sha256(&self) -> String { hex_digest(self.bytes) } pub fn applies_to(&self, profile: Profile) -> bool { self.profiles.contains(&profile) && self.profile == profile } } pub fn serialize_candidate_observation( record: &T, ) -> Result, serde_json::Error> { serde_json::to_vec(record) } pub fn serialize_expected(record: &ExpectedRecord) -> Result, serde_json::Error> { serde_json::to_vec(record) } pub fn parse_expected(json: &str) -> Result { serde_json::from_str(json) } pub fn hex_digest(bytes: &[u8]) -> String { Sha256::digest(bytes) .iter() .map(|byte| format!("{byte:02x}")) .collect() } pub fn normalize_certification(value: &str) -> Option<&'static str> { match value { "indeterminate" => Some("unknown"), "true" | "certified" => Some("certified"), "false" | "uncertified" => Some("uncertified"), "unknown" => Some("unknown"), _ => None, } } pub fn ordered_colors(colors: &[OracleColor]) -> Vec<(u16, u16)> { colors .iter() .map(|color| (color.code, color.slot)) .collect() } pub fn unique_strings(values: impl IntoIterator) -> bool { let values = values.into_iter().collect::>(); values.len() == values.iter().collect::>().len() } const REQUIRED_DERIVATION_GROUPS: &[(&str, &[&str])] = &[ ("syntax", &["syntax_expected"]), ("policy/limits", &["polymodel_policy_expected.limits"]), ( "policy/texmap", &[ "polymodel_policy_expected.texmap_events", "polymodel_policy_expected.texmap_geometry", ], ), ("policy/data", &["polymodel_policy_expected.data_payloads"]), ]; fn derivation_group(field: &str) -> Option<&'static str> { match field { "polymodel_policy_expected.limits" => Some("policy/limits"), "polymodel_policy_expected.texmap_events" | "polymodel_policy_expected.texmap_geometry" => { Some("policy/texmap") } "polymodel_policy_expected.data_payloads" => Some("policy/data"), "syntax_expected" => Some("syntax"), _ => None, } } fn validate_oracle_expected(projection: &OracleProjection, fixture_id: &str) -> Result<(), String> { match (projection.status.as_str(), projection.error.is_some()) { ("ok", false) | ("error", true) => {} (status, has_error) => { return Err(format!( "invalid oracle projection status/error shape {fixture_id}: status={status:?}, error={has_error}" )); } } let mut names = BTreeSet::new(); for (index, model) in projection.models.iter().enumerate() { if model.name.is_empty() { return Err(format!("oracle model has empty name {fixture_id}")); } if model.model_index != index as u32 { return Err(format!( "oracle model indices are not sequential {fixture_id}" )); } if !names.insert(&model.name) { return Err(format!( "duplicate oracle model name {fixture_id}: {}", model.name )); } if normalize_certification(&model.certify).is_none() { return Err(format!( "invalid oracle model certification {fixture_id}: {}", model.certify )); } } Ok(()) } const ORACLE_AUTHORITY_CLASSES: &[&str] = &[ "oracle_direct", "oracle_mechanical", "input_mechanical", "polymodel_policy", ]; const ORACLE_AUTHORITY_ROOTS: &[&str] = &[ "outcome", "profile", "syntax_expected", "oracle_expected", "polymodel_policy_expected", "derivations", ]; pub fn validate_oracle_authority_table( table: &OracleAuthorityTable, fixture_id: &str, ) -> Result<(), String> { if table.version != ORACLE_PROJECTION_RULE_VERSION { return Err(format!( "oracle authority revision mismatch {fixture_id}: {}", table.version )); } if table.classes != ORACLE_AUTHORITY_CLASSES { return Err(format!("oracle authority classes mismatch {fixture_id}")); } let mut paths = BTreeSet::new(); for rule in &table.rules { if rule.path.trim().is_empty() || rule.path.contains('*') { return Err(format!( "oracle authority path is not explicit {fixture_id}: {}", rule.path )); } if !ORACLE_AUTHORITY_CLASSES.contains(&rule.class.as_str()) { return Err(format!( "unknown oracle authority class {fixture_id}: {}", rule.class )); } if rule.rule.trim().is_empty() || rule.evidence.trim().is_empty() { return Err(format!( "incomplete oracle authority rule {fixture_id}: {}", rule.path )); } if !paths.insert(rule.path.as_str()) { return Err(format!( "duplicate oracle authority rule {fixture_id}: {}", rule.path )); } } for root in ORACLE_AUTHORITY_ROOTS { if !paths .iter() .any(|path| *path == *root || path.starts_with(&format!("{root}."))) { return Err(format!( "oracle authority table does not cover {root} {fixture_id}" )); } } Ok(()) } pub fn validate_expected_evidence( expected: &ExpectedRecord, fixture_id: &str, ) -> Result<(), String> { if expected.generated.authority != "ldparse_oracle_with_policy" { return Err(format!( "expected record is not an LDParse oracle projection {fixture_id}" )); } if expected.derivations.is_empty() { return Err(format!("missing derivation evidence {fixture_id}")); } let mut groups = BTreeSet::new(); for derivation in &expected.derivations { if derivation.field.trim().is_empty() || derivation.source.trim().is_empty() || derivation.revision.trim().is_empty() || derivation.locator.trim().is_empty() || derivation.basis.trim().is_empty() { return Err(format!("incomplete derivation evidence {fixture_id}")); } if derivation.basis.contains("Generated from") || derivation.basis.contains("cargo run") || derivation.basis.contains("refresh") { return Err(format!( "derivation evidence claims generated or refreshed output {fixture_id}" )); } if !derivation.locator.contains(fixture_id) || !derivation.locator.contains("sha256:") || !derivation.revision.contains("PM-78") { return Err(format!( "derivation evidence lacks fixture/hash/revision binding {fixture_id}" )); } if !derivation.field.starts_with("polymodel_policy_expected.") && derivation.field != "syntax_expected" { return Err(format!( "derivation evidence must target syntax or policy expected fields {fixture_id}: {}", derivation.field )); } if !matches!( derivation.authority, DerivationAuthority::InputMechanical | DerivationAuthority::PolymodelPolicy ) { return Err(format!( "oracle field cannot use derivation evidence {fixture_id}: {}", derivation.field )); } let group = derivation_group(&derivation.field).ok_or_else(|| { format!( "unknown policy derivation field {} {fixture_id}", derivation.field ) })?; groups.insert(group); } for (group, _) in REQUIRED_DERIVATION_GROUPS { if !groups.contains(group) { return Err(format!("missing derivation group {group} {fixture_id}")); } } let projection = &expected.oracle_expected; validate_oracle_authority_table(&projection.authority_rules, fixture_id)?; if expected.generated.fixture_sha256 != projection.fixture_sha256 || expected.generated.fixture_path != projection.fixture_path || expected.generated.oracle_revision != projection.oracle_revision || expected.generated.wrapper_schema_version != projection.wrapper_schema_version || expected.generated.projection_rule_version != projection.projection_rule_version || expected.generated.patch_identity != projection.patch_identity { return Err(format!( "generated metadata does not match oracle projection {fixture_id}" )); } validate_oracle_expected(projection, fixture_id) } #[cfg(test)] mod tests { use super::*; #[test] fn expected_evidence_rejects_missing_derivations_before_oracle_data() { let expected = ExpectedRecord { outcome: ExpectedOutcome::Accepted, profile: Profile::Strict, syntax_expected: Vec::new(), oracle_expected: serde_json::from_str( r#"{ "schema_version":"ldraw-oracle-v4", "oracle_revision":"0fe78dcee22982f26cceced1b483340faa4d04e9", "wrapper_schema_version":"wrapper-v4", "projection_rule_version":"pm-78-oracle-only-v4", "patch_identity":"ldparse-oracle-access.patch@pm-78-v2", "fixture_path":"corpus/fixture.ldr", "fixture_sha256":"fixture", "authority_rules":{"version":"pm-78-oracle-only-v4","classes":[],"rules":[]}, "status":"error", "model_names":[], "root_model":null, "models":[], "tokenized_models":[], "tokenized_geometry":null, "resolved_geometry":null, "diagnostics":[], "error":{"kind":"test","message":"test","diagnostics":[]} }"#, ) .unwrap(), polymodel_policy_expected: PolicyExpected { limits: Vec::new(), texmap_events: Vec::new(), texmap_geometry: Vec::new(), data_payloads: Vec::new(), }, derivations: Vec::new(), generated: GeneratedMetadata { authority: "ldparse_oracle_with_policy".to_owned(), oracle_revision: String::new(), wrapper_schema_version: String::new(), projection_rule_version: String::new(), patch_identity: String::new(), fixture_path: String::new(), fixture_sha256: String::new(), }, }; assert_eq!( validate_expected_evidence(&expected, "fixture").unwrap_err(), "missing derivation evidence fixture" ); } }