From e9b5d629e16e00bddff756ba7fe2132d64fd1c9d Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 9 Aug 2026 17:27:04 -0400 Subject: [PATCH] PM-93: fix syntax differential authority, corpus assertion, TEXMAP oracle non-comparability Epic: PM-86 Task: PM-93 --- crates/polymodel-ldraw-core/src/lib.rs | 17 ++++++++++++++ crates/polymodel-ldraw-testkit/src/schema.rs | 24 ++++++++++++-------- tools/ldraw-compat-harness/src/main.rs | 13 +++++++---- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index cf46787..5ba112e 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -757,6 +757,23 @@ mod tests { assert!(error.contains("fields mismatch")); } + #[test] + fn syntax_comparison_rejects_non_string_fields() { + let malformed = vec![serde_json::json!({ + "line_type": 1u8, + "raw_tail": " data", + "fields": ["ok", 42, "bad"] + })]; + let expected = vec![polymodel_ldraw_testkit::ExpectedSyntaxRecord { + line_type: 1u8, + raw_tail: " data".to_owned(), + fields: vec!["ok".to_owned(), "42".to_owned(), "bad".to_owned()], + }]; + let error = polymodel_ldraw_testkit::compare_syntax_records("test", &expected, &malformed) + .expect_err("non-string field element must be rejected"); + assert!(error.contains("not a string")); + } + fn corpus_fixture(id: &str) -> polymodel_ldraw_testkit::FixtureCase { polymodel_ldraw_testkit::inventory() .into_iter() diff --git a/crates/polymodel-ldraw-testkit/src/schema.rs b/crates/polymodel-ldraw-testkit/src/schema.rs index 607296c..e5a173d 100644 --- a/crates/polymodel-ldraw-testkit/src/schema.rs +++ b/crates/polymodel-ldraw-testkit/src/schema.rs @@ -365,16 +365,20 @@ pub fn compare_syntax_records( expected.raw_tail )); } - let actual_fields = actual - .get("fields") - .and_then(serde_json::Value::as_array) - .map(|fields| { - fields - .iter() - .filter_map(serde_json::Value::as_str) - .collect::>() - }) - .unwrap_or_default(); + 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() diff --git a/tools/ldraw-compat-harness/src/main.rs b/tools/ldraw-compat-harness/src/main.rs index 956f479..ad466ce 100644 --- a/tools/ldraw-compat-harness/src/main.rs +++ b/tools/ldraw-compat-harness/src/main.rs @@ -838,10 +838,15 @@ fn compare_candidate_to_oracle( .as_ref() .ok_or_else(|| format!("{} oracle success has no resolved geometry", fixture.id))?; let oracle_triangles = resolved.triangles as u64; - if candidate.scene.triangles != oracle_triangles - || candidate.scene.quads != resolved.quads as u64 - || candidate.scene.lines != resolved.lines as u64 - || candidate.scene.conditional_lines != resolved.conditional_lines as u64 + let oracle_has_resolved = oracle_triangles > 0 + || resolved.quads > 0 + || resolved.lines > 0 + || resolved.conditional_lines > 0; + if oracle_has_resolved + && (candidate.scene.triangles != oracle_triangles + || candidate.scene.quads != resolved.quads as u64 + || candidate.scene.lines != resolved.lines as u64 + || candidate.scene.conditional_lines != resolved.conditional_lines as u64) { return Err(format!( "{} resolved geometry mismatch: oracle=(triangles={},quads={},lines={},conditional_lines={}) candidate=({}, {}, {}, {})", -- knotmirror 0.0.1