From 162dd3eed2873efdc58e292bff82ca71e6d43de4 Mon Sep 17 00:00:00 2001 From: ian691 Date: Tue, 20 Oct 2026 20:52:11 +0200 Subject: [PATCH 001/491] difftest: choose traversal operations with present entities Co-Authored-By: Codex --- .../src/units/topology_topology_traverser.rs | 78 ++++++++++++++++++- plan/findings.md | 11 +++ plan/modules.toml | 2 +- .../difftest/topology.topology_traverser.json | 58 ++++---------- 4 files changed, 105 insertions(+), 44 deletions(-) diff --git a/crates/difftest/src/units/topology_topology_traverser.rs b/crates/difftest/src/units/topology_topology_traverser.rs index 327487fbb1..304ca1933b 100644 --- a/crates/difftest/src/units/topology_topology_traverser.rs +++ b/crates/difftest/src/units/topology_topology_traverser.rs @@ -93,6 +93,34 @@ fn available(r: Request) -> bool { && (r.op != 6 || r.start != r.target) } +// Generator eligibility is stricter than `available`: explicit fixtures may use +// null starts, while random cases should exercise entities the fixture contains. +fn eligible_ops(c: [usize; 10]) -> Vec { + (0..8) + .filter(|op| match op { + 0 => c[3] > 0, + 1 => c[7] > 0, + 2 | 3 => c[2] > 0, + 4 => c[9] > 0 && c[1] > 0, + 5 | 7 => c[2] > 0 && c[1] > 0, + 6 => c[1] > 1, + _ => false, + }) + .collect() +} + +fn select_generated_op(i: &mut Inputs, counts: [usize; 10], rng: &mut Rng) { + let ops = eligible_ops(counts); + if let Some(&op) = rng.choose(&ops) { + i.op = op; + if op == 6 { + let shells = counts[1] as u64; + i.start = rng.below(shells) as i64; + i.target = (i.start + 1 + rng.below(shells - 1) as i64) % shells as i64; + } + } +} + impl UnitDiff for Runner { fn id(&self) -> &'static str { "topology.topology_traverser" @@ -303,7 +331,7 @@ impl UnitDiff for Runner { } fn generate(&self, rng: &mut Rng) -> Option { - let i = Inputs { + let mut i = Inputs { brep: shared::gen_brep(rng), op: rng.below(8) as i32, start: rng.below(10) as i64, @@ -314,6 +342,54 @@ impl UnitDiff for Runner { prefill: rng.below(2) != 0, repeat: 1 + rng.below(2) as i32, }; + // Failed exports and empty graphs keep their original case so errors + // remain visible. Never stop generation or discard a difficult input. + if let Some(counts) = export(&i).ok().and_then(|ex| fixture::counts(&ex.graph)) { + select_generated_op(&mut i, counts, rng); + } serde_json::to_value(i).ok() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generator_requires_nonempty_entity_kinds() { + assert!(eligible_ops([0; 10]).is_empty()); + assert_eq!(eligible_ops([1, 1, 0, 0, 0, 0, 2, 1, 2, 2]), vec![1, 4]); + assert_eq!(eligible_ops([1, 1, 2, 1, 2, 1, 8, 4, 8, 4]), vec![0, 1, 2, 3, 4, 5, 7]); + assert_eq!(eligible_ops([1, 2, 4, 2, 4, 2, 16, 8, 16, 8]), (0..8).collect::>()); + } + + #[test] + fn generated_shell_removal_uses_distinct_shells() { + let mut rng = Rng::new(7); + for shells in [2, 3] { + let counts = [1, shells, 0, 0, 0, 0, 0, 0, 0, 0]; + for _ in 0..100 { + let mut i = Inputs { + brep: BrepIn::Network, op: 0, start: 0, target: 0, + mask: 1, want: 3, pre: 0, prefill: false, repeat: 1, + }; + select_generated_op(&mut i, counts, &mut rng); + let r = request(&i, counts); + assert_eq!(r.op, 6); + assert_ne!(r.start, r.target); + assert!(available(r)); + } + } + } + + #[test] + fn empty_graph_keeps_the_original_case() { + let mut i = Inputs { + brep: BrepIn::Network, op: 6, start: 8, target: 3, + mask: 2, want: 1, pre: 4, prefill: true, repeat: 2, + }; + let before = serde_json::to_value(&i).unwrap(); + select_generated_op(&mut i, [0; 10], &mut Rng::new(7)); + assert_eq!(serde_json::to_value(&i).unwrap(), before); + } +} diff --git a/plan/findings.md b/plan/findings.md index 363210743f..be05eb897b 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -10538,3 +10538,14 @@ with maximum relative error 0. `sm_GetEllipseTrimIntervals` through `IntersectWithCone`, which has no such guard yet. Reproduced with main's unpatched `cone/p2.rs` too. Tracked in issue #276; the `surface.cone_p2` refusals that are now stricter than needed are issue #277. + + +### 2026-10-20 audit #294: topology.topology_traverser + +Random traversal cases could choose entity kinds absent from their native fixture and pass by comparing the same missing-seed error. Select uniformly among operations whose required entity kinds exist, and give RemoveShell distinct source and target shells. Explicit null/missing-seed fixtures remain unchanged. + +On the 1,000-seed comparison, substantive cases increased from 1,478 to 1,678; all 1,893 case identities remain. The 118 fixed missing-seed error cases remain. Six default deferrals select different operations after resampling; no missing kernel paths were implemented. All eight operations are represented. Runtime increased from 35.67 s to 44.11 s on this Mac. + +Validation: native targeted parity and serial native regressions were rerun after incorporating upstream `bb137d7d`. Original optimized before/after evidence used upstream `7dd82727`; latest-base checks use difftest profile with opt-level 0. The pinned C++ reference remains `c9c5979`. Independent GPT-6 Astra review approved the code and named-case transition evidence. Linux full-suite CI remains required before merge. + +Local full-suite limitation: `difftest --green` aborts with exit 134 after `nlib.math`; the untouched `7dd82727` baseline binary independently aborts with exit 134 for the next unit, `topology.database_io`. The macOS crash report places the stack-protector abort in SmDatabaseIO::ReadType, consistent with the already-recorded UB-5 in plan/upstream-issues.md. The exact triggering case is not isolated here. This is not a full-suite pass. diff --git a/plan/modules.toml b/plan/modules.toml index 626f20ac19..16c0096731 100644 --- a/plan/modules.toml +++ b/plan/modules.toml @@ -2688,7 +2688,7 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL (PR #113): difftest 909 cases, 889 passed, 0 failed, 20 deferred: topology.face_p3(5) (were topology.face_p1 before PR #148 wired FlipFaceOrientation), topology.face_p4(15) (re-run in PR #154: the 7 topology.shell deferrals pass, RemoveShell / FaceuseShellReplacement wired to topology::shell); exact traversal order and all face/edge/vertex/faceuse/vertexuse marks compared, 0 absolute floors; wire recursion replaced by an explicit continuation stack in the same order (6000-edge regression). No existing UNPORTED(topology.topology_traverser) back-edges. 14 fns; largest: sm_ShellConnectivity, SmTopologyTraverser::IsInnerShell, sm_CollectFaces, SmTopologyTraverser::FaceuseTraversal; cycle back-edges (call into later units; stub or co-port): topology.face_p3(1); unit-SCC #22 (220 units); 200 upstream test entry points reach this unit PR #163 exposes the existing co-ported face_edges helper for merge_p1 reuse; it keeps duplicates between loops and deduplicates within each loop (regression added). PR #169 exposes its existing co-ported has_lamina_edge for brep_p6 reuse; no duplicated face algorithm. Re-run: 909 cases, 889 pass, 20 deferred, 0 failures, max_rel_err 0. [topology.face_p3 wired (PR #139): 909 cases, 889 pass, 0 failed, 20 deferred (topology.face_p4 20; the 5 topology.face_p3 deferrals now defer inside it)]" +notes = "2026-10-20 audit #294: generator-only eligibility uses native entity counts, requires nonempty starts, samples eligible operations uniformly and uses distinct shells for RemoveShell. Fixed null/missing-seed fixtures remain unchanged. Default 909 cases: 895 pass, 14 deferred, 0 failed; 777 compare values/integers, 118 matched-error passes. PARTIAL (PR #113): difftest 909 cases, 889 passed, 0 failed, 20 deferred: topology.face_p3(5) (were topology.face_p1 before PR #148 wired FlipFaceOrientation), topology.face_p4(15) (re-run in PR #154: the 7 topology.shell deferrals pass, RemoveShell / FaceuseShellReplacement wired to topology::shell); exact traversal order and all face/edge/vertex/faceuse/vertexuse marks compared, 0 absolute floors; wire recursion replaced by an explicit continuation stack in the same order (6000-edge regression). No existing UNPORTED(topology.topology_traverser) back-edges. 14 fns; largest: sm_ShellConnectivity, SmTopologyTraverser::IsInnerShell, sm_CollectFaces, SmTopologyTraverser::FaceuseTraversal; cycle back-edges (call into later units; stub or co-port): topology.face_p3(1); unit-SCC #22 (220 units); 200 upstream test entry points reach this unit PR #163 exposes the existing co-ported face_edges helper for merge_p1 reuse; it keeps duplicates between loops and deduplicates within each loop (regression added). PR #169 exposes its existing co-ported has_lamina_edge for brep_p6 reuse; no duplicated face algorithm. Re-run: 909 cases, 889 pass, 20 deferred, 0 failures, max_rel_err 0. [topology.face_p3 wired (PR #139): 909 cases, 889 pass, 0 failed, 20 deferred (topology.face_p4 20; the 5 topology.face_p3 deferrals now defer inside it)]" [[unit]] id = "query.face_p3" diff --git a/reports/difftest/topology.topology_traverser.json b/reports/difftest/topology.topology_traverser.json index 8ba0570500..e0273a9bf1 100644 --- a/reports/difftest/topology.topology_traverser.json +++ b/reports/difftest/topology.topology_traverser.json @@ -7328,50 +7328,34 @@ "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GetUpwardLoopuses", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0001", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926657", "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GlobalLineIntersect", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0002", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926658", "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GetUpwardLoopuses", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0003", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926659", "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GlobalLineIntersect", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0004", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926660", "values_compared": 0 }, @@ -7384,7 +7368,6 @@ "values_compared": 0 }, { - "both_errored": true, "max_rel_err": 0.0, "name": "gen0006", "outcome": "pass", @@ -7433,31 +7416,22 @@ "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GetUpwardLoopuses", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0012", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926668", "values_compared": 0 }, { - "deferred": { - "symbol": "SmFace::GlobalLineIntersect", - "unit": "topology.face_p4" - }, "max_rel_err": 0.0, "name": "gen0013", - "outcome": "deferred", - "pass": false, + "outcome": "pass", + "pass": true, "source": "generated:seed=12051413912706926669", "values_compared": 0 }, { - "both_errored": true, "max_rel_err": 0.0, "name": "gen0014", "outcome": "pass", @@ -7474,13 +7448,13 @@ "values_compared": 0 } ], - "cases_both_errored": 120, - "cases_deferred": 20, + "cases_both_errored": 118, + "cases_deferred": 14, "cases_failed": 0, - "cases_passed": 889, + "cases_passed": 895, "cases_total": 909, "deferred_by_unit": { - "topology.face_p4": 20 + "topology.face_p4": 14 }, "max_rel_err": 0.0, "pass": true, From 5115a4eb80d185d62a6bda695b6300c8f618770d Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 01:42:27 -0400 Subject: [PATCH 002/491] fix: topology_intersector FFI refuses surface points in the overflow hang range (HANG-46) `--fuzz 30000` of boolean.topology_intersector_p1 and _p2 timed out on the C++ side for a surface control point of +-1e300: face/face, coincidence, edge/face and full intersections do not return. Replaying fuzz05142 with that coordinate replaced: 1e154 and above give no result in 60 s, 1e150 and below return in 0.1 s. The shared FFI reader now refuses surface control point coordinates of MAX_SURFACE_COORD = 1e154 or more (as _p2's HANG_COORD already did for IntersectInsertRelate alone). The four inputs are kept as regressions in crates/difftest/tests/fuzz_regressions/ with a test that the FFI refuses them. HANG-46 in plan/upstream-issues.md; the slow approx = 1e300 cases (22-25 s, valid input, accepted) are noted in plan/findings.md. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../units/boolean_topology_intersector_p1.rs | 39 + .../boolean.topology_intersector_p1.json | 771 ++++++++++++++++++ .../boolean.topology_intersector_p2.json | 161 ++++ .../units/boolean_topology_intersector_p1.rs | 18 +- plan/findings.md | 7 + plan/upstream-issues.md | 9 + 6 files changed, 1001 insertions(+), 4 deletions(-) create mode 100644 crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p1.json create mode 100644 crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index e691dd2f28..d6cea13e85 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -4840,3 +4840,42 @@ fn edge_cases() -> Vec<(String, Value)> { } out } + +#[cfg(test)] +mod tests { + use super::*; + use crate::case::CaseSource; + + /// HANG-46: the `--fuzz 30000` inputs whose surface control point of `1e300` kept + /// the C++ intersection busy past the timeout, for this unit and `_p2` (which + /// shares this reader). They cannot be fixtures (a harness error always fails a + /// case), so this test keeps them: the FFI refuses each before any construction. + #[cfg(feature = "cpp")] + #[test] + fn ffi_refuses_surface_points_in_the_hang_range() { + let p2 = super::super::boolean_topology_intersector_p2::Runner; + let files: [(&str, &dyn UnitDiff); 2] = [ + ( + include_str!("../../tests/fuzz_regressions/boolean.topology_intersector_p1.json"), + &Runner, + ), + ( + include_str!("../../tests/fuzz_regressions/boolean.topology_intersector_p2.json"), + &p2, + ), + ]; + for (text, runner) in files { + let recorded: Value = serde_json::from_str(text).unwrap(); + for recorded in recorded["cases"].as_array().unwrap() { + let case = Case { + unit: runner.id().into(), + name: recorded["name"].as_str().unwrap().into(), + source: CaseSource::Inline, + inputs: recorded["inputs"].clone(), + }; + let error = runner.run_cpp(&case).unwrap_err().to_string(); + assert!(error.contains("surface: point out of range"), "{}: {error}", case.name); + } + } + } +} diff --git a/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p1.json b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p1.json new file mode 100644 index 0000000000..f7ec9824eb --- /dev/null +++ b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p1.json @@ -0,0 +1,771 @@ +{ + "cases": [ + { + "name": "fuzz05142_huge_surface_point", + "fuzz": { + "base": "gen0225", + "mutations": [ + "/brep_b/surface/bspline/net/1/0/1 = 1e+300" + ], + "seed": 17443733601064666790 + }, + "inputs": { + "ang": 0.0, + "approx": 0.0, + "brep_a": { + "dom": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + 0.09484455299885719, + -1.7976931348623157e+308 + ], + [ + 0.0, + 1.0, + -0.06439661725279089, + -1.7976931348623157e+308 + ], + [ + 0.0, + 2.0, + -0.10413275270617106, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1.0, + 0.0, + 0.3288814648573527, + -1.7976931348623157e+308 + ], + [ + 1.0, + 1.0, + 0.005970316868709635, + -1.7976931348623157e+308 + ], + [ + 1.0, + 2.0, + -0.35906844043669683, + -1.7976931348623157e+308 + ] + ], + [ + [ + 2.0, + 0.0, + -0.4418729093998027, + -1.7976931348623157e+308 + ], + [ + 2.0, + 1.0, + 0.07064902890385782, + -1.7976931348623157e+308 + ], + [ + 2.0, + 2.0, + 0.056399256710881085, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3.0, + 0.0, + -0.2791197258858432, + -1.7976931348623157e+308 + ], + [ + 3.0, + 1.0, + 0.23036266099961122, + -1.7976931348623157e+308 + ], + [ + 3.0, + 2.0, + -0.4934435645946934, + -1.7976931348623157e+308 + ] + ], + [ + [ + 4.0, + 0.0, + 0.2664045472022938, + -1.7976931348623157e+308 + ], + [ + 4.0, + 1.0, + 0.10563100761797539, + -1.7976931348623157e+308 + ], + [ + 4.0, + 2.0, + 0.10520677359636854, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 2, + "q": 1, + "u": [ + 0.0, + 0.0, + 0.0, + 0.21039741497800996, + 0.6821609267293149, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.7080026231242149, + 1.0, + 1.0 + ] + } + } + }, + "brep_b": { + "dom": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + 0.14677284727019535, + -1.7976931348623157e+308 + ], + [ + 0.0, + 1.0, + -0.21976612896721426, + -1.7976931348623157e+308 + ], + [ + 0.0, + 2.0, + -0.46608286037599944, + -1.7976931348623157e+308 + ], + [ + 0.0, + 3.0, + 0.07304731974557765, + -1.7976931348623157e+308 + ], + [ + 0.0, + 4.0, + 0.40152900354373955, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1.0, + 1e+300, + 0.42863090354036804, + -1.7976931348623157e+308 + ], + [ + 1.0, + 1.0, + 0.22979537203038702, + -1.7976931348623157e+308 + ], + [ + 1.0, + 2.0, + -0.35695910673680165, + -1.7976931348623157e+308 + ], + [ + 1.0, + 3.0, + 0.4820128034736554, + -1.7976931348623157e+308 + ], + [ + 1.0, + 4.0, + 0.047549419336499366, + -1.7976931348623157e+308 + ] + ], + [ + [ + 2.0, + 0.0, + -0.32683307582022847, + -1.7976931348623157e+308 + ], + [ + 2.0, + 1.0, + 0.40213618730985634, + -1.7976931348623157e+308 + ], + [ + 2.0, + 2.0, + -0.4598376596467002, + -1.7976931348623157e+308 + ], + [ + 2.0, + 3.0, + 0.18218527514551341, + -1.7976931348623157e+308 + ], + [ + 2.0, + 4.0, + -0.2793306545129983, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3.0, + 0.0, + 0.4336095184756382, + -1.7976931348623157e+308 + ], + [ + 3.0, + 1.0, + -0.12708704998180398, + -1.7976931348623157e+308 + ], + [ + 3.0, + 2.0, + -0.3613276455075588, + -1.7976931348623157e+308 + ], + [ + 3.0, + 3.0, + 0.2706656331262641, + -1.7976931348623157e+308 + ], + [ + 3.0, + 4.0, + -0.09917104287198775, + -1.7976931348623157e+308 + ] + ], + [ + [ + 4.0, + 0.0, + -0.11768545600854285, + -1.7976931348623157e+308 + ], + [ + 4.0, + 1.0, + -0.30762935201041675, + -1.7976931348623157e+308 + ], + [ + 4.0, + 2.0, + -0.004107808429224202, + -1.7976931348623157e+308 + ], + [ + 4.0, + 3.0, + -0.06159733948856805, + -1.7976931348623157e+308 + ], + [ + 4.0, + 4.0, + -0.19901880849510545, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 2, + "q": 2, + "u": [ + 0.0, + 0.0, + 0.0, + 0.4556434990018121, + 0.8950742880669398, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.0, + 0.26328901260444587, + 0.5654098627252987, + 1.0, + 1.0, + 1.0 + ] + } + } + }, + "ctor": 0, + "ops": [ + { + "h": [ + 1, + 3, + 27270 + ], + "op": "other_mate" + }, + { + "edges": [ + [ + 1, + 1, + 40109 + ] + ], + "faces": [ + [ + 1, + 2, + 42526 + ] + ], + "ignore": false, + "nesting": true, + "op": "face_face_coin" + }, + { + "capture": 0.01, + "op": "mgr_new", + "override_tol": 0.0 + }, + { + "first": 0, + "lamina": true, + "op": "wire_lamina", + "swap": false + } + ] + } + }, + { + "name": "fuzz17005_huge_surface_point", + "fuzz": { + "base": "gen0011", + "mutations": [ + "/brep_a/surface/bspline/net/1/0/1 = -1e+300" + ], + "seed": 17443733601064678653 + }, + "inputs": { + "ang": 0.1, + "approx": 1e-05, + "brep_a": { + "dom": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + 6.14156922975908e-05, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.001, + 0.00011030310017403589, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.002, + 6.416079488617265e-05, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.001, + -1e+300, + 0.00024995019135438744, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.001, + -0.00017786813217852472, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.002, + -0.00023955169792619092, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.002, + 0.0, + 0.0003505443146683263, + -1.7976931348623157e+308 + ], + [ + 0.002, + 0.001, + -0.00016314865915935384, + -1.7976931348623157e+308 + ], + [ + 0.002, + 0.002, + 0.0003107089290966193, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.003, + 0.0, + -0.00010222973666094814, + -1.7976931348623157e+308 + ], + [ + 0.003, + 0.001, + -0.00019023563931059716, + -1.7976931348623157e+308 + ], + [ + 0.003, + 0.002, + 0.00015744039067525618, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.004, + 0.0, + 0.00014525736986398686, + -1.7976931348623157e+308 + ], + [ + 0.004, + 0.001, + -0.0004934063069352071, + -1.7976931348623157e+308 + ], + [ + 0.004, + 0.002, + 0.00043343629110887837, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.005, + 0.0, + -0.00024549376687160886, + -1.7976931348623157e+308 + ], + [ + 0.005, + 0.001, + -0.00012923706453922735, + -1.7976931348623157e+308 + ], + [ + 0.005, + 0.002, + -0.0004185308206370565, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 3, + "q": 1, + "u": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.5502451306347363, + 0.8736732669655108, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.14268196526532223, + 1.0, + 1.0 + ] + } + } + }, + "brep_b": { + "file": "plane_square.smb", + "kind": "file", + "rebuild": true + }, + "ctor": 0, + "ops": [ + { + "op": "check_ssi", + "s1": [ + 0, + 4, + 54317 + ], + "s2": [ + 0, + 4, + 31190 + ] + }, + { + "ang": 0.04399515122550196, + "approx": 0.001, + "op": "set_tols" + }, + { + "edges": [ + [ + 0, + 1, + 48758 + ], + [ + 0, + 1, + 21092 + ], + [ + 1, + 1, + 30174 + ] + ], + "faces": [ + [ + 0, + 2, + 55266 + ], + [ + 1, + 2, + 62342 + ], + [ + 1, + 2, + 46173 + ] + ], + "ignore": true, + "manifold": false, + "op": "edge_face" + }, + { + "capture": 1e-05, + "op": "mgr_new", + "override_tol": 0.0 + }, + { + "first": 0, + "lamina": true, + "op": "wire_lamina", + "swap": true + } + ] + } + }, + { + "name": "fuzz27420_huge_surface_point", + "fuzz": { + "base": "gen0325", + "mutations": [ + "/brep_b/surface/bspline/net/1/0/0 = 1e+300" + ], + "seed": 17443733601064689068 + }, + "inputs": { + "ang": 0.3490658503988659, + "approx": 0.0001, + "brep_a": { + "file": "surf_rev.smb", + "kind": "file", + "rebuild": false + }, + "brep_b": { + "dom": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + 0.0002564416988382987, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.001, + 0.00047222174382138284, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.002, + 0.000417380736214401, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.003, + -6.56907843238318e-05, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1e+300, + 0.0, + -0.0003384366541685541, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.001, + -0.00042395675140557976, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.002, + -0.0004596728583800157, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.003, + -0.0004951146434853286, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 1, + "q": 3, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + } + }, + "ctor": 0, + "ops": [ + { + "faces1": [ + [ + 0, + 2, + 30590 + ] + ], + "faces2": [ + [ + 1, + 2, + 26022 + ] + ], + "ignore": false, + "nesting": false, + "op": "face_face", + "verts": [] + } + ] + } + } + ] +} diff --git a/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json new file mode 100644 index 0000000000..d3eed579f1 --- /dev/null +++ b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json @@ -0,0 +1,161 @@ +{ + "cases": [ + { + "name": "fuzz06354_huge_surface_point", + "fuzz": { + "base": "gen0011", + "mutations": [ + "/brep_b/rebuild = true", + "/brep_a/surface/bspline/net/1/4/1 = -1e+300" + ], + "seed": 17443736899599552635 + }, + "inputs": { + "ang": 0.1, + "approx": 0.01, + "brep_a": { + "dom": [ + 0.23681279216758705, + 0.08033138933760398, + 0.8932644086232581, + 0.8175444424227004 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + -446.56594467796293, + -1.7976931348623157e+308 + ], + [ + 0.0, + 1000.0, + 311.38330981708583, + -1.7976931348623157e+308 + ], + [ + 0.0, + 2000.0, + -78.03785075194392, + -1.7976931348623157e+308 + ], + [ + 0.0, + 3000.0, + 37.46012064578197, + -1.7976931348623157e+308 + ], + [ + 0.0, + 4000.0, + 98.49462627635641, + -1.7976931348623157e+308 + ], + [ + 0.0, + 5000.0, + 161.2149610325313, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1000.0, + 0.0, + -388.4017737179015, + -1.7976931348623157e+308 + ], + [ + 1000.0, + 1000.0, + 468.91172006304026, + -1.7976931348623157e+308 + ], + [ + 1000.0, + 2000.0, + -143.92262727635918, + -1.7976931348623157e+308 + ], + [ + 1000.0, + 3000.0, + 350.7256781478789, + -1.7976931348623157e+308 + ], + [ + 1000.0, + -1e+300, + 160.48878295143953, + -1.7976931348623157e+308 + ], + [ + 1000.0, + 5000.0, + -119.23814601168992, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 1, + "q": 3, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.42205376391238314, + 0.7141423721342484, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + } + }, + "brep_b": { + "file": "box_with_large_fillet.smb", + "kind": "file", + "rebuild": true + }, + "ctor": 0, + "ops": [ + { + "a": [ + 0, + 0, + 47738 + ], + "b": [ + 1, + 0, + 61550 + ], + "op": "relate" + }, + { + "capture": 1e-06, + "op": "mgr_new", + "override_tol": 0.0 + }, + { + "op": "intersect_topo" + } + ] + } + } + ] +} diff --git a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs index 36dc501f27..c9f7e02eae 100644 --- a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs +++ b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs @@ -90,6 +90,10 @@ pub const MAX_INDEX: usize = 32; pub const MAX_DEGREE: usize = 5; /// Largest coordinate magnitude. pub const MAX_COORD: f64 = 1.0e300; +/// Bound (exclusive) on surface control point coordinates: from about `1e154`, where +/// their squares overflow, upstream's face/face, edge/face and coincidence +/// intersections do not finish (HANG-46; below `1e150` they return at once). +pub const MAX_SURFACE_COORD: f64 = 1.0e154; /// Most ops in a script. pub const MAX_OPS: usize = 64; /// Most handles in one list. @@ -366,7 +370,13 @@ fn check_knots(knots: &[f64], p: usize, what: &str) -> Result<(f64, f64), FfiErr /// Checks `k` control points sharing their rational marker (`z = NOZ` allowed for /// curves when `planar`). -fn check_points(rd: &mut Reader<'_>, k: usize, planar: bool, what: &str) -> Result<(), FfiError> { +fn check_points( + rd: &mut Reader<'_>, + k: usize, + planar: bool, + bound: f64, + what: &str, +) -> Result<(), FfiError> { let mut w0 = None; for _ in 0..k { let x = rd.num()?; @@ -386,7 +396,7 @@ fn check_points(rd: &mut Reader<'_>, k: usize, planar: bool, what: &str) -> Resu let z = if z == NOZ { 0.0 } else { z }; if [x, y, z] .iter() - .any(|c| !c.is_finite() || c.abs() > MAX_COORD) + .any(|c| !c.is_finite() || c.abs() > MAX_COORD || c.abs() >= bound) { return Err(bad(format!("{what}: point out of range"))); } @@ -418,7 +428,7 @@ fn surface(rd: &mut Reader<'_>) -> Result { "surface: need 1 <= p <= n, 1 <= q <= m, r = n+p+1, s = m+q+1", )); } - check_points(rd, (n + 1) * (m + 1), false, "surface")?; + check_points(rd, (n + 1) * (m + 1), false, MAX_SURFACE_COORD, "surface")?; let u = rd.finites(r + 1)?; let v = rd.finites(s + 1)?; let (u0, u1) = check_knots(&u, p, "surface u")?; @@ -438,7 +448,7 @@ fn curve(rd: &mut Reader<'_>) -> Result<(f64, f64), FfiError> { if p == 0 || p > n || m != n + p + 1 { return Err(bad("curve: need 1 <= p <= n and m = n + p + 1")); } - check_points(rd, n + 1, dim == 2, "curve")?; + check_points(rd, n + 1, dim == 2, f64::INFINITY, "curve")?; let knots = rd.finites(m + 1)?; check_knots(&knots, p, "curve") } diff --git a/plan/findings.md b/plan/findings.md index 880076520f..b38afe4f80 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -11276,3 +11276,10 @@ guarded in `offset.offset_geometry_creation`; the fix belongs to the curve cache tessellation, before any intersection test, as in `fuzz00504`. - **Status.** Open, with `fuzz00504`. Reproduce with the fuzz output file as a fixture and the tolerance set to any accepted value. + +### boolean.topology_intersector_p1/_p2: `--fuzz 30000` (2026-10-21) + +- Hangs on surface control points of `1e154` or more: HANG-46 in `plan/upstream-issues.md`; the FFI reader now refuses them. +- Slow, not hung: `fuzz09748` (p1) and `fuzz15421` (p2) set `/approx = 1e300`. Replayed with `approx` from `1e3` to `1e300`, the C++ returns in 22-25 s every time (`1`: 0.5 s), just over the fuzz harness's fixed 20 s budget (`CASE_TIMEOUT`). These are valid upstream inputs, so the FFI accepts them; they stay `timeout` verdicts in the fuzz until the harness has a per-unit budget. +- 5 `differs` in p2 (`fuzz06628`, `fuzz08367`, `fuzz19033`, `fuzz19955`, `fuzz23628`): `values.state.len` is 3 shorter on the Rust side in each. Not yet assessed. + diff --git a/plan/upstream-issues.md b/plan/upstream-issues.md index 4e38152c7f..1e821a677d 100644 --- a/plan/upstream-issues.md +++ b/plan/upstream-issues.md @@ -1936,6 +1936,15 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Severity:** hang. - **Rust port:** Stops after `MAX_TOL_DOUBLINGS` (2200) doublings with `SolidError::InvalidInput`. +#### HANG-46. Topology intersections do not return for a surface control point of magnitude 1e154 or more +- **Location:** `source/SMLib/src/SmTopologyIntersector.cpp`, the face/face, face/face coincidence, edge/face and full (`IntersectInsertRelate`) intersections of `SmTopologyIntersector`; the stuck loop was not isolated (the same overflow class as HANG-16, HANG-19 and HANG-37). +- **What happens:** With one surface control point coordinate at `1e154` or beyond, the intersection gives no result within 60 s; at `1e150` and below the same script returns in 0.1 s. The squared distances of such coordinates overflow. +- **Trigger:** `--fuzz 30000` of `boolean.topology_intersector_p1`: `fuzz05142` (`face_face_coin` after `other_mate`, `/brep_b/surface/bspline/net/1/0/1 = 1e300`), `fuzz17005` (`check_ssi` then `edge_face`, `net/1/0/1 = -1e300`), `fuzz27420` (`face_face`, `net/1/0/0 = 1e300`); of `boolean.topology_intersector_p2`: `fuzz06354` (`relate`, `mgr_new`, `intersect_topo`, `/brep_a/surface/bspline/net/1/4/1 = -1e300`, other B-rep rebuilt from its file). +- **How found:** `--fuzz 30000` of both units on main `16f04231`; threshold by replaying `fuzz05142` with the coordinate replaced (60 s limit per run): `1e300`, `1e280`, `1e250`, `1e200`, `1e160`, `1e154` give no result; `1e150`, `1e100`, `1e50`, `1e10`, `1` return in 0.0-0.2 s. +- **Confirmed:** yes (no result within 60 s for six values; infinite execution not proven). +- **Severity:** hang on out-of-range geometry; not a parity blocker (no fixture or generated case reaches it). +- **Rust port:** the `boolean.topology_intersector_p1` FFI reader, shared with `_p2`, refuses surface control point coordinates of `MAX_SURFACE_COORD = 1e154` or more (the boundary of the measured hang, as `_p2`'s `HANG_COORD` for `IntersectInsertRelate`). + ## Nondeterminism #### ND-1. `srand(time(NULL))` in face sampling and shell classification From 6a52ff4dbf851620e6c13279918541e2a44dbe15 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 02:46:58 -0400 Subject: [PATCH 003/491] fix: HANG-29 guards in heal.gap, heal_data and fillet_geom_p2; record as HANG-29 gdb backtraces of the `--fuzz 30000` hangs show the same stuck loop in every unit: SmCurve::GlobalCoincidenceChecker splitting a face boundary forever once squared distances overflow (HANG-29). The topology intersector hang of the previous commit is HANG-29 too, so its separate HANG-46 entry is dropped and every case is recorded as a HANG-29 trigger. New guards (each measured by replaying a fuzz case with the coordinate replaced; regression inputs in crates/difftest/tests/fuzz_regressions/ with a test per unit): - heal.gap topology op: `check_topo_block` refuses a block value of 1e154 or more (fuzz14876; -1e155 hangs, -1e154 returns); the runner's Rust side runs it too. - heal.heal_data_p2/_p3: `check_block` refuses a B-rep block value of 1e154 or more (fuzz03677, fuzz22334, fuzz26581). - blend.fillet_geom_p2 RailRail: the original face's surface is bounded by fillet_geom_p1's MAX_MAGNITUDE (fuzz09135 and 6 more; -1e155 hangs, -1e154 returns). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/units/blend_fillet_geom_p2.rs | 21 + .../units/boolean_topology_intersector_p1.rs | 2 +- crates/difftest/src/units/heal_gap.rs | 27 +- .../difftest/src/units/heal_heal_data_p2.rs | 18 + .../blend.fillet_geom_p2.json | 419 ++++++++++++++++++ .../tests/fuzz_regressions/heal.gap.json | 227 ++++++++++ .../fuzz_regressions/heal.heal_data_p2.json | 297 +++++++++++++ .../src/units/blend_fillet_geom_p2.rs | 9 +- .../units/boolean_topology_intersector_p1.rs | 3 +- crates/solidcore-ffi/src/units/heal_gap.rs | 25 ++ .../src/units/heal_heal_data_p2.rs | 13 + plan/findings.md | 2 +- plan/upstream-issues.md | 15 +- 13 files changed, 1064 insertions(+), 14 deletions(-) create mode 100644 crates/difftest/tests/fuzz_regressions/blend.fillet_geom_p2.json create mode 100644 crates/difftest/tests/fuzz_regressions/heal.gap.json create mode 100644 crates/difftest/tests/fuzz_regressions/heal.heal_data_p2.json diff --git a/crates/difftest/src/units/blend_fillet_geom_p2.rs b/crates/difftest/src/units/blend_fillet_geom_p2.rs index e39416d2d5..8dcd3efa68 100644 --- a/crates/difftest/src/units/blend_fillet_geom_p2.rs +++ b/crates/difftest/src/units/blend_fillet_geom_p2.rs @@ -1154,6 +1154,27 @@ impl UnitDiff for Runner { mod tests { use super::*; + /// HANG-29: `--fuzz 30000` fuzz09135, `RailRail` whose original face has a control + /// point of `-1e300` (no native result in 60 s). Both sides refuse it. + #[test] + fn rail_rail_refuses_an_original_face_in_the_overflow_range() { + let recorded: Value = serde_json::from_str(include_str!( + "../../tests/fuzz_regressions/blend.fillet_geom_p2.json" + )) + .unwrap(); + for recorded in recorded["cases"].as_array().unwrap() { + let case = Case { + unit: "blend.fillet_geom_p2".into(), + name: recorded["name"].as_str().unwrap().into(), + source: crate::case::CaseSource::Inline, + inputs: recorded["inputs"].clone(), + }; + for error in [Runner.run_cpp(&case), Runner.run_rust(&case)] { + assert!(error.is_err(), "{}: {error:?}", case.name); + } + } + } + /// Tallies the Rust outcome of every case per query (status `0`, an upstream error /// status, a deferral): with no failed case, the passes' success-path / error-path /// split. `CASE_SEEDS` sets the generated count. diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index d6cea13e85..f2988c38c1 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -4846,7 +4846,7 @@ mod tests { use super::*; use crate::case::CaseSource; - /// HANG-46: the `--fuzz 30000` inputs whose surface control point of `1e300` kept + /// HANG-29: the `--fuzz 30000` inputs whose surface control point of `1e300` kept /// the C++ intersection busy past the timeout, for this unit and `_p2` (which /// shares this reader). They cannot be fixtures (a harness error always fails a /// case), so this test keeps them: the FFI refuses each before any construction. diff --git a/crates/difftest/src/units/heal_gap.rs b/crates/difftest/src/units/heal_gap.rs index f968ef526d..bb21cade86 100644 --- a/crates/difftest/src/units/heal_gap.rs +++ b/crates/difftest/src/units/heal_gap.rs @@ -1065,12 +1065,14 @@ fn run_rust(i: &Inputs) -> Result { f1, uv, } => { - let (_, _, ex) = topo_graph(brep)?; + let (_, block, ex) = topo_graph(brep)?; let Some(c) = counts(&ex) else { return Ok(state(vec![-1.0, ex.get(1).copied().unwrap_or(0.0)])); }; let (mut b, _) = import(&ex)?; let args = topo_args(&b, c, *kind, *sel0, *sel1, *f0, *f1, *uv)?; + // (the check `ffi::topo` runs at this point on the C++ side) + ffi::check_topo_block(&block).map_err(|e| harness(format!("c++: {e}")))?; match topo_rust(&k, &mut b, &args) { Ok(v) => o = v, Err(SolidError::InvalidInput { .. }) => o.push(NULL_DEREF), @@ -1461,3 +1463,26 @@ impl UnitDiff for Runner { serde_json::to_value(gen_inputs(rng)).ok() } } + +#[cfg(test)] +mod tests { + use super::*; + + /// HANG-29: fuzz14876, a topology op on a face whose B-spline surface has a control + /// point of `-1e300` (no native result in 60 s). Both sides refuse its block. It + /// cannot be a fixture (a harness error always fails a case). + #[test] + fn both_sides_refuse_a_topo_block_in_the_overflow_range() { + let recorded: Value = serde_json::from_str(include_str!( + "../../tests/fuzz_regressions/heal.gap.json" + )) + .unwrap(); + for recorded in recorded["cases"].as_array().unwrap() { + let i: Inputs = serde_json::from_value(recorded["inputs"].clone()).unwrap(); + for error in [run_rust(&i), run_cpp(&i)] { + let error = error.unwrap_err().to_string(); + assert!(error.contains("overflow range"), "{}: {error}", recorded["name"]); + } + } + } +} diff --git a/crates/difftest/src/units/heal_heal_data_p2.rs b/crates/difftest/src/units/heal_heal_data_p2.rs index de4018114f..8899f2395e 100644 --- a/crates/difftest/src/units/heal_heal_data_p2.rs +++ b/crates/difftest/src/units/heal_heal_data_p2.rs @@ -2006,6 +2006,24 @@ impl UnitDiff for Runner { mod tests { use super::*; + /// HANG-29: `--fuzz 30000` fuzz22334, a surface control point of `-1e300` in the + /// B-rep block; the healer's caching did not return. Both sides refuse the block + /// (the Rust side imports through the same validated export). + #[test] + fn both_sides_refuse_a_block_in_the_overflow_range() { + let recorded: Value = serde_json::from_str(include_str!( + "../../tests/fuzz_regressions/heal.heal_data_p2.json" + )) + .unwrap(); + for recorded in recorded["cases"].as_array().unwrap() { + let i: Inputs = serde_json::from_value(recorded["inputs"].clone()).unwrap(); + for error in [run_cpp(&i), run_rust(&i)] { + let error = error.unwrap_err().to_string(); + assert!(error.contains("overflow range"), "{}: {error}", recorded["name"]); + } + } + } + /// Object counts the C++ guard refuses are a harness error on the Rust side too, /// not a capacity-overflow panic or a 32 GiB object list (issue #181). #[test] diff --git a/crates/difftest/tests/fuzz_regressions/blend.fillet_geom_p2.json b/crates/difftest/tests/fuzz_regressions/blend.fillet_geom_p2.json new file mode 100644 index 0000000000..714e244cbb --- /dev/null +++ b/crates/difftest/tests/fuzz_regressions/blend.fillet_geom_p2.json @@ -0,0 +1,419 @@ +{ + "cases": [ + { + "name": "fuzz09135_hang29_surface_point", + "fuzz": { + "base": "gen0133", + "mutations": [ + "/query/face/b_spline/net/1/2/0 = -1e+300" + ], + "seed": 9522355457901207176 + }, + "inputs": { + "geom_solvers": [], + "model": { + "edges": [ + { + "corner": -1, + "etype": 11, + "eus": [ + { + "geom": -1, + "vu_uv": [ + 0.0, + 0.0 + ] + }, + { + "geom": -1, + "vu_uv": [ + 0.0, + 0.0 + ] + } + ], + "fbe": [ + -1, + -1 + ], + "status": 0, + "ve": 1, + "vs": 0 + }, + { + "corner": -1, + "curve": { + "cpts": [ + [ + 270.1072309700526, + 312.5462828946369, + 58.170988717651554, + -1.7976931348623157e+308 + ], + [ + 265.20916126663093, + 602.0048016265522, + 38.70086840334867, + -1.7976931348623157e+308 + ] + ], + "dim": 3, + "p": 1, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ] + }, + "etype": 9, + "eus": [ + { + "geom": -1, + "vu_uv": [ + 0.0, + 0.0 + ] + }, + { + "geom": -1, + "uv_curve": { + "cpts": [ + [ + 0.4191377385566351, + 0.2418910238792722, + 0.0, + -1.7976931348623157e+308 + ], + [ + 0.3633745502622246, + 0.5165239099820014, + 0.0, + -1.7976931348623157e+308 + ] + ], + "dim": 2, + "p": 1, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ] + }, + "vu_uv": [ + 0.0, + 0.0 + ] + } + ], + "fbe": [ + -1, + -1 + ], + "interval": [ + 0.0, + 1.0 + ], + "status": 0, + "ve": 2, + "vs": 0 + }, + { + "corner": -1, + "curve": { + "cpts": [ + [ + 490.1068097883441, + 615.2933805240965, + 101.93048886117796, + -1.7976931348623157e+308 + ], + [ + 357.7525045550069, + 308.91933595603143, + 64.61354894804764, + -1.7976931348623157e+308 + ] + ], + "dim": 3, + "p": 1, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ] + }, + "etype": 9, + "eus": [ + { + "geom": -1, + "vu_uv": [ + 0.0, + 0.0 + ] + }, + { + "geom": -1, + "uv_curve": { + "cpts": [ + [ + 0.6042330999477774, + 0.4883560002414041, + 0.0, + -1.7976931348623157e+308 + ], + [ + 0.5508869630758079, + 0.23321123600241173, + 0.0, + -1.7976931348623157e+308 + ] + ], + "dim": 2, + "p": 1, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ] + }, + "vu_uv": [ + 0.0, + 0.0 + ] + } + ], + "fbe": [ + -1, + -1 + ], + "interval": [ + 0.0, + 1.0 + ], + "status": 0, + "ve": 1, + "vs": 3 + } + ], + "faces": [], + "fillet_brep": [], + "geoms": [], + "targets": [], + "vertices": [ + { + "corner": -1, + "mates": [ + -1, + -1 + ], + "pc_class": 100, + "pc_face": -1, + "pc_index": -1, + "pc_kind": 0, + "pc_sub": 0, + "point": [ + 270.1072309700526, + 312.5462828946369, + 58.170988717651554 + ], + "status": 0, + "t_param": 0.0, + "uv": [ + 0.4191377385566351, + 0.2418910238792722 + ], + "vtype": 1 + }, + { + "corner": -1, + "mates": [ + -1, + -1 + ], + "pc_class": 100, + "pc_face": -1, + "pc_index": -1, + "pc_kind": 0, + "pc_sub": 0, + "point": [ + 357.7525045550069, + 308.91933595603143, + 64.61354894804764 + ], + "status": 0, + "t_param": 0.0, + "uv": [ + 0.5508869630758079, + 0.23321123600241173 + ], + "vtype": 3 + }, + { + "corner": -1, + "mates": [ + -1, + -1 + ], + "pc_class": 100, + "pc_face": -1, + "pc_index": -1, + "pc_kind": 0, + "pc_sub": 0, + "point": [ + 265.20916126663093, + 602.0048016265522, + 38.70086840334867 + ], + "status": 0, + "t_param": 0.0, + "uv": [ + 0.3633745502622246, + 0.5165239099820014 + ], + "vtype": 3 + }, + { + "corner": -1, + "mates": [ + -1, + -1 + ], + "pc_class": 100, + "pc_face": -1, + "pc_index": -1, + "pc_kind": 0, + "pc_sub": 0, + "point": [ + 490.1068097883441, + 615.2933805240965, + 101.93048886117796 + ], + "status": 0, + "t_param": 0.0, + "uv": [ + 0.6042330999477774, + 0.4883560002414041 + ], + "vtype": 3 + } + ] + }, + "query": { + "e": 0, + "face": { + "b_spline": { + "net": [ + [ + [ + 77.35030171522425, + 84.42401611721645, + -103.92968144629785, + 1.1679788277708982 + ], + [ + 47.90463458042485, + 405.2990361116051, + 280.7181619550648, + 1.965148261665444 + ], + [ + -41.472725533966525, + 1061.756780913049, + -291.6309720523001, + 1.107351262330258 + ] + ], + [ + [ + 326.9645674628707, + -94.13681590155923, + -225.4143683383236, + 1.1352410618437598 + ], + [ + 299.7106694867819, + 523.9943289864259, + -73.53621998747897, + 1.120150621879067 + ], + [ + -1e+300, + 970.6773124751174, + 135.03622223732592, + 1.6423353700479986 + ] + ], + [ + [ + 586.4353966964158, + 53.74004653775696, + -106.22668038276744, + 1.174570100044345 + ], + [ + 655.1817335621672, + 437.1906810292637, + 148.2036063713426, + 1.8867593260885225 + ], + [ + 649.6139757681688, + 905.4075306608922, + 148.37429170021454, + 0.7133957089650658 + ] + ], + [ + [ + 947.0603904666261, + -81.72870780971411, + -17.002892303963733, + 1.2385311223284332 + ], + [ + 1042.0725640484607, + 592.5802551659867, + 273.0767859850937, + 1.2823532357111875 + ], + [ + 1038.48426434397, + 933.9467485307696, + 198.52322210662703, + 0.7635345606975592 + ] + ] + ], + "p": 3, + "q": 1, + "u": [ + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.24206369586947935, + 0.6603299026143645, + 0.6603299026143645 + ] + } + }, + "face_tol": 0.001, + "q": "rail_rail" + }, + "solvers": [] + } + } + ] +} diff --git a/crates/difftest/tests/fuzz_regressions/heal.gap.json b/crates/difftest/tests/fuzz_regressions/heal.gap.json new file mode 100644 index 0000000000..64cb204d4b --- /dev/null +++ b/crates/difftest/tests/fuzz_regressions/heal.gap.json @@ -0,0 +1,227 @@ +{ + "cases": [ + { + "name": "fuzz14876_topo_surface_point_minus_1e300", + "fuzz": { + "base": "gen0019", + "mutations": [ + "/brep/surface/bspline/net/1/3/1 = -1e+300", + "/brep/surface/bspline/oob = true", + "/sel0 = 4294967296" + ], + "seed": 2709678135910684674 + }, + "inputs": { + "brep": { + "dom": [ + 0.28495004567498394, + 0.11560488617710181, + 0.9384143332653839, + 0.7854094106770744 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + -0.06426384609264546, + -1.7976931348623157e+308 + ], + [ + 0.0, + 1.0, + -0.3915280128409604, + -1.7976931348623157e+308 + ], + [ + 0.0, + 2.0, + 0.4254055242644449, + -1.7976931348623157e+308 + ], + [ + 0.0, + 3.0, + -0.27083086519440436, + -1.7976931348623157e+308 + ], + [ + 0.0, + 4.0, + 0.028420463260036888, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1.0, + 0.0, + -0.13993456782203617, + -1.7976931348623157e+308 + ], + [ + 1.0, + 1.0, + 0.26361749905553167, + -1.7976931348623157e+308 + ], + [ + 1.0, + 2.0, + 0.37457791181880906, + -1.7976931348623157e+308 + ], + [ + 1.0, + -1e+300, + -0.02046347842790186, + -1.7976931348623157e+308 + ], + [ + 1.0, + 4.0, + -0.1988374109438713, + -1.7976931348623157e+308 + ] + ], + [ + [ + 2.0, + 0.0, + 0.3929548533900564, + -1.7976931348623157e+308 + ], + [ + 2.0, + 1.0, + 0.4963008985878731, + -1.7976931348623157e+308 + ], + [ + 2.0, + 2.0, + 0.037510254458083536, + -1.7976931348623157e+308 + ], + [ + 2.0, + 3.0, + -0.017292679427664326, + -1.7976931348623157e+308 + ], + [ + 2.0, + 4.0, + -0.3416200502698228, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3.0, + 0.0, + 0.07916303576271266, + -1.7976931348623157e+308 + ], + [ + 3.0, + 1.0, + -0.24034449756379883, + -1.7976931348623157e+308 + ], + [ + 3.0, + 2.0, + 0.0519142354791885, + -1.7976931348623157e+308 + ], + [ + 3.0, + 3.0, + 0.2672903585943539, + -1.7976931348623157e+308 + ], + [ + 3.0, + 4.0, + 0.420170470111746, + -1.7976931348623157e+308 + ] + ], + [ + [ + 4.0, + 0.0, + -0.013815636000644282, + -1.7976931348623157e+308 + ], + [ + 4.0, + 1.0, + 0.2587164343271825, + -1.7976931348623157e+308 + ], + [ + 4.0, + 2.0, + 0.2930024607359608, + -1.7976931348623157e+308 + ], + [ + 4.0, + 3.0, + 0.07000214034379959, + -1.7976931348623157e+308 + ], + [ + 4.0, + 4.0, + -0.36698829790322374, + -1.7976931348623157e+308 + ] + ] + ], + "oob": true, + "p": 2, + "q": 2, + "u": [ + 0.0, + 0.0, + 0.0, + 0.3042064736624629, + 0.811051139602959, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.0, + 0.4342651854017705, + 0.8924264536259057, + 1.0, + 1.0, + 1.0 + ] + } + } + }, + "f0": 0.13684086049498556, + "f1": 0.5935117015657563, + "kind": 1, + "op": "topo", + "sel0": 0, + "sel1": 387677, + "uv": [ + 0.6528387219843295, + 0.3023516709158216 + ] + } + } + ] +} diff --git a/crates/difftest/tests/fuzz_regressions/heal.heal_data_p2.json b/crates/difftest/tests/fuzz_regressions/heal.heal_data_p2.json new file mode 100644 index 0000000000..19ad429e7b --- /dev/null +++ b/crates/difftest/tests/fuzz_regressions/heal.heal_data_p2.json @@ -0,0 +1,297 @@ +{ + "cases": [ + { + "name": "fuzz22334_hang29_surface_point", + "fuzz": { + "base": "gen0286", + "mutations": [ + "/brep/surface/bspline/net/1/0/2 = -1e+300" + ], + "seed": 2555077191992979210 + }, + "inputs": { + "brep": { + "dom": [ + 0.033167552042815886, + 0.17516066641250203, + 0.7795811162439683, + 0.96343913086097 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + -0.15931923940518578, + -1.7976931348623157e+308 + ], + [ + 0.0, + 1.0, + 0.26147778464658766, + -1.7976931348623157e+308 + ], + [ + 0.0, + 2.0, + 0.32192384347962655, + -1.7976931348623157e+308 + ], + [ + 0.0, + 3.0, + 0.18802350571588378, + -1.7976931348623157e+308 + ], + [ + 0.0, + 4.0, + 0.048326151474675894, + -1.7976931348623157e+308 + ] + ], + [ + [ + 1.0, + 0.0, + -1e+300, + -1.7976931348623157e+308 + ], + [ + 1.0, + 1.0, + 0.18092309474868684, + -1.7976931348623157e+308 + ], + [ + 1.0, + 2.0, + -0.4116160783702201, + -1.7976931348623157e+308 + ], + [ + 1.0, + 3.0, + -0.2692912997882043, + -1.7976931348623157e+308 + ], + [ + 1.0, + 4.0, + 0.242113381169355, + -1.7976931348623157e+308 + ] + ], + [ + [ + 2.0, + 0.0, + -0.183096153697491, + -1.7976931348623157e+308 + ], + [ + 2.0, + 1.0, + -0.3330194686005823, + -1.7976931348623157e+308 + ], + [ + 2.0, + 2.0, + 0.08191272807358196, + -1.7976931348623157e+308 + ], + [ + 2.0, + 3.0, + 0.17001222620784295, + -1.7976931348623157e+308 + ], + [ + 2.0, + 4.0, + 0.049719443533998375, + -1.7976931348623157e+308 + ] + ], + [ + [ + 3.0, + 0.0, + -0.027504715390783807, + -1.7976931348623157e+308 + ], + [ + 3.0, + 1.0, + -0.08896491065566237, + -1.7976931348623157e+308 + ], + [ + 3.0, + 2.0, + -0.1770849299377555, + -1.7976931348623157e+308 + ], + [ + 3.0, + 3.0, + -0.17706478447930285, + -1.7976931348623157e+308 + ], + [ + 3.0, + 4.0, + 0.2656927729124655, + -1.7976931348623157e+308 + ] + ], + [ + [ + 4.0, + 0.0, + 0.2537630367649967, + -1.7976931348623157e+308 + ], + [ + 4.0, + 1.0, + -0.49367444310821185, + -1.7976931348623157e+308 + ], + [ + 4.0, + 2.0, + -0.24484753459175101, + -1.7976931348623157e+308 + ], + [ + 4.0, + 3.0, + 0.06783029447777855, + -1.7976931348623157e+308 + ], + [ + 4.0, + 4.0, + 0.3474868741863969, + -1.7976931348623157e+308 + ] + ], + [ + [ + 5.0, + 0.0, + -0.4849719226573814, + -1.7976931348623157e+308 + ], + [ + 5.0, + 1.0, + 0.18374982052586486, + -1.7976931348623157e+308 + ], + [ + 5.0, + 2.0, + 0.4230460556036377, + -1.7976931348623157e+308 + ], + [ + 5.0, + 3.0, + -0.16258459587538177, + -1.7976931348623157e+308 + ], + [ + 5.0, + 4.0, + -0.4229328364057856, + -1.7976931348623157e+308 + ] + ] + ], + "oob": true, + "p": 3, + "q": 2, + "u": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.41581973960436147, + 0.45351214663280326, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 0.0, + 0.5285257131120971, + 0.944090950678377, + 1.0, + 1.0, + 1.0 + ] + } + } + }, + "case": "heal", + "init_gaps": true, + "steps": [ + { + "step": "cache_edge_props" + }, + { + "step": "cache_vertex_props" + }, + { + "step": "cache_face_props_gaps" + }, + { + "step": "cache_coin_vertices" + }, + { + "step": "cache_degen_faces" + }, + { + "step": "cache_coin_edges" + }, + { + "step": "fix_coin_edges" + }, + { + "step": "cache_missed_edge_xsects" + }, + { + "step": "fix_missed_edge_xsects" + }, + { + "step": "fix_uncontained_edges" + }, + { + "step": "fix_bad_gaps" + } + ], + "targets": { + "e": "reverse", + "f": "all", + "v": "all" + }, + "validate_only": true, + "voxels": [ + 12, + 12, + 12 + ] + } + } + ] +} diff --git a/crates/solidcore-ffi/src/units/blend_fillet_geom_p2.rs b/crates/solidcore-ffi/src/units/blend_fillet_geom_p2.rs index 9cc16033cc..046ae488b0 100644 --- a/crates/solidcore-ffi/src/units/blend_fillet_geom_p2.rs +++ b/crates/solidcore-ffi/src/units/blend_fillet_geom_p2.rs @@ -46,7 +46,9 @@ use crate::{call_with_buffer, FfiError}; -use super::blend_fillet_geom_p1::{surface, validate_block, Reader, MAX_LIST, MAX_SOLVER_MAGNITUDE}; +use super::blend_fillet_geom_p1::{ + surface, validate_block, Reader, MAX_LIST, MAX_MAGNITUDE, MAX_SOLVER_MAGNITUDE, +}; /// The upstream routine run by the shim. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -148,7 +150,12 @@ pub fn validate(op: Op, input: &[f64]) -> Result<(), FfiError> { Op::RailRail => { rd.opt_index(b.ne())?.ok_or_else(|| bad("edge -1"))?; if let Some(kind) = rd.opt_index(3)? { + // the original face's boundary reaches HANG-29 (UV trim curves dropped + // through GlobalCurveIntersect), bounded as for `blend.fillet_geom_p1`'s + // ClassifyFacePoint + rd.bound = MAX_MAGNITUDE; any_surface(&mut rd, kind)?; + rd.bound = MAX_SOLVER_MAGNITUDE; rd.finite()?; } } diff --git a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs index c9f7e02eae..c16b3fb3e7 100644 --- a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs +++ b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs @@ -92,7 +92,8 @@ pub const MAX_DEGREE: usize = 5; pub const MAX_COORD: f64 = 1.0e300; /// Bound (exclusive) on surface control point coordinates: from about `1e154`, where /// their squares overflow, upstream's face/face, edge/face and coincidence -/// intersections do not finish (HANG-46; below `1e150` they return at once). +/// intersections do not finish (HANG-29, `SmCurve::GlobalCoincidenceChecker` splitting +/// a face boundary forever; below `1e150` they return at once). pub const MAX_SURFACE_COORD: f64 = 1.0e154; /// Most ops in a script. pub const MAX_OPS: usize = 64; diff --git a/crates/solidcore-ffi/src/units/heal_gap.rs b/crates/solidcore-ffi/src/units/heal_gap.rs index 750895870c..d311c48cfa 100644 --- a/crates/solidcore-ffi/src/units/heal_gap.rs +++ b/crates/solidcore-ffi/src/units/heal_gap.rs @@ -411,6 +411,30 @@ pub fn eval(op: Op, input: &[f64]) -> Result, FfiError> { raw(op, "", input) } +/// Bound (exclusive) on the values of a topology op's B-rep block: from about `1e154`, +/// where squares overflow, the native topology op does not always return (HANG-29, +/// `SmCurve::GlobalCoincidenceChecker`: a B-spline control point of `-1e155` or +/// `-1e300` gave no result in 60 s, while `-1e154`, `-1e160` and `-1e200` returned at +/// once). +pub const MAX_TOPO_BLOCK_VALUE: f64 = 1.0e154; + +/// The check [`topo`] runs on its B-rep block (the difftest runner's Rust side runs it +/// too): every value below [`MAX_TOPO_BLOCK_VALUE`] in magnitude, except the `+-f64::MAX` +/// markers of the block format. +/// +/// # Errors +/// +/// [`FfiError::InvalidArgument`] for a value in the overflow range. +pub fn check_topo_block(block: &[f64]) -> Result<(), FfiError> { + if block + .iter() + .any(|v| v.abs() < f64::MAX && v.abs() >= MAX_TOPO_BLOCK_VALUE) + { + return Err(bad("B-rep value in the overflow range")); + } + Ok(()) +} + /// The export of `block` (a B-rep block as [`super::topology_vertexuse`] reads it): /// the graph the topology op runs on. /// @@ -430,6 +454,7 @@ pub fn export(path: &str, block: &[f64]) -> Result, FfiError> { /// [`FfiError::InvalidArgument`] for a malformed input; [`FfiError::Status`] for a shim /// failure. pub fn topo(path: &str, block: &[f64], args: &[f64]) -> Result, FfiError> { + check_topo_block(block)?; let ex = export(path, block)?; let mut rd = Reader::new(args); let kind = rd.index(5)?; diff --git a/crates/solidcore-ffi/src/units/heal_heal_data_p2.rs b/crates/solidcore-ffi/src/units/heal_heal_data_p2.rs index d038a4d54c..1c759348c5 100644 --- a/crates/solidcore-ffi/src/units/heal_heal_data_p2.rs +++ b/crates/solidcore-ffi/src/units/heal_heal_data_p2.rs @@ -139,6 +139,12 @@ fn raw(op: Op, path: &str, input: &[f64]) -> Result, FfiError> { }) } +/// Bound (exclusive) on the values of a B-rep block of kinds `0..=3`: from about +/// `1e154`, where squares overflow, the healer's edge and face caching does not return +/// (HANG-29, `SmCurve::GlobalCoincidenceChecker`; `--fuzz 30000` cases with one surface +/// control point of `+-1e300`). +pub const MAX_BLOCK_VALUE: f64 = 1.0e154; + /// Checks a B-rep block (all of `block`). pub(crate) fn check_block(path: &str, block: &[f64]) -> Result<(), FfiError> { let kind = block.first().copied().ok_or_else(|| bad("empty block"))?; @@ -167,6 +173,13 @@ pub(crate) fn check_block(path: &str, block: &[f64]) -> Result<(), FfiError> { rd.flag()?; return rd.done(); } + // (the block format's NOW / NOZ markers are +-f64::MAX) + if block + .iter() + .any(|v| v.abs() < f64::MAX && v.abs() >= MAX_BLOCK_VALUE) + { + return Err(bad("B-rep value in the overflow range")); + } // kinds 0..=3: the topology.vertexuse validator (it builds the export once) vu::eval(vu::Op::Export, path, block).map(|_| ()) } diff --git a/plan/findings.md b/plan/findings.md index b38afe4f80..8d2060c68d 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -11279,7 +11279,7 @@ guarded in `offset.offset_geometry_creation`; the fix belongs to the curve cache ### boolean.topology_intersector_p1/_p2: `--fuzz 30000` (2026-10-21) -- Hangs on surface control points of `1e154` or more: HANG-46 in `plan/upstream-issues.md`; the FFI reader now refuses them. +- Hangs on surface control points of `1e154` or more: new triggers of HANG-29 in `plan/upstream-issues.md` (also in `heal.gap`, `heal.heal_data_p2` and `blend.fillet_geom_p2`); the FFIs now refuse them. - Slow, not hung: `fuzz09748` (p1) and `fuzz15421` (p2) set `/approx = 1e300`. Replayed with `approx` from `1e3` to `1e300`, the C++ returns in 22-25 s every time (`1`: 0.5 s), just over the fuzz harness's fixed 20 s budget (`CASE_TIMEOUT`). These are valid upstream inputs, so the FFI accepts them; they stay `timeout` verdicts in the fuzz until the harness has a per-unit budget. - 5 `differs` in p2 (`fuzz06628`, `fuzz08367`, `fuzz19033`, `fuzz19955`, `fuzz23628`): `values.state.len` is 3 shorter on the Rust side in each. Not yet assessed. diff --git a/plan/upstream-issues.md b/plan/upstream-issues.md index 1e821a677d..563d6b88a3 100644 --- a/plan/upstream-issues.md +++ b/plan/upstream-issues.md @@ -1767,10 +1767,15 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Location:** `SmCurve.cpp:19950` `SmCurve::GlobalCoincidenceChecker` (closed / half-closed split into thirds; the only recursion guard is the parameter length against `SM_ZONE_TOL_3D/10`), reached from `sm_ClassifyFacePoint` through `SmFace::CalculateUVDomainFromUVTrimCurves` -> `SmEdgeuse::CreateUVTrimCurve` -> `SmSurface::DropAndTrimCurve` -> `SmSurfaceDropCurve::FindBoundaryStartPoints` -> `SmCurve::GlobalCurveIntersect`. - **What happens:** With huge coordinates, squared distances lose precision or overflow, so the closedness / polar-box tests keep asking for a split and every third is split again; the parameter-length guard is reached late or never. Non-termination is established only where squared distances overflow to infinity: on a 3 x 4 surface with one control point at `1e153` the call returns in 90 ms and at `1e154` never (an hour on one core). Below that the call terminates but is slow in a shape-dependent way from about `1e15` on, and uniform scaling triggers it as much as a single outlier does: on a 4 x 3 cubic-by-quadratic fixture (the pre-review's shape), one control point of `-1e15` in all three coordinates takes 54 s, `-1e20` 14 s, `-1e40` 10 s, `-1e50` 12 s, while the same magnitude in one coordinate only takes 0.14-0.20 s and `-1e10` takes 5 ms; both surfaces and the point scaled uniformly by `1e30`, `1e45` or `1e49` take 43-55 s, scaled by `1e10` 3 ms; a homogeneous `9.9e39` over a weight of `1e-10` (Euclidean `9.9e49`) 37 s; one control point at `-1e80` 10 s, `-1e100` 23 s, `-1e120`..`-1e150` 70-99 s; a homogeneous `1e150` under a weight of `1e-150` 34 s. A random sweep over the two ops reaching it found 14 calls in 40,000 above 2 s (worst 38 s), every one with a value of at least `3e15`, and none above 120 s. - **Trigger:** `sm_ClassifyFacePoint` on a face whose surface has coordinates of magnitude `1e154` or more (never returns on the shape above); slow from about `1e15`. +- **More triggers (`--fuzz 30000` on main `16f04231`, 2026-10-21; each a surface control point of `+-1e300`; gdb backtraces in `GlobalCoincidenceChecker` after 40 s):** + - `boolean.topology_intersector_p1` (`fuzz05142` `face_face_coin`, `fuzz17005` `check_ssi`/`edge_face`, `fuzz27420` `face_face`) and `_p2` (`fuzz06354` `intersect_topo`). Replaying `fuzz05142`: `1e154` and above give no result in 60 s, `1e150` and below return in 0.1 s. + - `heal.gap` topology op (`fuzz14876`): `-1e300` and `-1e155` hang, `-1e200`, `-1e160`, `-1e154` and below return at once (erratic above the overflow point). + - `heal.heal_data_p2` heal scripts through the edge/face caching (`fuzz03677`, `fuzz22334`, `fuzz26581`). + - `blend.fillet_geom_p2` `RailRail` with an original face (`fuzz09135` and 6 more), through `SmSurfaceDropCurve::FindBoundaryStartPoints`: `-1e155` and above hang, `-1e154` and below return. - **How found:** `fuzz_guards` sweep of the `blend.fillet_geom_p1` FFI guards (PR #147), located with gdb; the weight bypass by the PR's pre-review; the shape dependence and the `1e15` onset by its re-review (timing probes and a 120 s-per-call fuzz). - **Confirmed:** yes (gdb backtrace 25 frames deep in `GlobalCoincidenceChecker`; never returned; timings measured in the reference build). - **Severity:** hang (above `1e154`); seconds to about a minute per call from `1e15`. -- **Rust port:** The FFI wrapper rejects, for the ops that reach `sm_ClassifyFacePoint` (`ClassifyFacePoint`, `FbSplitFace`), any value and any Euclidean control point (`Pw / w`) above `MAX_MAGNITUDE = 1e150`, the overflow regime (see `crates/solidcore-ffi/src/units/blend_fillet_geom_p1.rs`); slow inputs under it are valid upstream inputs and are accepted (the guard fuzz allows those two ops 180 s per call). The other ops take any finite value. The Rust `classify_face_point` still defers on `topology.face_p1`. +- **Rust port:** The FFI wrapper rejects, for the ops that reach `sm_ClassifyFacePoint` (`ClassifyFacePoint`, `FbSplitFace`), any value and any Euclidean control point (`Pw / w`) above `MAX_MAGNITUDE = 1e150`, the overflow regime (see `crates/solidcore-ffi/src/units/blend_fillet_geom_p1.rs`); slow inputs under it are valid upstream inputs and are accepted (the guard fuzz allows those two ops 180 s per call). The other ops take any finite value. The Rust `classify_face_point` still defers on `topology.face_p1`. Since 2026-10-21 also: `boolean.topology_intersector_p1`/`_p2` refuse surface control point coordinates of `MAX_SURFACE_COORD = 1e154` or more; `heal.gap`'s topology op (`MAX_TOPO_BLOCK_VALUE`) and every `heal.heal_data_p2`/`_p3` B-rep block (`MAX_BLOCK_VALUE`) refuse a block value of `1e154` or more; `blend.fillet_geom_p2`'s `RailRail` bounds its original face's surface by `MAX_MAGNITUDE`. Regression inputs: `crates/difftest/tests/fuzz_regressions/`. #### HANG-20. The global solvers never finish when a surface coordinate's own box arithmetic overflows - **Location:** `SmGlobalSolver::SolveBranch` with each of its local solvers: `SmCSIGlobalSolver::LocalSolve` -> `SmCurve::SimpleCoincidenceChecker` -> `SmCurve::LocalPointSolve` -> `SmLocalSolve1d::SolveIt` (gdb after 25 s on the cliff program: eleven `SolveBranch` frames, the time in `LocalSolve`), reached from `SmSurface::GlobalCurveIntersect` (`SmFilletVertex::CalcCliffRailIntEdge`), from `SmSurface::GlobalPointSolve` (`sm_SurfaceDropPoint`) and from `SmSurface::PointBasedSurfaceIntersect` (`sm_TwoPntsIntersection`), all in `SmFilletGeom.cpp`. @@ -1936,14 +1941,6 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Severity:** hang. - **Rust port:** Stops after `MAX_TOL_DOUBLINGS` (2200) doublings with `SolidError::InvalidInput`. -#### HANG-46. Topology intersections do not return for a surface control point of magnitude 1e154 or more -- **Location:** `source/SMLib/src/SmTopologyIntersector.cpp`, the face/face, face/face coincidence, edge/face and full (`IntersectInsertRelate`) intersections of `SmTopologyIntersector`; the stuck loop was not isolated (the same overflow class as HANG-16, HANG-19 and HANG-37). -- **What happens:** With one surface control point coordinate at `1e154` or beyond, the intersection gives no result within 60 s; at `1e150` and below the same script returns in 0.1 s. The squared distances of such coordinates overflow. -- **Trigger:** `--fuzz 30000` of `boolean.topology_intersector_p1`: `fuzz05142` (`face_face_coin` after `other_mate`, `/brep_b/surface/bspline/net/1/0/1 = 1e300`), `fuzz17005` (`check_ssi` then `edge_face`, `net/1/0/1 = -1e300`), `fuzz27420` (`face_face`, `net/1/0/0 = 1e300`); of `boolean.topology_intersector_p2`: `fuzz06354` (`relate`, `mgr_new`, `intersect_topo`, `/brep_a/surface/bspline/net/1/4/1 = -1e300`, other B-rep rebuilt from its file). -- **How found:** `--fuzz 30000` of both units on main `16f04231`; threshold by replaying `fuzz05142` with the coordinate replaced (60 s limit per run): `1e300`, `1e280`, `1e250`, `1e200`, `1e160`, `1e154` give no result; `1e150`, `1e100`, `1e50`, `1e10`, `1` return in 0.0-0.2 s. -- **Confirmed:** yes (no result within 60 s for six values; infinite execution not proven). -- **Severity:** hang on out-of-range geometry; not a parity blocker (no fixture or generated case reaches it). -- **Rust port:** the `boolean.topology_intersector_p1` FFI reader, shared with `_p2`, refuses surface control point coordinates of `MAX_SURFACE_COORD = 1e154` or more (the boundary of the measured hang, as `_p2`'s `HANG_COORD` for `IntersectInsertRelate`). ## Nondeterminism From ee8efd068aa2df797cfc0482dab41ef580e728c8 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 02:51:35 -0400 Subject: [PATCH 004/491] fix: topology_intersector bounds curve points too; shim refuses UB-166 surfaces A second `--fuzz 30000` of boolean.topology_intersector_p2 (its case mix changed with the new fixtures) found two more: - fuzz20492: a wire's curve control point of -1e300 keeps IntersectInsertRelate in GlobalCoincidenceChecker (HANG-29; -1e155 and above 27 s, -1e154 and below at once). The reader's bound, now MAX_GEOMETRY_COORD, applies to curve control points as well. - fuzz23120: a surface v knot of 1e-300 snaps onto the start knot, so sm_DecomposeSrf writes past its patch array when FaceIntersect builds the surface cache (UB-166, gdb). The shim reads back the constructed knot vectors and refuses such a B-rep (SC_ERR_ARG), as the blend.fillet_geom_p1 shim does. Both inputs join the regression file and its test; UB-166 and HANG-29 record the new triggers. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../units/boolean_topology_intersector_p1.rs | 17 +- .../boolean.topology_intersector_p2.json | 515 ++++++++++++++++++ .../units/boolean_topology_intersector_p1.rs | 16 +- .../shim/boolean_topology_intersector_p1.cpp | 44 +- plan/upstream-issues.md | 5 +- 5 files changed, 579 insertions(+), 18 deletions(-) diff --git a/crates/difftest/src/units/boolean_topology_intersector_p1.rs b/crates/difftest/src/units/boolean_topology_intersector_p1.rs index f2988c38c1..c65a791b70 100644 --- a/crates/difftest/src/units/boolean_topology_intersector_p1.rs +++ b/crates/difftest/src/units/boolean_topology_intersector_p1.rs @@ -4846,10 +4846,11 @@ mod tests { use super::*; use crate::case::CaseSource; - /// HANG-29: the `--fuzz 30000` inputs whose surface control point of `1e300` kept - /// the C++ intersection busy past the timeout, for this unit and `_p2` (which - /// shares this reader). They cannot be fixtures (a harness error always fails a - /// case), so this test keeps them: the FFI refuses each before any construction. + /// HANG-29: the `--fuzz 30000` inputs whose surface or curve control point of + /// `+-1e300` kept the C++ intersection busy past the timeout, for this unit and `_p2` + /// (which shares this reader), and UB-166's v knot snap (`ub166` in the name: the + /// shim refuses the B-rep). They cannot be fixtures (a harness error always fails a + /// case), so this test keeps them: each is refused before the intersection runs. #[cfg(feature = "cpp")] #[test] fn ffi_refuses_surface_points_in_the_hang_range() { @@ -4874,7 +4875,13 @@ mod tests { inputs: recorded["inputs"].clone(), }; let error = runner.run_cpp(&case).unwrap_err().to_string(); - assert!(error.contains("surface: point out of range"), "{}: {error}", case.name); + let expected = if case.name.contains("ub166") { + // the shim's UB-166 refusal (SC_ERR_ARG) + "shim_rejected_args" + } else { + "point out of range" + }; + assert!(error.contains(expected), "{}: {error}", case.name); } } } diff --git a/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json index d3eed579f1..7f8a9ee22f 100644 --- a/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json +++ b/crates/difftest/tests/fuzz_regressions/boolean.topology_intersector_p2.json @@ -156,6 +156,521 @@ } ] } + }, + { + "name": "fuzz20492_huge_curve_point", + "fuzz": { + "base": "gen0466", + "mutations": [ + "/brep_a/curve/u/2 = -1.0", + "/brep_a/curve/cpts/0/1 = -1e+300" + ], + "seed": 17443736899599566773 + }, + "inputs": { + "ang": 0.001, + "approx": 0.0001, + "brep_a": { + "curve": { + "cpts": [ + [ + -0.1526689657354592, + -1e+300, + 0.7359818720926314, + -1.7976931348623157e+308 + ], + [ + 1.0876450352265095, + -0.12108896908248257, + 0.4758534266897121, + -1.7976931348623157e+308 + ], + [ + 1.9781932488527034, + -0.7023960844083053, + -0.15710784909658582, + -1.7976931348623157e+308 + ], + [ + 2.976298605252458, + -0.1901217818253127, + 0.49424732984180286, + -1.7976931348623157e+308 + ] + ], + "dim": 3, + "p": 1, + "u": [ + -3.3129657536430828, + -3.3129657536430828, + -1.0, + 0.5396116360947252, + 4.204338501951987, + 4.204338501951987 + ] + }, + "kind": "wire", + "t0": -2.5018394856420985, + "t1": 3.4003539056870595 + }, + "brep_b": { + "curve": { + "cpts": [ + [ + -0.1526689657354592, + -0.7981917893863129, + 0.7359818720926314, + -1.7976931348623157e+308 + ], + [ + 1.0876450352265095, + -0.12108896908248257, + 0.4758534266897121, + -1.7976931348623157e+308 + ], + [ + 1.9781932488527034, + -0.7023960844083053, + -0.15710784909658582, + -1.7976931348623157e+308 + ], + [ + 2.976298605252458, + -0.1901217818253127, + 0.49424732984180286, + -1.7976931348623157e+308 + ] + ], + "dim": 3, + "p": 1, + "u": [ + -3.3129657536430828, + -3.3129657536430828, + -2.2222083588314954, + 0.5396116360947252, + 4.204338501951987, + 4.204338501951987 + ] + }, + "kind": "wire", + "t0": -2.5018394856420985, + "t1": 3.4003539056870595 + }, + "ctor": 1, + "ops": [ + { + "bv": [ + 0, + 0, + 40000 + ], + "op": "squeeze", + "ov": [ + 1, + 0, + 39745 + ] + }, + { + "capture": 1e-05, + "op": "mgr_new", + "override_tol": 0.0 + }, + { + "op": "intersect_topo" + }, + { + "a": [ + 0, + 0, + 29541 + ], + "b": [ + 1, + 0, + 49115 + ], + "op": "remove_rel" + } + ] + } + }, + { + "name": "fuzz23120_ub166_v_knot_snap", + "fuzz": { + "base": "gen0411", + "mutations": [ + "/brep_b/surface/bspline/v/2 = 1e-300" + ], + "seed": 17443736899599569401 + }, + "inputs": { + "ang": 0.3490658503988659, + "approx": 0.01, + "brep_a": { + "file": "sphere.smb", + "kind": "file", + "rebuild": false + }, + "brep_b": { + "dom": [ + 0.043997991847559814, + 0.047915250513455764, + 0.7817484618648011, + 0.9934555492579829 + ], + "kind": "face", + "surface": { + "bspline": { + "form": 0, + "net": [ + [ + [ + 0.0, + 0.0, + -0.0004061737374495282, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.001, + -0.0002625158101562726, + -1.7976931348623157e+308 + ], + [ + 0.0, + 0.002, + -0.00014868590649097968, + -1.7976931348623157e+308 + ] + ], + [ + [ + 0.001, + 0.0, + -0.00023965526183137087, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.001, + -0.0003163136496518546, + -1.7976931348623157e+308 + ], + [ + 0.001, + 0.002, + 0.00013292683888084145, + -1.7976931348623157e+308 + ] + ] + ], + "oob": false, + "p": 1, + "q": 1, + "u": [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + "v": [ + 0.0, + 0.0, + 1e-300, + 1.0, + 1.0 + ] + } + } + }, + "ctor": 0, + "ops": [ + { + "b": { + "c": [ + 0, + 3, + 64385 + ], + "ivls": [ + { + "a": 0.0, + "b": 0.4217614757775817, + "pcs": [ + { + "cls": 100, + "dev": 0.01, + "h": [ + -1, + 8, + -1 + ], + "t": 0.0 + }, + { + "cls": 100, + "dev": 0.01, + "h": [ + -1, + 8, + -1 + ], + "t": 0.21088073788879086 + }, + { + "cls": 100, + "dev": 0.0, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4217614757775817 + } + ] + }, + { + "a": 0.4217614757775817, + "b": 0.42227554152481955, + "pcs": [ + { + "cls": 100, + "dev": 0.01, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4217614757775817 + }, + { + "cls": 100, + "dev": 0.0001, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4220185086512006 + }, + { + "cls": 100, + "dev": 1e-06, + "h": [ + -1, + 8, + -1 + ], + "t": 0.42227554152481955 + } + ] + }, + { + "a": 0.42227554152481955, + "b": 1.0, + "pcs": [ + { + "cls": 100, + "dev": 0.0, + "h": [ + -1, + 8, + -1 + ], + "t": 0.42227554152481955 + }, + { + "cls": 100, + "dev": 1e-06, + "h": [ + -1, + 8, + -1 + ], + "t": 0.7111377707624098 + }, + { + "cls": 100, + "dev": 1e-06, + "h": [ + -1, + 8, + -1 + ], + "t": 1.0 + } + ] + } + ], + "relative": true, + "t0": 0.0, + "t1": 1.0, + "tol": 1e-05 + }, + "index": 1, + "o": { + "c": [ + 0, + 3, + 64385 + ], + "ivls": [ + { + "a": 0.0, + "b": 0.4217614757775817, + "pcs": [ + { + "cls": 100, + "dev": 1e-06, + "h": [ + -1, + 8, + -1 + ], + "t": 0.0 + }, + { + "cls": 100, + "dev": 0.01, + "h": [ + -1, + 8, + -1 + ], + "t": 0.23522569703289375 + }, + { + "cls": 100, + "dev": 0.0, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4704513940657875 + } + ] + }, + { + "a": 0.4217614757775817, + "b": 0.42227554152481955, + "pcs": [ + { + "cls": 103, + "dev": 0.0001, + "h": [ + 1, + 1, + 3426 + ], + "t": 0.4704513940657875 + }, + { + "cls": 103, + "dev": 0.0, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4708165891725634 + }, + { + "cls": 102, + "dev": 1e-06, + "h": [ + 0, + 2, + 11126 + ], + "t": null + } + ] + }, + { + "a": 0.42227554152481955, + "b": 1.0, + "pcs": [ + { + "cls": 100, + "dev": 0.0001, + "h": [ + -1, + 8, + -1 + ], + "t": 0.4711817842793393 + }, + { + "cls": 103, + "dev": 0.0001, + "h": [ + 1, + 1, + 39800 + ], + "t": 0.7355908921396697 + }, + { + "cls": 100, + "dev": 0.0, + "h": [ + -1, + 8, + -1 + ], + "t": 1.0 + } + ] + } + ], + "relative": true, + "t0": 0.0, + "t1": 1.0, + "tol": 1e-06 + }, + "op": "fixup_split" + }, + { + "op": "iir" + }, + { + "edges1": [ + [ + 0, + 1, + 22861 + ], + [ + 0, + 1, + 53360 + ], + [ + 0, + 1, + 17004 + ], + [ + 0, + 1, + 45291 + ] + ], + "edges2": [ + [ + 1, + 1, + 10181 + ] + ], + "ignore": false, + "manifold": true, + "op": "edge_edge" + } + ] + } } ] } diff --git a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs index c16b3fb3e7..535eadec32 100644 --- a/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs +++ b/crates/solidcore-ffi/src/units/boolean_topology_intersector_p1.rs @@ -90,11 +90,13 @@ pub const MAX_INDEX: usize = 32; pub const MAX_DEGREE: usize = 5; /// Largest coordinate magnitude. pub const MAX_COORD: f64 = 1.0e300; -/// Bound (exclusive) on surface control point coordinates: from about `1e154`, where -/// their squares overflow, upstream's face/face, edge/face and coincidence -/// intersections do not finish (HANG-29, `SmCurve::GlobalCoincidenceChecker` splitting -/// a face boundary forever; below `1e150` they return at once). -pub const MAX_SURFACE_COORD: f64 = 1.0e154; +/// Bound (exclusive) on B-spline surface and curve control point coordinates: from +/// about `1e154`, where their squares overflow, upstream's face/face, edge/face, +/// coincidence and full intersections do not finish (HANG-29, +/// `SmCurve::GlobalCoincidenceChecker` splitting a boundary forever; a surface point of +/// `1e154` gave no result in 60 s, `1e150` returned in 0.1 s; a curve point of `-1e155` +/// took 27 s, `-1e154` none). +pub const MAX_GEOMETRY_COORD: f64 = 1.0e154; /// Most ops in a script. pub const MAX_OPS: usize = 64; /// Most handles in one list. @@ -429,7 +431,7 @@ fn surface(rd: &mut Reader<'_>) -> Result { "surface: need 1 <= p <= n, 1 <= q <= m, r = n+p+1, s = m+q+1", )); } - check_points(rd, (n + 1) * (m + 1), false, MAX_SURFACE_COORD, "surface")?; + check_points(rd, (n + 1) * (m + 1), false, MAX_GEOMETRY_COORD, "surface")?; let u = rd.finites(r + 1)?; let v = rd.finites(s + 1)?; let (u0, u1) = check_knots(&u, p, "surface u")?; @@ -449,7 +451,7 @@ fn curve(rd: &mut Reader<'_>) -> Result<(f64, f64), FfiError> { if p == 0 || p > n || m != n + p + 1 { return Err(bad("curve: need 1 <= p <= n and m = n + p + 1")); } - check_points(rd, n + 1, dim == 2, f64::INFINITY, "curve")?; + check_points(rd, n + 1, dim == 2, MAX_GEOMETRY_COORD, "curve")?; let knots = rd.finites(m + 1)?; check_knots(&knots, p, "curve") } diff --git a/harness/cpp/shim/boolean_topology_intersector_p1.cpp b/harness/cpp/shim/boolean_topology_intersector_p1.cpp index 1c7b8f18bc..28ce9196b8 100644 --- a/harness/cpp/shim/boolean_topology_intersector_p1.cpp +++ b/harness/cpp/shim/boolean_topology_intersector_p1.cpp @@ -1727,7 +1727,39 @@ void run_script(Rd& rd, Two& t, std::vector& o) } } -void run_ti(int32_t op, Rd& rd, const char* path, std::vector& o) +// UB-166: the B-spline surface constructor's sm_FixupKnotVector can raise a v knot +// vector's start multiplicity above q + 1 (a first interior knot snapped or spread onto +// the start knot, e.g. a v knot of 1e-300); sm_DecomposeSrf then writes past its patch +// array when the face/face intersection builds the surface cache. As in the +// blend.fillet_geom_p1 shim, read back the knot vectors the constructor left and refuse +// such a B-rep (the precondition: some runs never build that cache). +bool decomposition_overflows(const Two& t) +{ + for (int b = 0; b < 2; b++) + { + if (t.brep[b] == nullptr) + continue; + for (SmFace* f : t.g[b].faces) + { + const SmSurface* s = f ? f->GetSurface() : nullptr; + if (s == nullptr || !s->IsKindOf(SmBSplineSurface_TYPE)) + continue; + const gw_SURFACE* g = ((SmBSplineSurface*)s)->GetGwNurbPointer(); + if (g == NULL) + continue; + const gw_INDEX last = g->knv->m; + gw_INDEX mult = 1; + while (mult <= last && g->knv->U[mult] == g->knv->U[0]) + ++mult; + if (mult > (gw_INDEX)g->q + 1) + return true; + } + } + return false; +} + +// Runs one call; false when the B-reps are refused (UB-166). +bool run_ti(int32_t op, Rd& rd, const char* path, std::vector& o) { Two t; struct Clean @@ -1742,7 +1774,9 @@ void run_ti(int32_t op, Rd& rd, const char* path, std::vector& o) } } clean{t}; if (!build_two(rd, path, t, o)) - return; + return true; + if (decomposition_overflows(t)) + return false; o.push_back(0.0); if (op == TI_EXPORT) { @@ -1759,9 +1793,10 @@ void run_ti(int32_t op, Rd& rd, const char* path, std::vector& o) o.push_back((double)ex.size()); o.insert(o.end(), ex.begin(), ex.end()); } - return; + return true; } run_script(rd, t, o); + return true; } } // namespace @@ -1773,7 +1808,8 @@ SC_EXPORT int32_t SC_TI_EVAL_NAME(int32_t op, const char* path, const double* in SC_TRY Rd rd{in, 0}; std::vector o; - run_ti(op, rd, path, o); + if (!run_ti(op, rd, path, o)) + return SC_ERR_ARG; return sc_copy_out(o.data(), (int64_t)o.size(), out, cap, out_len); SC_CATCH } diff --git a/plan/upstream-issues.md b/plan/upstream-issues.md index 563d6b88a3..1b6eecc074 100644 --- a/plan/upstream-issues.md +++ b/plan/upstream-issues.md @@ -1575,6 +1575,7 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Location:** `source/SMLib/src/SmNurbsSrf.cpp:1524` `sm_DecomposeSrf` (v-direction loop, `:1690`-`:1740`: `Qw = surA[iq][jq]->net->Pw` with no bound on `jq`; the u direction has the `if (iq >= nsu) NL_QUIT` guard, "B262"), called from `sm_CreateBezierPatchBlock` (`SmSurfaceCache.cpp:4848`) <- `SmSurfaceCache::BuildTreeBase` (`:4996`) <- `SmTrimSrfCache::BuildTree` <- `smsurf_GetSurfaceCache`, reached from `sm_SurfaceDropPoint` (`SmSurface::GlobalPointSolve`) and from `sm_ClassifyFacePoint` through `SmFace::CalculateUVDomainFromUVTrimCurves` -> `SmEdgeuse::CreateUVTrimCurve` -> `SmSurface::DropCurve`; also from `sm_SrfSrfIntersection` (`SmSurfaceIntersector::DoIntersection`, `SmSurfaceIntersector.cpp:6146`) and `SmSurface::GlobalCurveIntersect` (`SmSurface.cpp:17196`). The fixup: `sm_FixupKnotVector` (`SmNurbsCrv.cpp:386`), run by every `SmBSplineSurface` constructor (`SmBSplineSurface.cpp:135/136, 203/204, 295/296, 344/345`) with `SM_EFF_ZERO_PARAM` (`1.2e-8`). - **What happens:** `sm_FixupKnotVector` first makes every knot less than `SmTol::GetScaledZero(u0, u1)` (`SM_EFF_ZERO * (1 + max(|u0|, |u1|))`, about `2e-12` on `[0, 1]`) above its already-snapped predecessor equal to it. A first interior knot that close to the start knot therefore becomes the start knot, and the start multiplicity rises to `q + 2`. Its second stage then walks the distinct knots and spreads a pair closer than `SM_EFF_ZERO_PARAM` to `1.01 * SM_EFF_ZERO_PARAM = 1.212e-8` apart: at the first pair the second knot moves up, at the last pair the first knot moves down (`vKts[i] = vKts[i+1] - dIncrement`), elsewhere both move away from their midpoint. Nothing keeps a moved knot off the start knot, so on a knot range under about `2.4e-8` the last-pair move lands a knot exactly on the start and raises its multiplicity the same way (a move below the start leaves a non-monotone vector, which runs or fails with status 1006, no crash). `N_BasisGetSpanCount` (exact comparisons from index `q`) does not count the zero-length first span, but `sm_DecomposeSrf` starts its v loop at `iev = q + 1` without comparing `VP[q]` with `VP[q + 1]`, so it extracts a patch for that zero-length span and then one more patch than the `nsu * nsv` the caller allocated: `surA[iq][nsv]` is read past the `SmTArray` and dereferenced (SIGSEGV). In u the same over-count reaches the B262 guard and the decomposition fails (`NL_QUIT`, an error, no crash). Curves are safe: `sm_CreateBezSegments` (`SmNurbsCrv.cpp:478`) skips a start over-multiplicity ("bad data") and bounds `iq` by the span count. - **Trigger:** any surface whose v knot vector, after both fixup stages, has start multiplicity above `degree + 1`: by the snap, `V = [0, 0, 0, 1e-12, 1, 1, 1]` (degree 2) or `[0, 0, 0, 6.2e-31, 6.8e-31, 1, 1, 1]`; by the spread, `[0, 0, 0, 5e-9, 5e-9, 1.212e-8, 1.212e-8, 1.212e-8]` (the snap leaves it, the first pair moves `5e-9` up to `1.212e-8`, the last pair moves it down to exactly `0`); by the end snap first, `[0, 0, 0, 5e-9, 5e-9, E, E + d x3]` with `E = 1.212e-8` and `d` under the scaled zero (the end group snaps to `E` before the spread, which then moves `5e-9` to `E - E = 0`). `(1e-11, 2e-11)` interior knots run. The spread can also leave a non-monotone vector, which upstream handles without undefined behaviour (valgrind clean): `[0, 0, 0, 5e-9, 8e-9, 1.212e-8 x3]` (spread to `[.., 4e-9, 0, ..]`) fails with 1006, `[0, 0, 0, 5e-9, 5e-9, 1e-8 x3]` (a knot moved below the start) returns 1001 (no solution), `[0, 0, 0, 5e-9, 5e-9, E, E + 2e-12 x3]` solves with status 0. The same tiny gaps away from the start (`0.5`, `0.5 + 1e-13`) only raise an interior multiplicity, which the decomposition tolerates. The same vectors in u fail with 1006 (B262 guard). Only the callers that build the surface cache reach it. +- **More triggers:** `--fuzz 30000` of `boolean.topology_intersector_p2` (2026-10-21), `fuzz23120`: a B-rep face whose B-spline surface has the v knot vector's first interior knot set to `1e-300` (`fixup_split`, `iir`, `edge_edge`); segfault in `sm_DecomposeSrf` <- `SmSurfaceCache::BuildTreeBase` <- `SmFace::FaceIntersect` <- `SmTopologyIntersector::IntersectTopology` (gdb). The `boolean.topology_intersector_p1` shim (shared by `_p2`) now refuses a B-rep with such a surface, as the `blend.fillet_geom_p1` shim does. - **How found:** the reviewer's timing fuzz of the `blend.fillet_geom_p1` FFI guards (PR #147 re-review; three reproducers, `SurfaceDropPoint` and `ClassifyFacePoint`), located with gdb; mechanism by source reading (`sm_FixupKnotVector`, `N_BasisGetSpanCount`, `sm_DecomposeSrf`). The spread-stage trigger: the round-3 review; the end-snap route: the round-4 review (three reproducers, gdb, same frames), which a Rust replica of the fixup had missed by one operation. - **Confirmed:** yes (gdb backtraces in the reference build for the four reproducers; the FFI's rejection of exactly the constructed condition makes them rejected cases and the fuzz sweeps no longer crash). - **Severity:** crash (out-of-bounds read and write through a stale pointer). @@ -1768,14 +1769,14 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **What happens:** With huge coordinates, squared distances lose precision or overflow, so the closedness / polar-box tests keep asking for a split and every third is split again; the parameter-length guard is reached late or never. Non-termination is established only where squared distances overflow to infinity: on a 3 x 4 surface with one control point at `1e153` the call returns in 90 ms and at `1e154` never (an hour on one core). Below that the call terminates but is slow in a shape-dependent way from about `1e15` on, and uniform scaling triggers it as much as a single outlier does: on a 4 x 3 cubic-by-quadratic fixture (the pre-review's shape), one control point of `-1e15` in all three coordinates takes 54 s, `-1e20` 14 s, `-1e40` 10 s, `-1e50` 12 s, while the same magnitude in one coordinate only takes 0.14-0.20 s and `-1e10` takes 5 ms; both surfaces and the point scaled uniformly by `1e30`, `1e45` or `1e49` take 43-55 s, scaled by `1e10` 3 ms; a homogeneous `9.9e39` over a weight of `1e-10` (Euclidean `9.9e49`) 37 s; one control point at `-1e80` 10 s, `-1e100` 23 s, `-1e120`..`-1e150` 70-99 s; a homogeneous `1e150` under a weight of `1e-150` 34 s. A random sweep over the two ops reaching it found 14 calls in 40,000 above 2 s (worst 38 s), every one with a value of at least `3e15`, and none above 120 s. - **Trigger:** `sm_ClassifyFacePoint` on a face whose surface has coordinates of magnitude `1e154` or more (never returns on the shape above); slow from about `1e15`. - **More triggers (`--fuzz 30000` on main `16f04231`, 2026-10-21; each a surface control point of `+-1e300`; gdb backtraces in `GlobalCoincidenceChecker` after 40 s):** - - `boolean.topology_intersector_p1` (`fuzz05142` `face_face_coin`, `fuzz17005` `check_ssi`/`edge_face`, `fuzz27420` `face_face`) and `_p2` (`fuzz06354` `intersect_topo`). Replaying `fuzz05142`: `1e154` and above give no result in 60 s, `1e150` and below return in 0.1 s. + - `boolean.topology_intersector_p1` (`fuzz05142` `face_face_coin`, `fuzz17005` `check_ssi`/`edge_face`, `fuzz27420` `face_face`) and `_p2` (`fuzz06354` `intersect_topo`). Replaying `fuzz05142`: `1e154` and above give no result in 60 s, `1e150` and below return in 0.1 s. A curve control point does it too (`_p2` `fuzz20492`, `intersect_topo` on a wire with a point of `-1e300`): `-1e155` and above take 27 s, `-1e154` and below return at once. - `heal.gap` topology op (`fuzz14876`): `-1e300` and `-1e155` hang, `-1e200`, `-1e160`, `-1e154` and below return at once (erratic above the overflow point). - `heal.heal_data_p2` heal scripts through the edge/face caching (`fuzz03677`, `fuzz22334`, `fuzz26581`). - `blend.fillet_geom_p2` `RailRail` with an original face (`fuzz09135` and 6 more), through `SmSurfaceDropCurve::FindBoundaryStartPoints`: `-1e155` and above hang, `-1e154` and below return. - **How found:** `fuzz_guards` sweep of the `blend.fillet_geom_p1` FFI guards (PR #147), located with gdb; the weight bypass by the PR's pre-review; the shape dependence and the `1e15` onset by its re-review (timing probes and a 120 s-per-call fuzz). - **Confirmed:** yes (gdb backtrace 25 frames deep in `GlobalCoincidenceChecker`; never returned; timings measured in the reference build). - **Severity:** hang (above `1e154`); seconds to about a minute per call from `1e15`. -- **Rust port:** The FFI wrapper rejects, for the ops that reach `sm_ClassifyFacePoint` (`ClassifyFacePoint`, `FbSplitFace`), any value and any Euclidean control point (`Pw / w`) above `MAX_MAGNITUDE = 1e150`, the overflow regime (see `crates/solidcore-ffi/src/units/blend_fillet_geom_p1.rs`); slow inputs under it are valid upstream inputs and are accepted (the guard fuzz allows those two ops 180 s per call). The other ops take any finite value. The Rust `classify_face_point` still defers on `topology.face_p1`. Since 2026-10-21 also: `boolean.topology_intersector_p1`/`_p2` refuse surface control point coordinates of `MAX_SURFACE_COORD = 1e154` or more; `heal.gap`'s topology op (`MAX_TOPO_BLOCK_VALUE`) and every `heal.heal_data_p2`/`_p3` B-rep block (`MAX_BLOCK_VALUE`) refuse a block value of `1e154` or more; `blend.fillet_geom_p2`'s `RailRail` bounds its original face's surface by `MAX_MAGNITUDE`. Regression inputs: `crates/difftest/tests/fuzz_regressions/`. +- **Rust port:** The FFI wrapper rejects, for the ops that reach `sm_ClassifyFacePoint` (`ClassifyFacePoint`, `FbSplitFace`), any value and any Euclidean control point (`Pw / w`) above `MAX_MAGNITUDE = 1e150`, the overflow regime (see `crates/solidcore-ffi/src/units/blend_fillet_geom_p1.rs`); slow inputs under it are valid upstream inputs and are accepted (the guard fuzz allows those two ops 180 s per call). The other ops take any finite value. The Rust `classify_face_point` still defers on `topology.face_p1`. Since 2026-10-21 also: `boolean.topology_intersector_p1`/`_p2` refuse surface and curve control point coordinates of `MAX_GEOMETRY_COORD = 1e154` or more; `heal.gap`'s topology op (`MAX_TOPO_BLOCK_VALUE`) and every `heal.heal_data_p2`/`_p3` B-rep block (`MAX_BLOCK_VALUE`) refuse a block value of `1e154` or more; `blend.fillet_geom_p2`'s `RailRail` bounds its original face's surface by `MAX_MAGNITUDE`. Regression inputs: `crates/difftest/tests/fuzz_regressions/`. #### HANG-20. The global solvers never finish when a surface coordinate's own box arithmetic overflows - **Location:** `SmGlobalSolver::SolveBranch` with each of its local solvers: `SmCSIGlobalSolver::LocalSolve` -> `SmCurve::SimpleCoincidenceChecker` -> `SmCurve::LocalPointSolve` -> `SmLocalSolve1d::SolveIt` (gdb after 25 s on the cliff program: eleven `SolveBranch` frames, the time in `LocalSolve`), reached from `SmSurface::GlobalCurveIntersect` (`SmFilletVertex::CalcCliffRailIntEdge`), from `SmSurface::GlobalPointSolve` (`sm_SurfaceDropPoint`) and from `SmSurface::PointBasedSurfaceIntersect` (`sm_TwoPntsIntersection`), all in `SmFilletGeom.cpp`. From 62099e3aaaea63afd261a66ef5251ddf88ca021d Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 04:47:10 -0400 Subject: [PATCH 005/491] plan/findings.md: remaining fuzz timeouts after the HANG-29 guards Co-Authored-By: Claude Opus 5.5 (1M context) --- plan/findings.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plan/findings.md b/plan/findings.md index 8d2060c68d..492648933f 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -11281,5 +11281,7 @@ guarded in `offset.offset_geometry_creation`; the fix belongs to the curve cache - Hangs on surface control points of `1e154` or more: new triggers of HANG-29 in `plan/upstream-issues.md` (also in `heal.gap`, `heal.heal_data_p2` and `blend.fillet_geom_p2`); the FFIs now refuse them. - Slow, not hung: `fuzz09748` (p1) and `fuzz15421` (p2) set `/approx = 1e300`. Replayed with `approx` from `1e3` to `1e300`, the C++ returns in 22-25 s every time (`1`: 0.5 s), just over the fuzz harness's fixed 20 s budget (`CASE_TIMEOUT`). These are valid upstream inputs, so the FFI accepts them; they stay `timeout` verdicts in the fuzz until the harness has a per-unit budget. -- 5 `differs` in p2 (`fuzz06628`, `fuzz08367`, `fuzz19033`, `fuzz19955`, `fuzz23628`): `values.state.len` is 3 shorter on the Rust side in each. Not yet assessed. +- Also slow, not hung: p2 `fuzz07961` and `fuzz29932` (`/approx = -1e+300`, accepted as upstream accepts negative tolerances): the C++ returns in 12.5 s alone, past the fuzz budget only under load. +- 5 `differs` in p2 (`fuzz06628`, `fuzz08367`, `fuzz19033`, `fuzz19955`, `fuzz23628`): a squeeze whose vertex was related to one of its own B-rep; fixed in the shim by #360. +- Open, `blend.fillet_geom_p2` `fuzz23694` (`SrfSrf`, base `gen0060`, `/query/s1/b_spline/net/0/1/2 = 1.0`): a valid input on which the C++ gives no result within 120 s, in `SmLSIGlobalSolver::LocalSolve` -> `SmSurface::LocalPointSolve` under a deep `SmGlobalSolver::SolveBranch` recursion (gdb after 40 s). Not guarded (a valid input); not yet known whether it terminates. From 8083b0f15f96f99b894e3a58c719ea17b530dc72 Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 09:02:46 -0400 Subject: [PATCH 006/491] fix: GlueCoincidentBrepVertices stops once the second vertex is gone (#371) Port of the SolidCraft fix tangency-labs/smlib da8d05e. For two coincident vertices joined by several common edges, the loop over those edges squeezed a short one or glued the vertices (deleting the second vertex) and then carried on with the next edge, using the deleted vertex (UB-241, a use-after-free upstream). The loop now stops once the vertex is recorded as deleted, in both B-reps' halves. Regression test glue_stops_once_the_second_vertex_is_glued_away: a B-rep stand-in with two long common edges, where the second edge's step glued the already-deleted vertex again. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/topology/proto_topology/p3.rs | 223 ++++++++++++++++++ plan/upstream-issues.md | 10 + 2 files changed, 233 insertions(+) diff --git a/crates/solidcore/src/topology/proto_topology/p3.rs b/crates/solidcore/src/topology/proto_topology/p3.rs index d9c049afb2..57695467a3 100644 --- a/crates/solidcore/src/topology/proto_topology/p3.rs +++ b/crates/solidcore/src/topology/proto_topology/p3.rs @@ -15,6 +15,11 @@ //! | [`ProtoTopologyManager::resolve_overlapping_proto_edges`] | `SmProtoTopologyManager::ResolveOverlappingProtoEdges` | //! //! Deviations: +//! - **Deleted vertex in `GlueCoincidentBrepVertices`** (SolidCraft fix, +//! tangency-labs/smlib da8d05e, UB-241). For two coincident vertices joined by +//! several common edges, upstream goes on to the next edge after squeezing or +//! gluing away the second vertex, and uses it after the free. The loop stops once +//! that vertex is recorded as deleted, in both B-reps' halves. //! - **B-rep access.** Topology objects are the caller's handles `O`, as in //! [`p1`](super::p1). What these functions read from vertices and edges, and the //! store new curves go to, is [`ProtoBrep`] (of `topology.proto_topology_p2`, which @@ -1112,6 +1117,13 @@ impl ProtoTopologyManager { } } else { for &edge in edges { + // SolidCraft change (tangency-labs/smlib da8d05e): squeezing + // or gluing for an earlier common edge deleted v2; every + // later step would use it (and the edges it ended) after the + // free, so stop. + if deleted_v.get(jj).copied().flatten() == Some(v2) { + break; + } let curve_handle = brep.edge_curve(edge).ok_or(dangling("edge is unknown"))?; let ivl = brep @@ -1663,3 +1675,214 @@ fn set_slot(list: &mut [Option], index: usize, vertex: O) { *slot = Some(vertex); } } + +#[cfg(test)] +mod tests { + use super::super::p1::proto::{ProtoGeometry, ProtoTopology, VertexDefinition}; + use super::*; + use crate::curve::line::Line; + use crate::geom::extent2d::Extent2d; + use crate::intersect::point_class::PointClassType; + use crate::surface::surface::Surface; + use crate::surface::volume::Volume; + use crate::topology::Brep; + + #[derive(Debug, Clone, Copy, PartialEq)] + struct H(usize, ClassObjectKind); + + impl ClassObject for H { + fn class_kind(&self) -> ClassObjectKind { + self.1 + } + } + + const fn vtx(i: usize) -> H { + H(i, ClassObjectKind::Vertex) + } + + const fn edg(i: usize) -> H { + H(i, ClassObjectKind::Edge) + } + + /// Vertices 0 and 1 of B-rep 1 coincide and are joined by the long edges 0 and 1 + /// (both on curve 0); vertices 2 and 3 of B-rep 2 are far apart. Records every + /// `GlueVertices` call and refuses one of a vertex already glued away. + struct Pool { + curves: Vec, + glued: Vec<(H, H)>, + } + + impl Pool { + fn new() -> Self { + let line = Line::create_line_segment( + 3, + Vec3::new(0.0, 0.0, 0.0), + Vec3::new(10.0, 0.0, 0.0), + None, + ) + .unwrap(); + Self { + curves: vec![Curve::from(line)], + glued: Vec::new(), + } + } + } + + impl ClassGeometry for Pool { + fn curve(&self, o: H) -> Option<&Curve> { + (o.1 == ClassObjectKind::Curve) + .then(|| self.curves.get(o.0)) + .flatten() + } + fn surface(&self, _o: H) -> Option<&Surface> { + None + } + fn volume(&self, _o: H) -> Option<&Volume> { + None + } + fn point(&self, _o: H) -> Option { + None + } + } + + impl ProtoGeometry for Pool { + fn curve_mut(&mut self, o: H) -> Option<&mut Curve> { + (o.1 == ClassObjectKind::Curve) + .then(|| self.curves.get_mut(o.0)) + .flatten() + } + } + + impl ProtoTopology for Pool { + fn vertex_use_faces(&self, _vertex: H) -> Result>> { + Ok(Vec::new()) + } + fn edge_use_faces(&self, _edge: H) -> Result>> { + Ok(Vec::new()) + } + fn edge_start_vertex(&self, _edge: H) -> Result> { + Ok(Some(vtx(0))) + } + fn edge_other_vertex(&self, _edge: H, _vertex: Option) -> Result> { + Ok(Some(vtx(1))) + } + fn vertex_point(&self, vertex: H) -> Result { + Ok(match vertex.0 { + 2 => Point3::new(0.0, 5.0, 0.0), + 3 => Point3::new(0.0, -5.0, 0.0), + _ => Point3::new(0.0, 0.0, 0.0), + }) + } + fn edge_brep(&self, _edge: H) -> Result> { + Ok(None) + } + fn edge_faces(&self, _edge: H) -> Result> { + Ok(Vec::new()) + } + } + + impl ProtoBrep for Pool { + fn edge_interval(&self, _edge: H) -> Option { + Some(self.curves.first()?.natural_interval()) + } + fn edge_curve(&self, _edge: H) -> Option> { + Some(Some(H(0, ClassObjectKind::Curve))) + } + fn topology_tolerance(&self, _topo: H) -> Option { + Some(1e-5) + } + fn face_uv_domain(&self, _face: H) -> Option { + None + } + fn set_face_uv_domain(&mut self, _face: H, _domain: Extent2d) -> Option<()> { + None + } + fn face_surface(&self, _face: H) -> Option> { + None + } + fn edge_edgeuse_of_face(&self, _edge: H, _face: H) -> Result> { + Ok(None) + } + fn edge_is_connected_to_face(&self, _edge: H, _face: H) -> Result { + Ok(false) + } + fn insert_curve(&mut self, curve: Curve) -> Option { + self.curves.push(curve); + Some(H(self.curves.len() - 1, ClassObjectKind::Curve)) + } + fn vertex_common_edges(&self, v1: H, v2: H) -> Result> { + Ok(if (v1, v2) == (vtx(0), vtx(1)) { + vec![edg(0), edg(1)] + } else { + Vec::new() + }) + } + fn face_curve_on_classify( + &self, + _k: &Kernel, + _face: H, + _do_point_classify: bool, + _cc: &mut CurveClassification, + ) -> Result { + Ok(false) + } + } + + impl BrepGlue for Pool { + fn glue_vertices( + &mut self, + _k: &mut Kernel, + _brep: H, + v1: H, + v2: H, + _opt_wire_edges_in_brep: Option<&[H]>, + ) -> Result<()> { + let again = self.glued.iter().any(|&(_, gone)| gone == v2); + self.glued.push((v1, v2)); + if again { + return Err(SolidError::invalid_input( + "test", + "vertex already glued away", + )); + } + Ok(()) + } + } + + fn region_pc(o: H) -> PointClassification { + let mut pc = PointClassification::new(1e-5); + pc.set_class_object(PointClassType::Region, Some(o)); + pc + } + + #[test] + fn glue_stops_once_the_second_vertex_is_glued_away() { + // da8d05e (#371): two coincident vertices joined by two long common edges; + // gluing for the first edge deletes the second vertex, which the second + // edge's step glued again (upstream: a use-after-free) + let mut k = Kernel::default(); + let mut pool = Pool::new(); + let b = H(100, ClassObjectKind::Other); + let e = Brep::new(); + let mut m = ProtoTopologyManager::new(&k, (b, &e), (b, &e), b, 1e-5, 1e-3, -1.0).unwrap(); + let p = Point3::new(0.0, 0.0, 0.0); + let pv = m.new_proto_vertex(p, 1e-5).unwrap(); + m.add_proto_vertex(pv); + let r = H(50, ClassObjectKind::Region); + for (v1, v2) in [(vtx(0), vtx(2)), (vtx(1), vtx(3))] { + let mut vd = + VertexDefinition::new(&k, &pool, ®ion_pc(r), ®ion_pc(r), p, 1e-5, 1e-6, None) + .unwrap(); + vd.set_final_vertex(1, Some(v1)); + vd.set_final_vertex(2, Some(v2)); + let id = m.insert_vertex_definition(vd).unwrap(); + m.add_vertex_definition(pv, id).unwrap(); + } + + m.glue_coincident_brep_vertices(&mut k, &mut pool, &mut NoMates) + .unwrap(); + assert_eq!(pool.glued, vec![(vtx(0), vtx(1))]); + // the second definition, whose B-rep 1 vertex is gone, is deleted + assert_eq!(m.proto_vertex(pv).unwrap().vertex_definitions().len(), 1); + } +} diff --git a/plan/upstream-issues.md b/plan/upstream-issues.md index b07b0f4339..f7933b4ae0 100644 --- a/plan/upstream-issues.md +++ b/plan/upstream-issues.md @@ -1218,6 +1218,16 @@ meantime, whoever merges renumbers the new entry and every reference to it (here - **Severity:** crash/UB (use-after-free), after every output is final. - **Rust port:** The arena teardown (`ProtoTopologyManager::destroy`) skips edges already deleted, so it is defined. `IntersectInsertRelate` detects the condition before the teardown (replaying the unlisting) and returns `SolidError::InvalidInput` after destroying the manager. The difftest shim runs a verbatim copy of `IntersectInsertRelate` that unlinks the stale entries before the delete and answers `-2` in that case; the shim's own manager teardown unlinks them too. +#### UB-241. `SmProtoTopologyManager::GlueCoincidentBrepVertices` keeps using a vertex it has deleted +- **Location:** `source/SMLib/src/SmProtoTopology.cpp:5854` `GlueCoincidentBrepVertices`, the common-edge loops at `:5999` (Brep 1) and `:6091` (Brep 2). +- **What happens:** For two coincident vertices joined by several common edges, the loop squeezes a short edge (`SqueezeEdge` deletes the second vertex) or glues the vertices (`GlueVertices` deletes it), records the vertex as deleted, and then goes on to the next edge. That step calls `GetOtherVertex`, `SqueezeEdge` or `GlueVertices` with the freed vertex. +- **Trigger:** a shell operation (ASan reports the use-after-free in `sm_RmFromSpatialTree`, through `SmBrep::Notify` from `GlueVertices`; tangency-labs/solidcore#371). +- **How found:** ASan run of the SolidCraft C API (tangency-labs/smlib `capi/`). +- **Confirmed:** yes (ASan, in the fork's C API tests); not reproduced in the solidcore difftest. +- **Severity:** crash/UB (use-after-free). +- **Fix:** tangency-labs/smlib da8d05e stops the loop once the vertex is recorded as deleted, in both B-reps' halves. +- **Rust port:** carries the fix (test `glue_stops_once_the_second_vertex_is_glued_away`, where two long common edges glued the deleted vertex a second time). Without it nothing is freed, but the later step glued, squeezed or deleted through a vertex that was already gone. + ### Topology #### UB-105. `SmVertex::GetMaxUpDimGap3d` adds to a NULL gap array From 412e1e5424f06d8c45a7dea8fcadb559bf8671aa Mon Sep 17 00:00:00 2001 From: ian691 Date: Wed, 21 Oct 2026 15:27:33 +0200 Subject: [PATCH 007/491] fix: stabilize generated curve curvature-extrema reports Retain the original flat-circle case as a complete native parity regression while using noncircular ellipses for generated extrema cases. Co-Authored-By: Codex --- crates/difftest/src/units/curve_curve_p7.rs | 99 ++++++++++++++++- .../tests/data/curve_p7_flat_circle.json | 105 ++++++++++++++++++ plan/findings.md | 17 +++ plan/modules.toml | 2 +- reports/difftest/curve.curve_p7.json | 14 +-- 5 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 crates/difftest/tests/data/curve_p7_flat_circle.json diff --git a/crates/difftest/src/units/curve_curve_p7.rs b/crates/difftest/src/units/curve_curve_p7.rs index 1bf950f331..87c53fac57 100644 --- a/crates/difftest/src/units/curve_curve_p7.rs +++ b/crates/difftest/src/units/curve_curve_p7.rs @@ -20,6 +20,11 @@ //! intersection (`LocalCurveIntersect`, through `SmCCIntersectENFO::Evaluate`) and the //! property solves that reach `SmCurve::LocalPropertyAnalysis` (every span the property //! solver does not cull). +//! +//! Generated circle-derived curvature-extrema cases use noncircular ellipses: a +//! circle's constant curvature otherwise makes the number of noise-selected extrema +//! machine-dependent. The original flat-circle case remains a full native regression +//! outside the canonical report corpus; no solver or comparison rule is changed. use std::collections::BTreeMap; @@ -784,12 +789,26 @@ fn gen_circle(rng: &mut Rng, closed: bool) -> CurveIn { } fn gen_curve(rng: &mut Rng) -> CurveIn { + gen_curve_with_circle_origin(rng).0 +} + +fn gen_curve_with_circle_origin(rng: &mut Rng) -> (CurveIn, bool) { let s = if rng.chance(0.7) { 3.0 } else { 30.0 }; match rng.below(10) { - 0 => gen_circle(rng, false), - 1 => gen_circle(rng, true), - 2..=6 => CurveIn::Bspline(gen_bspline(rng, s)), - _ => CurveIn::Hermite(gen_hermite(rng, s)), + 0 => (gen_circle(rng, false), true), + 1 => (gen_circle(rng, true), true), + 2..=6 => (CurveIn::Bspline(gen_bspline(rng, s)), false), + _ => (CurveIn::Hermite(gen_hermite(rng, s)), false), + } +} + +// Generated circles have their X axis along global X. Stretching homogeneous X +// by exactly two gives a noncircular rational ellipse without new random draws. +fn stretch_circle_x(curve: &mut CurveIn) { + if let CurveIn::Bspline(bs) = curve { + for point in &mut bs.cpts { + point[0] *= 2.0; + } } } @@ -1043,8 +1062,11 @@ fn gen_cci(rng: &mut Rng) -> CciIn { } fn gen_property(rng: &mut Rng) -> OpIn { - let curve = gen_curve(rng); + let (mut curve, from_circle) = gen_curve_with_circle_origin(rng); let prop = rng.below(29) as i32; + if from_circle && matches!(prop, 13 | 14) { + stretch_circle_x(&mut curve); + } let (a, bb) = range_of(&curve); let t = a + (bb - a) * rng.unit(); let p = point_at(&curve, t); @@ -1164,6 +1186,73 @@ impl UnitDiff for Runner { mod tests { use super::*; + // Compare the complete ordered native result, including its solution count. + // Constant-curvature circles intentionally have no frozen extrema count. + fn assert_native_parity(op: &OpIn) -> (usize, f64) { + let native = cpp(op).unwrap(); + let port = rust(op).unwrap(); + for out in [&native, &port] { + assert!(out.error.is_none(), "{out:?}"); + assert!(out.deferred.is_none(), "{out:?}"); + assert!(out.rejected.is_none(), "{out:?}"); + assert_eq!(out.ints.get("status"), Some(&vec![0])); + assert!(!out.values["out"].is_empty()); + } + let comparison = crate::compare(&native, &port, &[]); + assert!(comparison.pass(), "{comparison:?}"); + assert!(comparison.values_compared > 0); + (comparison.values_compared, native.values["out"][0]) + } + + #[test] + fn flat_circle_original_gen1409_retains_full_native_parity() { + let op: OpIn = serde_json::from_str(include_str!("../../tests/data/curve_p7_flat_circle.json")).unwrap(); + assert!(assert_native_parity(&op).1 > 0.0); + } + + #[test] + fn generated_ellipse_extrema_have_stable_report_shapes() { + // Frozen after replacing the seven circle-derived extrema inputs. CI on + // Linux must reproduce these macOS shapes AND compare every native value. + // No case may become rejected, deferred or error-only. + for (index, prop, expected_values) in [ + (191, 14, 942), (510, 14, 981), (1409, 13, 929), + (1526, 13, 16), (1682, 13, 955), (2276, 14, 916), (2526, 14, 16), + ] { + let seed = CaseContext::seed_for("curve.curve_p7", index); + let input = Runner.generate(&mut Rng::new(seed)).unwrap(); + let op: OpIn = serde_json::from_value(input).unwrap(); + assert!(matches!(&op, OpIn::Property { prop: actual, .. } if *actual == prop)); + assert_eq!(assert_native_parity(&op).0, expected_values, "gen{index:04}, seed {seed}"); + } + } + + #[test] + fn stretched_circle_has_known_nonconstant_curvature_and_native_extrema() { + // Semiaxes 4 and 2: rho_min = b*b/a = 1, rho_max = a*a/b = 8. + let circle = CurveIn::Circle(CircleIn { + center: [0.0; 3], x: [1.0, 0.0, 0.0], y: [0.0, 1.0, 0.0], + radius: 2.0, a0: 0.0, a1: 360.0, + }); + let mut ellipse = resolve(&circle).unwrap(); + stretch_circle_x(&mut ellipse); + let curve = build(&ellipse).unwrap(); + let (a, b) = range_of(&ellipse); + for (fraction, expected) in [(0.0, 1.0), (0.25, 8.0), (0.5, 1.0), (0.75, 8.0)] { + let derivatives = curve.evaluate(a + fraction * (b - a), 2, true, false).unwrap(); + let speed = derivatives[1].length(); + let radius = speed * speed * speed / derivatives[1].cross(derivatives[2]).length(); + assert!((radius - expected).abs() <= 1e-9 * expected, "{radius} != {expected}"); + } + for prop in [13, 14] { + let (_, solutions) = assert_native_parity(&OpIn::Property { + curve: ellipse.clone(), ivl: [a, b], prop, val: 0.0, + vecs: vec![[0.0; 3]; 3], tol: 1e-6, comb: true, + }); + assert!(solutions > 0.0); + } + } + /// Generated cases cover every op, with passing (not deferred) cases. #[test] fn generated_cases_are_diverse() { diff --git a/crates/difftest/tests/data/curve_p7_flat_circle.json b/crates/difftest/tests/data/curve_p7_flat_circle.json new file mode 100644 index 0000000000..d4a0c08874 --- /dev/null +++ b/crates/difftest/tests/data/curve_p7_flat_circle.json @@ -0,0 +1,105 @@ +{ + "comb": true, + "curve": { + "bspline": { + "cpts": [ + [ + 4.509329707598036, + -1.6176860458042364, + 0.37178685699264424, + 1.0 + ], + [ + 5.5436055560310376, + 1.044454648452781, + 0.8984189550280688, + 0.7071067811865476 + ], + [ + 4.6172073270559535, + 4.675447183620512, + 2.1994119681578965, + 1.0 + ], + [ + 0.9861116661799192, + 5.5676261687824375, + 2.212019279586531, + 0.7071067811865476 + ], + [ + -1.9359399618619269, + 4.779044463185695, + 2.229498251520921, + 1.0 + ], + [ + -3.7239442161851084, + 1.19096332623882, + 0.9409673850014574, + 0.7071067811865476 + ], + [ + -2.0438175813198436, + -1.5140887662390528, + 0.40187314035566835, + 1.0 + ], + [ + 0.8335496736660108, + -3.332208194090837, + -0.3726329395570052, + 0.7071067811865476 + ], + [ + 4.509329707598036, + -1.6176860458042364, + 0.37178685699264424, + 1.0 + ] + ], + "dim": 3, + "oob": false, + "p": 2, + "u": [ + 0.0, + 0.0, + 0.0, + 0.25, + 0.25, + 0.5, + 0.5, + 0.75, + 0.75, + 1.0, + 1.0, + 1.0 + ] + } + }, + "ivl": [ + 0.0, + 1.0 + ], + "op": "property", + "prop": 13, + "tol": 0.001, + "val": 1.7732820777376772, + "vecs": [ + [ + 2.2326173468970767, + 5.93750786982702, + 2.565934289843482 + ], + [ + -0.5102210063849107, + 0.058767684342949426, + 0.8580331484975021 + ], + [ + -0.6228072934871176, + -0.15083094712622644, + 0.7676985740303656 + ] + ] +} \ No newline at end of file diff --git a/plan/findings.md b/plan/findings.md index c2c6275d11..d4c5067493 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -2411,6 +2411,23 @@ No Rust/C++ disagreements (3136 cases, 3048 bit-identical, 88 deferred on `curve ## curve.curve_p7 (`SmCCIGlobalSolver::LocalSolve`, `SmCurve::GlobalPropertyAnalysis`) +- **Flat-circle report stability (#357).** Exact rational circles have constant + radius of curvature; rounding noise can select different extrema counts on + different machines even when C++ and Rust agree locally. The generated circle + family now uses a noncircular rational ellipse (homogeneous X coordinates scaled + by exactly two) only for Minimize/MaximizeRadiusOfCurvature (13/14). Circle-origin + provenance is carried through generation; no random draws, seeds, operation + choices, validation rules, solver implementation or comparison tolerances change. Seven + of 3000 generated inputs change; the remaining 2993 and all 27 fixtures remain. + The original gen1409 (seed 7577092825472747628) is retained in + `crates/difftest/tests/data/curve_p7_flat_circle.json` as a complete ordered native + parity regression, including solution count, outside autodiscovered report + fixtures. Its platform-dependent extrema count is deliberately not frozen. + Separate analytic ellipse controls check radii 1 and 8 for semiaxes 4 and 2; + seven frozen generated-case output sizes also test report shape across macOS + and Linux without weakening the full native comparison. This addresses the + curve half only; the polar-box work remains separate. + - **Pruned curve trees.** `LocalSolve` casts every leaf's `m_pData` to `SmBezierSpan` (the `m_eAuxDataType == SM_AD_BEZIER_CURVE` check is an `SM_ASSERT`). A cache built with aux data but without a full tree (`bComputeAuxTreeData && !bMakeFullTree`) turns parents diff --git a/plan/modules.toml b/plan/modules.toml index 2782a11cfc..e9fb7bdf9a 100644 --- a/plan/modules.toml +++ b/plan/modules.toml @@ -1400,7 +1400,7 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "green" claimed_by = "swarm-curve.curve_p7-a1" claimed_at = "2026-10-13T04:46:38Z" -notes = "GREEN (promoted by curve.curve_p5: 3027 cases, 0 deferred after its stubs were wired); PARTIAL: difftest 3027 cases (27 fixture: SM_API_test TestSmCreateRectangle / TestSmCreateRegularPolygon edge pairs, my_test_reversed_line_join segments, arc plane intersection / classification with comb, edge cases: coincident, tolerance-limit gaps, degenerate Hermite, self-intersecting loop, projected / perspective, sub-interval + 3000 generated: whole SolveIt and single leaf-pair LocalSolve for intersect, intersection test and the three projected intersections, GlobalPropertyAnalysis over all 29 properties with/without comb), 1716 pass bit-identical (max_rel_err 0), 1311 deferred (curve.curve_p5 1311: LocalCurveIntersect -> SmCCIntersectENFO::Evaluate, LocalPropertyAnalysis); --perturb SENSITIVE; co-ported sm_FindAdditionalCcTangentGuess (curve.curve_p5); wired UNPORTED(curve.curve_p7) in curve.curve_p2 (CciGlobalSolver::local_solve, solver now carries curves + kernel) and nurbs.b_spline_curve_p3 (AssertValid GlobalPropertyAnalysis: its 116 deferred moved to curve.curve_p5); no unit promoted. 2 fns; largest: SmCCIGlobalSolver::LocalSolve, SmCurve::GlobalPropertyAnalysis; cycle back-edges (call into later units; stub or co-port): curve.curve_p5(2); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit" +notes = "Report stability (#357): generated circle-derived radius extrema use noncircular rational ellipses without extra RNG draws; original gen1409 retained as full native parity regression outside canonical report fixtures; 3027/3027 pass, 0 deferred, max_rel_err 0; solver and 1e-9 comparison unchanged. GREEN (promoted by curve.curve_p5: 3027 cases, 0 deferred after its stubs were wired); PARTIAL: difftest 3027 cases (27 fixture: SM_API_test TestSmCreateRectangle / TestSmCreateRegularPolygon edge pairs, my_test_reversed_line_join segments, arc plane intersection / classification with comb, edge cases: coincident, tolerance-limit gaps, degenerate Hermite, self-intersecting loop, projected / perspective, sub-interval + 3000 generated: whole SolveIt and single leaf-pair LocalSolve for intersect, intersection test and the three projected intersections, GlobalPropertyAnalysis over all 29 properties with/without comb), 1716 pass bit-identical (max_rel_err 0), 1311 deferred (curve.curve_p5 1311: LocalCurveIntersect -> SmCCIntersectENFO::Evaluate, LocalPropertyAnalysis); --perturb SENSITIVE; co-ported sm_FindAdditionalCcTangentGuess (curve.curve_p5); wired UNPORTED(curve.curve_p7) in curve.curve_p2 (CciGlobalSolver::local_solve, solver now carries curves + kernel) and nurbs.b_spline_curve_p3 (AssertValid GlobalPropertyAnalysis: its 116 deferred moved to curve.curve_p5); no unit promoted. 2 fns; largest: SmCCIGlobalSolver::LocalSolve, SmCurve::GlobalPropertyAnalysis; cycle back-edges (call into later units; stub or co-port): curve.curve_p5(2); unit-SCC #22 (220 units); 317 upstream test entry points reach this unit" [[unit]] id = "curve.ellipse_p1" diff --git a/reports/difftest/curve.curve_p7.json b/reports/difftest/curve.curve_p7.json index 73dd629869..4e58969201 100644 --- a/reports/difftest/curve.curve_p7.json +++ b/reports/difftest/curve.curve_p7.json @@ -1751,7 +1751,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472746410", - "values_compared": 2749 + "values_compared": 942 }, { "max_rel_err": 0.0, @@ -4303,7 +4303,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472746729", - "values_compared": 1605 + "values_compared": 981 }, { "max_rel_err": 0.0, @@ -11495,7 +11495,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472747628", - "values_compared": 2320 + "values_compared": 929 }, { "max_rel_err": 0.0, @@ -12431,7 +12431,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472747745", - "values_compared": 965 + "values_compared": 16 }, { "max_rel_err": 0.0, @@ -13679,7 +13679,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472747901", - "values_compared": 1462 + "values_compared": 955 }, { "max_rel_err": 0.0, @@ -18431,7 +18431,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472748495", - "values_compared": 1319 + "values_compared": 916 }, { "max_rel_err": 0.0, @@ -20431,7 +20431,7 @@ "outcome": "pass", "pass": true, "source": "generated:seed=7577092825472748745", - "values_compared": 757 + "values_compared": 16 }, { "max_rel_err": 0.0, From 633b87e155b68a4915e4963dc8bcc83e925bbf9d Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 21 Oct 2026 09:59:15 -0400 Subject: [PATCH 008/491] plan/findings.md: the two Rust-side slow fuzz cases are bounded heal.gap fuzz14344 (52 s, per-drop cache builds on an overflowing curve) and offset_geometry_creation fuzz07978 (34 s, 5.3M coincidence split nodes against at most 15 139 in any fixture) terminate on inputs the FFI refuses. No bound added: the C++ terminates slowly on valid inputs of the same shape (HANG-29), where a bound would fire. Co-Authored-By: Claude Opus 5.5 (1M context) --- plan/findings.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plan/findings.md b/plan/findings.md index 492648933f..c9ed494b94 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -11284,4 +11284,8 @@ guarded in `offset.offset_geometry_creation`; the fix belongs to the curve cache - Also slow, not hung: p2 `fuzz07961` and `fuzz29932` (`/approx = -1e+300`, accepted as upstream accepts negative tolerances): the C++ returns in 12.5 s alone, past the fuzz budget only under load. - 5 `differs` in p2 (`fuzz06628`, `fuzz08367`, `fuzz19033`, `fuzz19955`, `fuzz23628`): a squeeze whose vertex was related to one of its own B-rep; fixed in the shim by #360. - Open, `blend.fillet_geom_p2` `fuzz23694` (`SrfSrf`, base `gen0060`, `/query/s1/b_spline/net/0/1/2 = 1.0`): a valid input on which the C++ gives no result within 120 s, in `SmLSIGlobalSolver::LocalSolve` -> `SmSurface::LocalPointSolve` under a deep `SmGlobalSolver::SolveBranch` recursion (gdb after 40 s). Not guarded (a valid input); not yet known whether it terminates. +- Rust slow, not hung, on inputs the FFI refuses (the fuzz runs the Rust side on them): + - `heal.gap` `fuzz14344` (a Hermite curve point of `1e300`): 52 s in `GapFunction::build_sample_set` -> `Curve::drop_point`. Each drop builds a curve cache (a documented deviation: upstream keeps one on the curve). Its tessellation never reached 1000 leaves; the time is the many cache builds on an overflowing curve. On the unit's fixtures the Rust side takes 0.52× the C++ time. + - `offset.offset_geometry_creation` `fuzz07978` (control-point weights of `±1e-300`, so Euclidean coordinates near `1e300`): 34 s in `Curve::global_coincidence_checker`, 5 319 076 split nodes against at most 15 139 over every green and partial unit's fixtures. `MAX_COINCIDENCE_DEPTH` bounds it; upstream, at these magnitudes, is HANG-29. + - No node or sample bound was added. The C++ terminates, slowly, on valid inputs of the same shape (HANG-29's `1e15`..`1e150` range, up to 99 s), so a bound would fire where upstream returns. From 01a707f17096a21984742c7356800722cb4d695a Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Fri, 23 Oct 2026 13:03:20 +0200 Subject: [PATCH 009/491] docs: propose scoped claims for completion work Co-Authored-By: OpenAI Codex --- AGENTS.md | 50 +++++++++++++++++++++++++++++++++++++++++--------- README.md | 18 +++++++++++++----- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2f3a23067e..680d1d93b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,7 +141,11 @@ Every ported file starts with: Unit list and states: `plan/units/.toml`, one file per unit (format: `plan/modules.schema.md`, order: `plan/order.md`). **`plan/PROGRESS.md` is the checklist** (regenerated on `main` by CI; -never edit it in a PR). **Any `todo` unit can be claimed.** A unit is *ready* when its `depends_on` are all +never edit it in a PR). **Any `todo` unit or unclaimed completion task can be claimed.** A claim reserves +its stated scope, not every future change to the unit. Missing functions, back-edge +wiring, exact runner imports, fixtures and demonstrated parity bugs remain useful +work in `partial` and `green` units; inspect source and reports, not status alone. +A unit is *ready* when its `depends_on` are all `green` or `partial` (`soft_depends_on` never blocks; see section 4a); ready units are the easiest, lowest `layer` first. Claiming a unit that is not ready is fine: - Calls into a missing dependency are stubbed (`UNPORTED()`, cases deferred; 4a). @@ -156,14 +160,42 @@ the easiest, lowest `layer` first. Claiming a unit that is not ready is fine: into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or `nice -n 10 harness/cpp/build.sh --jobs 6` to build from source. -1. **Claim = a draft PR.** Check nobody has it: - `gh pr list --state open --search " in:title"`. If none, branch from the - latest `main` (`git fetch origin && git switch -c unit/ origin/main`), - `git commit --allow-empty -m "claim: "`, push, and open a draft PR titled - exactly **`port: `**. **The lowest-numbered open PR for a unit wins**; if - yours lost, close it. Do not edit `plan/units/` to claim. Push WIP at least - daily; a draft PR with no pushes for 48 h may be taken over (comment on it first). - If the unit's `notes` mention a WIP branch on `origin`, start from it. +1. **Claim = a scoped draft PR.** Search open PR titles and bodies for the unit, + symbols and affected paths, including `wire:`, `fix:` and `difftest:` work; a title + search for `port:` alone misses completion work. Branch from the latest `main`. + Do not edit `plan/units/` to claim. + Initial whole-unit ports keep the title **`port: `**. For completion work, + use a descriptive title such as **`wire: — `** and name + the unit in the body. An empty claim commit is sufficient to open a draft. + + Include this scope in the PR body (update it as the work changes): + ```text + Units: + In scope: upstream symbols, Rust functions and intended behavior + Expected edits: paths and shared interfaces + Evidence: failing/deferred case IDs or missing branches; validation planned + Related PRs / dependencies: + Available follow-ups: independent work others can take + Status / blockers / next step: + ``` + The earliest open claim wins only for overlapping work. Non-overlapping work in + the same unit is welcome. Link the related PR and state the boundary before + starting; independent work need not wait for a whole-unit owner to reply. + If work changes the same function or shared interface, agree on a split in the + existing PR first, or build on its branch and document the dependency. If the + boundary is unclear, ask there and take a different independent task meanwhile. + Never implement a competing version of an actively claimed change. + + Push WIP or post a concrete progress update at least daily while implementing. + After 48 h without either, comment with the exact scope you propose to take over + and allow 24 h for a response; then continue from published WIP, crediting its + author. Do not take over a PR merely because it awaits review or CI: offer review, + conflict resolution or a coordinated follow-up instead. A blocker update should + identify what is blocked and release independent work, rather than reserve the + whole unit indefinitely. When pausing, release the scope and link the WIP. + Existing PR authors retain their active scope; this is not a reset of claims. + If the unit's notes mention a WIP branch, inspect it before starting. + 2. **Port** the upstream functions the unit owns (listed in `harness/callgraph/units.json`) into its `rust_module`, plus the C++ shim (`harness/cpp/shim/.cpp`, functions `sc__`, plain diff --git a/README.md b/README.md index 2f36611e73..31cdc08485 100644 --- a/README.md +++ b/README.md @@ -107,12 +107,20 @@ so they are cached like the Rust crates. ## Contributing -Contributions are pull requests, one per port unit, and AI agents are welcome: +Contributions are pull requests, and AI agents are welcome: -1. Pick any unclaimed unit in [`plan/PROGRESS.md`](plan/PROGRESS.md). "Ready" ones are - easiest; the others need stubs or co-porting of missing dependencies. -2. Claim it by opening a draft PR titled `port: ` (lowest-numbered PR wins). -3. Port it, prove it with difftest against the C++ reference, mark the PR ready. +1. Find an unclaimed unit in [`plan/PROGRESS.md`](plan/PROGRESS.md), or a concrete + completion task in an existing unit: a missing function, back-edge, exact runner + import, coverage gap or parity bug. Green reports alone do not prove every + upstream branch is exercised. +2. Search open PR titles and bodies for the unit, symbols and paths. Claim the + specific work in a draft PR; initial ports use `port: `. A claim covers + its stated scope, so independent tasks in the same unit remain available. + Coordinate overlapping functions or shared interfaces in the existing PR first. +3. Describe the intended behavior, affected paths, related PRs and validation in the + draft, and list independent follow-ups others can take. Prove the change against + the C++ reference before marking it ready. See [AGENTS.md, section 4](AGENTS.md#4-porting-a-unit-pr-workflow) + for the claim template, handoffs and inactivity rules. To keep parallel PRs from conflicting: edit only your unit's `plan/units/.toml`, never commit `reports/difftest/` (CI generates them), and only append to From 2755b050211761afc01d0405f08533a357be6966 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Fri, 23 Oct 2026 13:08:51 +0200 Subject: [PATCH 010/491] docs: clarify precedence for overlapping port claims Co-Authored-By: OpenAI Codex --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 680d1d93b3..56288c2d03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -178,7 +178,9 @@ into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or Available follow-ups: independent work others can take Status / blockers / next step: ``` - The earliest open claim wins only for overlapping work. Non-overlapping work in + For overlapping whole-unit `port:` claims, the lowest-numbered open PR wins. + For completion tasks, the earliest open claim wins only for overlapping work. + Non-overlapping work in the same unit is welcome. Link the related PR and state the boundary before starting; independent work need not wait for a whole-unit owner to reply. If work changes the same function or shared interface, agree on a split in the From 4fbe08cea2d4df7469313b3450a0141a33dd0b12 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Fri, 23 Oct 2026 13:21:38 +0200 Subject: [PATCH 011/491] docs: propose completion evidence for port and wiring changes Co-Authored-By: OpenAI Codex --- AGENTS.md | 22 ++++++++++ docs/completion-evidence.md | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 docs/completion-evidence.md diff --git a/AGENTS.md b/AGENTS.md index 2f3a23067e..d4ac2ec8a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -245,6 +245,28 @@ into `upstream/` (see README), then run `scripts/fetch-cpp.sh`, or with just that so others can see it. Never sit on a draft PR you have stopped working on: close it, and say where the WIP is. +### Completion evidence + +For new ports and completion changes, include the [completion evidence checklist](docs/completion-evidence.md) +in the PR description. Identify the owned functions and branches implemented, which +operations actually complete successfully against C++, and which remain unreachable. +A passing status or empty result is evidence for that case, not proof that the +operation's successful construction or mutation path works. + +A dependency stub replaces the missing call only. Port the upstream logic following +it as well, preserving control flow and operation order; do not omit the continuation +and present the function as complete. If an incremental PR deliberately leaves an +owned body or continuation missing, list the exact symbols and remaining work. +Report that limitation even if existing fixtures never reach it. Unit status follows +the existing gate; it does not certify complete function or branch coverage. + +When a dependency is wired, rerun the affected callers and add cases that reach the +previously blocked continuations. Keep newly exposed mismatches visible. Review +runner skips, stand-in geometry and absent-entity cases before claiming coverage. +Record independent harness follow-ups with reproducible evidence and check existing +PRs before implementing them. This checklist supplements the existing correctness +gate; it does not change tolerances, outcomes or status accounting. + ## 4a. Cycles, back-edges and the merge lock The kernel core is one big call cycle, so the plan dropped ~2,300 calls as *back-edges* diff --git a/docs/completion-evidence.md b/docs/completion-evidence.md new file mode 100644 index 0000000000..5515bb2ced --- /dev/null +++ b/docs/completion-evidence.md @@ -0,0 +1,88 @@ +# Completion evidence for port and wiring PRs + +This checklist makes implementation gaps and useful follow-up tasks visible during +completion of the port. It complements AGENTS.md and the native comparison gate. +It does not add a second status system or change acceptance tolerances. + +## PR evidence + +Use a short table for the operations affected by the PR. Group identical paths; +there is no need to inventory unrelated functions on every change. + +| Operation / upstream symbols | Implementation | Successful native comparison | Remaining gap | +| --- | --- | --- | --- | +| `` | Complete body, or exact missing branches | Case IDs and observable results, or none | Dependency, missing body, runner/export gap, or none | + +Include the tested commit, native summary lines and affected callers. Distinguish +these forms of evidence: + +- A successful operation compared against the real native operation, including + observable geometry, topology or state changes appropriate to that operation. +- A legitimate refusal, error or no-op compared against native behavior. +- A path blocked by an unported call or a harness limitation. + +All are useful information. Only the first demonstrates the corresponding successful +operation. An operation that legitimately has no successful input needs an explanation, +not an invented success case. Status-only tests remain useful for status contracts; +use an observable result when claiming geometry or mutation coverage. + +## Dependency stubs and missing continuations + +An `Unported` call should occupy the place of the unavailable upstream call, with +the surrounding algorithm ported faithfully. Do not end a function at the first +missing dependency and silently omit the upstream continuation. A compiling function +with an early return is not proof that its complete body has been implemented. + +Small incremental PRs remain welcome. If an owned body, branch or continuation is +still missing, name it in the evidence table and identify an independent follow-up +where possible. Do not infer implementation completeness from a green unit report +or count all of a unit's upstream functions as implemented without checking them. + +Once a dependency becomes available: + +1. Wire the actual service with the caller's real geometry, topology and context. +2. Exercise the previously blocked continuation and its relevant boundary branches. +3. Rerun affected callers, retaining existing case identities and comparisons. +4. Diagnose newly reached mismatches; do not hide them by broadening guards, adding + deferrals or loosening tolerances. + +## Turning harness gaps into contributor tasks + +Before debugging or taking a task, search current open PRs for its symbols and paths. +The historical inventories in issues #210 and #294 are starting points; their counts +and ownership must be checked against current main. + +For each available task, provide this information in an issue or related PR body: + +```text +Unit / operation / symbols: +Current main SHA and reproducing command / case IDs: +Gap: missing body, missing dependency wiring, shim/export, or generator +Expected edit boundary and related active PRs: +Evidence needed to finish: +``` + +Useful independent tasks include constructing real B-reps instead of opaque handles, +preserving analytic geometry types in exports, replacing stale runner skips with +real calls, and generating existing entities so the intended operation is exercised. +Each task needs a checkable result, not just fewer deferred cases. + +Keep legitimate empty-input and refusal tests. Add meaningful successful cases rather +than deleting negative cases to improve a coverage percentage. A broader native crash +guard must not be narrowed without evidence for the exact inputs it protects. + +For runner or shim changes, show that both engines execute the intended operation +on equivalent inputs. Preserve previous output comparisons and add operation-specific +checks for the new results. Use the existing perturbation check where applicable. +A lower deferral count alone does not establish equivalence. + +## Reporting scope + +The table above is manual PR evidence today. Automated per-operation success-path +reporting would need a separate reviewed implementation defining operation identities, +success semantics and serialization for each runner; this document does not claim +that such a metric already exists. + +The numeric gate and existing unit states remain unchanged. Any future change to +completion accounting or treatment of confirmed reference defects must be proposed +and implemented separately with auditable evidence. From 5674103dad7c893fb5fdbd02b08d14df3e4ba05f Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Fri, 23 Oct 2026 14:43:49 +0200 Subject: [PATCH 012/491] docs: require evidence for removed dependency stubs Co-Authored-By: OpenAI Codex --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d4ac2ec8a5..532450bce8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -248,7 +248,8 @@ on: close it, and say where the WIP is. ### Completion evidence For new ports and completion changes, include the [completion evidence checklist](docs/completion-evidence.md) -in the PR description. Identify the owned functions and branches implemented, which +in the PR description. The evidence table is required for `wire:` PRs that remove +`UNPORTED` stubs, including when existing fixtures do not reach the continuation. Identify the owned functions and branches implemented, which operations actually complete successfully against C++, and which remain unreachable. A passing status or empty result is evidence for that case, not proof that the operation's successful construction or mutation path works. From 97d2c171a4aeb597bda9466280b6f89264b706c1 Mon Sep 17 00:00:00 2001 From: momysnow Date: Fri, 23 Oct 2026 19:35:41 +0200 Subject: [PATCH 013/491] difftest: SolidCraft #674 case (partial face contact of a cylinder end) Three --solidcraft-cases from tangency-labs/solidcraft#674: contact of a cylinder end with a block face, half of it hanging below the block, and robustness seed 16 as a 4-copy circular pattern. Expected volumes are closed form. every_case_file_loads: 57 -> 60 cases. Co-Authored-By: Claude Sonnet 5.5 --- crates/difftest/src/solidcraft.rs | 2 +- .../solidcraft/674-partial-face-contact.json | 224 ++++++++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 fixtures/solidcraft/674-partial-face-contact.json diff --git a/crates/difftest/src/solidcraft.rs b/crates/difftest/src/solidcraft.rs index dd404d4adc..36f10b9a7b 100644 --- a/crates/difftest/src/solidcraft.rs +++ b/crates/difftest/src/solidcraft.rs @@ -882,7 +882,7 @@ mod tests { let cases = all(); assert_eq!( cases.len(), - 57, + 60, "{:?}", cases.iter().map(|c| &c.id).collect::>() ); diff --git a/fixtures/solidcraft/674-partial-face-contact.json b/fixtures/solidcraft/674-partial-face-contact.json new file mode 100644 index 0000000000..a0d3f32533 --- /dev/null +++ b/fixtures/solidcraft/674-partial-face-contact.json @@ -0,0 +1,224 @@ +{ + "issue": "https://github.com/tangency-labs/solidcraft/issues/674", + "pr": "https://github.com/tangency-labs/solidcraft/pull/704", + "test": "crates/engine/src/tests/robustness.rs: fixed_seeds_run_clean (seed 16, PR #704); contact case as crates/kernel/src/issue_674_tests.rs: partial_circular_contact_preserves_material_and_history (added in PR #782)", + "summary": "Block 25 x 47.5 x 37.5 at the origin, joined with r 5, 2.5 long cylinders. Robustness seed 16 is a 4-copy circular pattern about Z of the cylinder based at (0,7.5,0) along +X, so half its end hangs below the bottom edge z=0. The runner has no pattern on union, so the copies are written out. Volumes are closed form: block plus the parts of each cylinder outside it. The copies do not overlap each other. Tolerance 0.01 mm^3, as the other analytic cases (SolidCraft's tests allow 0.1 and 0.2 for their mesh measure).", + "cases": [ + { + "name": "partial_face_contact_back_to_back", + "note": "#674 contact, as issue_674_tests.rs: end on face x=0 from outside, half of it below z=0. Adds a whole cylinder.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + 0, + 0, + 0 + ], + "size": [ + 25, + 47.5, + 37.5 + ] + }, + { + "op": "cylinder", + "id": "c4", + "origin": [ + 0, + 7.5, + 0 + ], + "axis": [ + -1, + 0, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c4" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 44727.59954084936, + "tol": 0.01, + "formula": "25*47.5*37.5 + pi*5^2*2.5 = 44531.25 + 62.5*pi" + } + } + }, + { + "name": "half_embedded_end_control", + "note": "Control: the 0 degree copy alone. Its end is on face x=0 and half of it lies inside the block, so only the half below z=0 adds material.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + 0, + 0, + 0 + ], + "size": [ + 25, + 47.5, + 37.5 + ] + }, + { + "op": "cylinder", + "id": "c1", + "origin": [ + 0, + 7.5, + 0 + ], + "axis": [ + 1, + 0, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c1" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 44629.42477042468, + "tol": 0.01, + "formula": "25*47.5*37.5 + (1/2)*pi*5^2*2.5 = 44531.25 + 31.25*pi" + } + } + }, + { + "name": "seed_16_pattern", + "note": "Robustness seed 16 as a 4-copy circular pattern about Z. The 90 and 180 degree copies stay clear of the block and of each other, so 3 lumps.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + 0, + 0, + 0 + ], + "size": [ + 25, + 47.5, + 37.5 + ] + }, + { + "op": "cylinder", + "id": "c1", + "origin": [ + 0, + 7.5, + 0 + ], + "axis": [ + 1, + 0, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c1" + }, + { + "op": "cylinder", + "id": "c2", + "origin": [ + -7.5, + 0, + 0 + ], + "axis": [ + 0, + 1, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c2" + }, + { + "op": "cylinder", + "id": "c3", + "origin": [ + 0, + -7.5, + 0 + ], + "axis": [ + -1, + 0, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c3" + }, + { + "op": "cylinder", + "id": "c4", + "origin": [ + 7.5, + 0, + 0 + ], + "axis": [ + 0, + -1, + 0 + ], + "radius": 5, + "height": 2.5 + }, + { + "op": "union", + "id": "result", + "a": "result", + "b": "c4" + } + ], + "expect": { + "lumps": 3, + "volume": { + "value": 45218.473392972766, + "tol": 0.01, + "formula": "25*47.5*37.5 + 3.5*pi*5^2*2.5 = 44531.25 + 218.75*pi" + } + } + } + ] +} From 36aa7a7c9411b69830cf5250afd84d219daf2aa1 Mon Sep 17 00:00:00 2001 From: momysnow Date: Fri, 23 Oct 2026 20:04:38 +0200 Subject: [PATCH 014/491] difftest: SolidCraft #788 case (equal bores crossing at right angles) Fixture from tangency-labs/solidcraft#788 with closed-form expected values. every_case_file_loads: 57 -> 60 cases. Co-Authored-By: Claude Sonnet 5.5 --- crates/difftest/src/solidcraft.rs | 2 +- .../solidcraft/788-equal-bores-crossing.json | 224 ++++++++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 fixtures/solidcraft/788-equal-bores-crossing.json diff --git a/crates/difftest/src/solidcraft.rs b/crates/difftest/src/solidcraft.rs index dd404d4adc..36f10b9a7b 100644 --- a/crates/difftest/src/solidcraft.rs +++ b/crates/difftest/src/solidcraft.rs @@ -882,7 +882,7 @@ mod tests { let cases = all(); assert_eq!( cases.len(), - 57, + 60, "{:?}", cases.iter().map(|c| &c.id).collect::>() ); diff --git a/fixtures/solidcraft/788-equal-bores-crossing.json b/fixtures/solidcraft/788-equal-bores-crossing.json new file mode 100644 index 0000000000..8f373c1e39 --- /dev/null +++ b/fixtures/solidcraft/788-equal-bores-crossing.json @@ -0,0 +1,224 @@ +{ + "issue": "https://github.com/tangency-labs/solidcraft/issues/788", + "pr": "https://github.com/tangency-labs/solidcraft/pull/1179", + "test": "crates/kernel/src/issue_788_tests.rs: equal_bores_crossing_at_right_angles", + "summary": "A 45 mm cube with two equal bores crossing at right angles at its centre, cut one after the other; with r 10 SolidCraft refused the second cut (r 17.5 built before the fix too, per the issue's table; r 4 is from the test). The Steinmetz solid the bores share, 16 r^3/3, lies inside the cube for r < 22.5, so the removed volume is 2 pi r^2 45 - 16 r^3/3. Radii 10, 17.5 and 4 as in the test. Tolerance 0.01 mm^3.", + "cases": [ + { + "name": "equal_bores_crossing_r10", + "note": "Two bores of radius 10 through a 45 mm cube, axes Z and Y, crossing at the centre.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + -22.5, + -22.5, + -22.5 + ], + "size": [ + 45, + 45, + 45 + ] + }, + { + "op": "cylinder", + "id": "b1", + "origin": [ + 0, + 0, + -30 + ], + "axis": [ + 0, + 0, + 1 + ], + "radius": 10, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b1" + }, + { + "op": "cylinder", + "id": "b2", + "origin": [ + 0, + -30, + 0 + ], + "axis": [ + 0, + 1, + 0 + ], + "radius": 10, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b2" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 68183.99945102519, + "tol": 0.01, + "formula": "45^3 - 2*pi*10^2*45 + 16*10^3/3 (the Steinmetz solid of the crossing is removed once)" + } + } + }, + { + "name": "equal_bores_crossing_r17_5", + "note": "Control from the issue: radius 17.5 built before the fix too. Two bores of radius 17.5 through a 45 mm cube, axes Z and Y, crossing at the centre.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + -22.5, + -22.5, + -22.5 + ], + "size": [ + 45, + 45, + 45 + ] + }, + { + "op": "cylinder", + "id": "b1", + "origin": [ + 0, + 0, + -30 + ], + "axis": [ + 0, + 0, + 1 + ], + "radius": 17.5, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b1" + }, + { + "op": "cylinder", + "id": "b2", + "origin": [ + 0, + -30, + 0 + ], + "axis": [ + 0, + 1, + 0 + ], + "radius": 17.5, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b2" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 33118.18581876466, + "tol": 0.01, + "formula": "45^3 - 2*pi*17.5^2*45 + 16*17.5^3/3 (the Steinmetz solid of the crossing is removed once)" + } + } + }, + { + "name": "equal_bores_crossing_r4", + "note": "Two bores of radius 4 through a 45 mm cube, axes Z and Y, crossing at the centre.", + "steps": [ + { + "op": "box", + "id": "result", + "origin": [ + -22.5, + -22.5, + -22.5 + ], + "size": [ + 45, + 45, + 45 + ] + }, + { + "op": "cylinder", + "id": "b1", + "origin": [ + 0, + 0, + -30 + ], + "axis": [ + 0, + 0, + 1 + ], + "radius": 4, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b1" + }, + { + "op": "cylinder", + "id": "b2", + "origin": [ + 0, + -30, + 0 + ], + "axis": [ + 0, + 1, + 0 + ], + "radius": 4, + "height": 60 + }, + { + "op": "difference", + "id": "result", + "a": "result", + "b": "b2" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 86942.43991216403, + "tol": 0.01, + "formula": "45^3 - 2*pi*4^2*45 + 16*4^3/3 (the Steinmetz solid of the crossing is removed once)" + } + } + } + ] +} From 462befb1807b1574f24f87381a936b729e8afff0 Mon Sep 17 00:00:00 2001 From: momysnow Date: Fri, 23 Oct 2026 20:04:48 +0200 Subject: [PATCH 015/491] difftest: SolidCraft #232 case (union of two spheres of the same size) Fixture from tangency-labs/solidcraft#232 with closed-form expected values. every_case_file_loads: 57 -> 60 cases. Co-Authored-By: Claude Sonnet 5.5 --- crates/difftest/src/solidcraft.rs | 2 +- .../232-overlapping-equal-spheres.json | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 fixtures/solidcraft/232-overlapping-equal-spheres.json diff --git a/crates/difftest/src/solidcraft.rs b/crates/difftest/src/solidcraft.rs index dd404d4adc..36f10b9a7b 100644 --- a/crates/difftest/src/solidcraft.rs +++ b/crates/difftest/src/solidcraft.rs @@ -882,7 +882,7 @@ mod tests { let cases = all(); assert_eq!( cases.len(), - 57, + 60, "{:?}", cases.iter().map(|c| &c.id).collect::>() ); diff --git a/fixtures/solidcraft/232-overlapping-equal-spheres.json b/fixtures/solidcraft/232-overlapping-equal-spheres.json new file mode 100644 index 0000000000..309f560bbc --- /dev/null +++ b/fixtures/solidcraft/232-overlapping-equal-spheres.json @@ -0,0 +1,128 @@ +{ + "issue": "https://github.com/tangency-labs/solidcraft/issues/232", + "pr": "https://github.com/tangency-labs/solidcraft/pull/838", + "test": "crates/kernel/src/tests.rs: similar_spheres_meet (PR #838, the fix); crates/engine/src/tests.rs: join_of_two_overlapping_equal_spheres_is_not_refused (PR #836, on top of it)", + "summary": "Union of two spheres of radius 10, which SolidCraft refused with 'contact work exceeds the limit' (centres 12 and 4 mm apart, per the issue). The overlap is a lens of volume pi (4r + d)(2r - d)^2 / 12. d 12 is the engine test's join; d 4 and d 20.5 are positions from the kernel test, which checks only whether the spheres meet, so their volumes and lump counts here are closed form. The tangent position d 20 is left out (point contact). Tolerance 0.01 mm^3.", + "cases": [ + { + "name": "overlapping_equal_spheres_d12", + "note": "Two spheres of radius 10 with centres 12 mm apart.", + "steps": [ + { + "op": "sphere", + "id": "a", + "center": [ + 0, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "sphere", + "id": "b", + "center": [ + 12, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "union", + "id": "result", + "a": "a", + "b": "b" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 7506.312046977212, + "tol": 0.01, + "formula": "2*(4/3)*pi*10^3 - pi*(4*10+12)*(2*10-12)^2/12" + } + } + }, + { + "name": "overlapping_equal_spheres_d4", + "note": "Two spheres of radius 10 with centres 4 mm apart.", + "steps": [ + { + "op": "sphere", + "id": "a", + "center": [ + 0, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "sphere", + "id": "b", + "center": [ + 4, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "union", + "id": "result", + "a": "a", + "b": "b" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 5428.672105403162, + "tol": 0.01, + "formula": "2*(4/3)*pi*10^3 - pi*(4*10+4)*(2*10-4)^2/12" + } + } + }, + { + "name": "separate_equal_spheres_d20_5", + "note": "Control: centres 20.5 mm apart do not touch (the kernel test's `meets` = false); the union keeps two lumps.", + "steps": [ + { + "op": "sphere", + "id": "a", + "center": [ + 0, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "sphere", + "id": "b", + "center": [ + 20.5, + 0, + 0 + ], + "radius": 10 + }, + { + "op": "union", + "id": "result", + "a": "a", + "b": "b" + } + ], + "expect": { + "lumps": 2, + "volume": { + "value": 8377.580409572782, + "tol": 0.01, + "formula": "2*(4/3)*pi*10^3" + } + } + } + ] +} From 7afb5e1e4254eb22452724e08fd7f97a96a00ab1 Mon Sep 17 00:00:00 2001 From: momysnow Date: Fri, 23 Oct 2026 20:04:58 +0200 Subject: [PATCH 016/491] difftest: SolidCraft #1668 case (rounds on both sides of a thin wall) Fixture from tangency-labs/solidcraft#1668 with closed-form expected values. every_case_file_loads: 57 -> 60 cases. Co-Authored-By: Claude Sonnet 5.5 --- crates/difftest/src/solidcraft.rs | 2 +- .../1668-round-both-sides-of-wall.json | 183 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 fixtures/solidcraft/1668-round-both-sides-of-wall.json diff --git a/crates/difftest/src/solidcraft.rs b/crates/difftest/src/solidcraft.rs index dd404d4adc..36f10b9a7b 100644 --- a/crates/difftest/src/solidcraft.rs +++ b/crates/difftest/src/solidcraft.rs @@ -882,7 +882,7 @@ mod tests { let cases = all(); assert_eq!( cases.len(), - 57, + 60, "{:?}", cases.iter().map(|c| &c.id).collect::>() ); diff --git a/fixtures/solidcraft/1668-round-both-sides-of-wall.json b/fixtures/solidcraft/1668-round-both-sides-of-wall.json new file mode 100644 index 0000000000..b08f52b67d --- /dev/null +++ b/fixtures/solidcraft/1668-round-both-sides-of-wall.json @@ -0,0 +1,183 @@ +{ + "issue": "https://github.com/tangency-labs/solidcraft/issues/1668", + "pr": "https://github.com/tangency-labs/solidcraft/pull/1720", + "test": "crates/kernel/src/issue_1080_tests.rs: rounds_on_both_sides_of_a_wall_build_as_one_fillet", + "summary": "A 104 x 64 x 42 block less a 100 x 60 x 41 cavity open at the bottom (2 mm walls and roof), as in the test (the issue's 100 x 60 x 40 box shelled outward 2 mm). R5 on the outer top edge (y = 32, z = 42, length 104) together with R5 on the concave inner roof/wall edge below it (y = 30, z = 40, length 100) in one fillet was refused as too large (largest 4 mm), although the two built one after the other; #1720 made the fit check count the material the inner round adds. The outer round removes 25(1-pi/4) mm^2 along 104 mm, the inner one adds it along 100 mm: the cavity corner (30, 40) lies 3*sqrt(2) = 4.24 mm from the outer round's ball centre (27, 37), inside its radius, so the removed region stays in material and misses the added one. #1720's test also refused R5 on the outer edge alone; SolidCraft PR #2852 later let it build (it leaves the inner corner 0.76 mm inside the round and reaches it only past 2*sqrt(2)/(sqrt(2)-1) = 6.83 mm), and the test on main builds it. The R7 refusals (outer edge alone, and both edges) are not expressible in the runner. Tolerances are 0.1 % of the outer round's volume. Volumes are closed form.", + "cases": [ + { + "name": "shelled_block", + "note": "Control: 2 mm walls and roof, open bottom, no rounds.", + "steps": [ + { + "op": "box", + "id": "outer", + "origin": [ + -52, + -32, + 0 + ], + "size": [ + 104, + 64, + 42 + ] + }, + { + "op": "box", + "id": "cavity", + "origin": [ + -50, + -30, + -1 + ], + "size": [ + 100, + 60, + 41 + ] + }, + { + "op": "difference", + "id": "result", + "a": "outer", + "b": "cavity" + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 39552.0, + "tol": 0.01, + "formula": "104*64*42 - 100*60*40" + } + } + }, + { + "name": "round_5_outer_edge_alone", + "note": "R5 on the outer top edge only: refused when #1668 was fixed, builds since SolidCraft PR #2852 (the inner corner stays 0.76 mm inside the round).", + "steps": [ + { + "op": "box", + "id": "outer", + "origin": [ + -52, + -32, + 0 + ], + "size": [ + 104, + 64, + 42 + ] + }, + { + "op": "box", + "id": "cavity", + "origin": [ + -50, + -30, + -1 + ], + "size": [ + 100, + 60, + 41 + ] + }, + { + "op": "difference", + "id": "body", + "a": "outer", + "b": "cavity" + }, + { + "op": "fillet", + "id": "result", + "body": "body", + "edges": [ + [ + 0, + 32, + 42 + ] + ], + "radius": 5 + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 38994.035224833366, + "tol": 0.5579647751666345, + "formula": "104*64*42 - 100*60*40 - 25*(1-pi/4)*104 (tol: 0.1 % of the outer round)" + } + } + }, + { + "name": "round_5_both_sides_of_wall", + "note": "#1668: R5 on the outer top edge and the inner roof/wall edge in one fillet call, refused before #1720.", + "steps": [ + { + "op": "box", + "id": "outer", + "origin": [ + -52, + -32, + 0 + ], + "size": [ + 104, + 64, + 42 + ] + }, + { + "op": "box", + "id": "cavity", + "origin": [ + -50, + -30, + -1 + ], + "size": [ + 100, + 60, + 41 + ] + }, + { + "op": "difference", + "id": "body", + "a": "outer", + "b": "cavity" + }, + { + "op": "fillet", + "id": "result", + "body": "body", + "edges": [ + [ + 0, + 32, + 42 + ], + [ + 0, + 30, + 40 + ] + ], + "radius": 5 + } + ], + "expect": { + "lumps": 1, + "volume": { + "value": 39530.53981633975, + "tol": 0.5579647751666345, + "formula": "104*64*42 - 100*60*40 - 25*(1-pi/4)*104 + 25*(1-pi/4)*100 (tol: 0.1 % of the outer round)" + } + } + } + ] +} From 94c3c759b15bd9f81c287a9d2ce39a40b5199029 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 12:37:10 +0200 Subject: [PATCH 017/491] test: expose public solid conversion and reflection mismatches Co-Authored-By: OpenAI Codex --- .../src/units/topology_proto_topology_p1.rs | 73 ++++- .../src/units/topology_proto_topology_p1.rs | 14 +- .../public_scale_phase2.json | 289 ++++++++++++++++++ .../cpp/shim/topology_proto_topology_p1.cpp | 51 +++- 4 files changed, 418 insertions(+), 9 deletions(-) create mode 100644 fixtures/topology.proto_topology_p1/public_scale_phase2.json diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index 94c564a16e..485530a8b5 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -310,6 +310,8 @@ pub(super) enum Script { }, /// One object and one API call. Transform { obj: TObjIn, op: TOpIn }, + /// A constructed-solid transform with runtime type and directed topology records. + ScaleProbe { obj: TObjIn, op: TOpIn }, } // ---- encoding for the C++ side ------------------------------------------------------------ @@ -433,8 +435,8 @@ pub(super) fn cpp_input(s: &Script) -> Vec { v.extend_from_slice(o); } } - Script::Transform { obj, op } => { - v.push(kind::TRANSFORM as f64); + Script::Transform { obj, op } | Script::ScaleProbe { obj, op } => { + v.push(if matches!(s, Script::ScaleProbe { .. }) { kind::SCALE_PROBE } else { kind::TRANSFORM } as f64); match obj { TObjIn::Curve { owned, c } => { v.extend([tobj::CURVE as f64, b(*owned)]); @@ -4408,7 +4410,67 @@ const ANCHOR: ObjectRef = ObjectRef { key: ObjectKey(1), }; -fn run_transform(obj: &TObjIn, op: &TOpIn) -> Result, Fail> { +fn traversal_label(items: &[T], item: Option) -> f64 { + item.and_then(|item| items.iter().position(|entry| *entry == item)).map_or(0.0, |i| (i + 1) as f64) +} + +struct ScaleProbeFrame { + vertices: Vec, + edges: Vec, + faces: Vec, + shells: Vec, + regions: Vec, + edgeuses: Vec, + faceuses: Vec<(Option, Option)>, +} + +impl ScaleProbeFrame { + fn new(k: &mut Kernel, body: &Brep) -> solidcore::Result { + use solidcore::topology::brep::p1 as bp; + let vertices = bp::vertices(body, None)?; + let edges = bp::edges(body, None)?; + let faces = bp::faces(body, None)?; + let shells = bp::shells(k, body, None)?; + let regions = bp::regions(body, None)?; + let mut edgeuses = Vec::new(); + for edge in &edges { edgeuses.extend(solidcore::topology::edge::edgeuses(body, *edge, None, None)?); } + let faceuses = faces.iter().map(|face| solidcore::topology::face::p2::faceuses(body, *face)).collect::>>()?; + Ok(Self { vertices, edges, faces, shells, regions, edgeuses, faceuses }) + } +} + +fn append_scale_probe(body: &Brep, frame: &ScaleProbeFrame, out: &mut Vec) -> solidcore::Result<()> { + use solidcore::topology::edgeuse::p1 as eu; + out.extend([frame.shells.len() as f64, frame.regions.len() as f64]); + for edge in &frame.edges { + let record = body.edge(*edge)?; + out.push(record.curve.as_ref().map_or(0, Curve::type_code) as f64); + let uses = solidcore::topology::edge::edgeuses(body, *edge, None, None)?; + out.push(uses.len() as f64); + for id in uses { + let use_ = body.edgeuse(id)?; + let vertex = eu::vertexuse(body, id)?.map(|v| body.vertexuse(v)).transpose()?.and_then(|v| v.vertex); + out.extend([traversal_label(&frame.edgeuses, Some(id)), use_.orientation as u8 as f64, + traversal_label(&frame.vertices, vertex), traversal_label(&frame.edgeuses, eu::mate(body, id)?), + traversal_label(&frame.edgeuses, eu::radial(body, id)?), traversal_label(&frame.faces, eu::face(body, id)?)]); + } + } + for (face, pair) in frame.faces.iter().zip(&frame.faceuses) { + let record = body.face(*face)?; + out.push(record.surface.as_ref().map_or(0, Surface::type_code) as f64); + for id in [pair.0, pair.1] { + if let Some(id) = id { + let use_ = body.faceuse(id)?; + let region = eu::faceuse_region(body, id)?; + out.extend([1.0, use_.orientation as u8 as f64, traversal_label(&frame.shells, use_.shell), traversal_label(&frame.regions, region), + region.map(|r| body.region(r)).transpose()?.map_or(-1.0, |r| f64::from(r.is_void))]); + } else { out.extend([0.0, 0.0, 0.0, 0.0, -1.0]); } + } + } + Ok(()) +} + +fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail> { let mut k = Kernel::default(); let mut curve = None; let mut surface = None; @@ -4554,6 +4616,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn) -> Result, Fail> { source = Some(brep); brep = copy; } + let probe_frame = if probe { Some(st(ScaleProbeFrame::new(&mut k, &brep))?.map_err(|code| harness(format!("scale frame status {code}")))?) } else { None }; let target = match obj { TObjIn::Composite { .. } | TObjIn::CrvOnSurf { .. } => { special.as_mut().map(TransformObject::Curve) @@ -4598,6 +4661,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn) -> Result, Fail> { st(crate::util::primitive::append(&k, &brep, &mut o))?.map_err(|code| harness(format!("solid geometry status {code}")))?; for face in faces { o.push(f64::from(brep.face(face).map_err(|e| harness(e.to_string()))?.rectangular_trim)); } } + if let Some(frame) = probe_frame { st(append_scale_probe(&brep, &frame, &mut o))?.map_err(|code| harness(format!("scale probe status {code}")))?; } if let Some(source) = source { o.extend(source_before); st(crate::util::primitive::append(&k, &source, &mut o))? @@ -4637,7 +4701,8 @@ pub(super) fn rust_run(s: &Script) -> Result, Fail> { override_tol, ops, } => run_proto(objects, *approx_tol, *capture, *override_tol, ops), - Script::Transform { obj, op } => run_transform(obj, op), + Script::Transform { obj, op } => run_transform(obj, op, false), + Script::ScaleProbe { obj, op } => run_transform(obj, op, true), } } diff --git a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs index fcf174f375..6c39e6acec 100644 --- a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs +++ b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs @@ -78,6 +78,8 @@ pub mod kind { pub const PROTO: i64 = 1; /// One API transformation. pub const TRANSFORM: i64 = 2; + /// Constructed-solid transform with representation and orientation record. + pub const SCALE_PROBE: i64 = 4; } /// List-script slot kinds. @@ -831,8 +833,10 @@ fn validate_proto(rd: &mut Reader<'_>) -> Result<(), FfiError> { Ok(()) } -fn validate_transform(rd: &mut Reader<'_>) -> Result<(), FfiError> { - match rd.int(0, tobj::COPIED_SPHERE)? { +fn validate_transform(rd: &mut Reader<'_>, probe: bool) -> Result<(), FfiError> { + let object = rd.int(0, tobj::COPIED_SPHERE)?; + if probe && !matches!(object, tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE) { return Err(bad("scale probe requires constructed solid")); } + match object { tobj::CALLER_FACE => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("face dimension")); } } } tobj::PRIMITIVE_BOX | tobj::COPIED_BOX => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("box dimension")); } } } tobj::PRIMITIVE_SPHERE | tobj::COPIED_SPHERE => { rd.vec3()?; if rd.finite()?.abs() <= 1e-12 { return Err(bad("sphere radius")); } } @@ -912,10 +916,12 @@ fn validate_transform(rd: &mut Reader<'_>) -> Result<(), FfiError> { /// [`FfiError::InvalidArgument`] for a malformed script. pub fn validate(input: &[f64]) -> Result<(), FfiError> { let mut rd = Reader::new(input); - match rd.int(0, kind::TRANSFORM)? { + match rd.int(0, kind::SCALE_PROBE)? { kind::LIST => validate_list(&mut rd)?, kind::PROTO => validate_proto(&mut rd)?, - _ => validate_transform(&mut rd)?, + kind::TRANSFORM => validate_transform(&mut rd, false)?, + kind::SCALE_PROBE => validate_transform(&mut rd, true)?, + _ => return Err(bad("unknown script kind")), } if rd.at != input.len() { return Err(bad("trailing data")); diff --git a/fixtures/topology.proto_topology_p1/public_scale_phase2.json b/fixtures/topology.proto_topology_p1/public_scale_phase2.json new file mode 100644 index 0000000000..1ecd2c3f52 --- /dev/null +++ b/fixtures/topology.proto_topology_p1/public_scale_phase2.json @@ -0,0 +1,289 @@ +{ + "cases": [ + { + "name": "public_scale_phase2_box_anisotropic", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_box": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 2, + 3, + 4 + ] + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_phase2_sphere_anisotropic", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_sphere": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2 + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_phase2_copied_box_anisotropic", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "copied_box": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 2, + 3, + 4 + ] + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_phase2_copied_sphere_anisotropic", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "copied_sphere": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2 + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_phase2_box_odd_reflection", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_box": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 2, + 3, + 4 + ] + } + }, + "op": { + "scale": { + "s": [ + -1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_phase2_sphere_odd_reflection", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_sphere": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2 + } + }, + "op": { + "scale": { + "s": [ + -1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_phase2_box_even_reflection", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_box": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 2, + 3, + 4 + ] + } + }, + "op": { + "scale": { + "s": [ + -1, + -1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_phase2_sphere_all_negative", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_sphere": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2 + } + }, + "op": { + "scale": { + "s": [ + -1, + -1, + -1 + ] + } + } + } + }, + { + "name": "public_scale_phase2_copied_sphere_scale_by_point", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "copied_sphere": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2 + } + }, + "op": { + "scale_by_pt": { + "r": [ + 1, + -2, + 3 + ], + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_phase2_box_zero_rejected", + "upstream_ref": "source/SM_API/src/SmApiGeneral.cpp:SmApiScale;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_box": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 2, + 3, + 4 + ] + } + }, + "op": { + "scale": { + "s": [ + 0, + 1, + 1 + ] + } + } + } + } + ] +} diff --git a/harness/cpp/shim/topology_proto_topology_p1.cpp b/harness/cpp/shim/topology_proto_topology_p1.cpp index 74d25f4280..785148adaa 100644 --- a/harness/cpp/shim/topology_proto_topology_p1.cpp +++ b/harness/cpp/shim/topology_proto_topology_p1.cpp @@ -22,6 +22,11 @@ #include "support/primitive_record.h" #include #include +#include +#include +#include +#include +#include #include "StdAfx.h" #include @@ -4853,10 +4858,50 @@ public: ScCos(SmPoint2d& a, SmPoint2d& b, SmSurface& s, const SmContext* ctx) : SmCrvOnSurf(a, b, s, NULL, 0, ctx) {} }; -int32_t run_transform(Rd& rd, std::vector& o) +template double scale_label(const std::vector& items, T* item) { + if (!item) return 0.0; + auto it=std::find(items.begin(),items.end(),item); + return it==items.end() ? 0.0 : (double)(std::distance(items.begin(),it)+1); +} +struct ScaleProbeFrame { + std::vector vertices; + std::vector edges; + std::vector faces; + std::vector shells; + std::vector regions; + std::vector edgeuses; + std::vector> faceuses; + explicit ScaleProbeFrame(SmBrep* body) { + SmTArray vs; body->GetVertices(vs); for(ULONG i=0;i es; body->GetEdges(es); for(ULONG i=0;i us; es[i]->GetEdgeuses(us); for(ULONG j=0;j fs; body->GetFaces(fs); for(ULONG i=0;iGetFaceuses(a,b); faceuses.push_back({a,b}); } + SmTArray ss; body->GetShells(ss); for(ULONG i=0;i rs; body->GetRegions(rs); for(ULONG i=0;i& out) const { + out.insert(out.end(),{(double)shells.size(),(double)regions.size()}); + for(auto edge: edges) { + out.push_back(edge->GetCurve()?(double)edge->GetCurve()->GetType():0.0); + SmTArray uses; edge->GetEdgeuses(uses); out.push_back((double)uses.GetSize()); + for(ULONG i=0;iGetVertexuse(); + out.insert(out.end(),{scale_label(edgeuses,use),(double)use->GetOrientation(),scale_label(vertices,vu?vu->GetVertex():NULL),scale_label(edgeuses,use->GetMate()),scale_label(edgeuses,use->GetRadial()),scale_label(faces,use->GetFace())}); + } + } + for(size_t i=0;iGetSurface()?(double)faces[i]->GetSurface()->GetType():0.0); + for(auto use: {faceuses[i].first,faceuses[i].second}) { + if(use) { auto region=use->GetRegion(); out.insert(out.end(),{1.0,(double)use->GetOrientation(),scale_label(shells,use->GetShell()),scale_label(regions,region),region?(double)region->IsVoid():-1.0}); } + else out.insert(out.end(),{0.0,0.0,0.0,0.0,-1.0}); + } + } + } +}; + +int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) { const SmContext& ctx = the_context(); int64_t kind = rd.idx(); + if(probe && kind!=T_PRIMITIVE_BOX && kind!=T_PRIMITIVE_SPHERE && kind!=T_COPIED_BOX && kind!=T_COPIED_SPHERE) return SC_ERR_ARG; SmObject* obj = NULL; SmBrep* source = NULL; std::vector source_before; @@ -4958,6 +5003,8 @@ int32_t run_transform(Rd& rd, std::vector& o) } default: return SC_ERR_ARG; } + std::optional frame; + if(probe) frame.emplace((SmBrep*)obj); int64_t op = rd.idx(); SmApiStatus s = 0; switch (op) @@ -5020,6 +5067,7 @@ int32_t run_transform(Rd& rd, std::vector& o) primitive_record::append(o,body); for (ULONG i=0;iGetRectangularTrim()); } + if(frame) frame->append(o); if (source) { o.insert(o.end(), source_before.begin(), source_before.end()); primitive_record::append(o, source); @@ -5059,6 +5107,7 @@ int32_t run(Rd& rd, std::vector& o) case 0: s = run_list(rd, o); break; case 1: s = run_proto(rd, o); break; case 2: s = run_transform(rd, o); break; + case 4: s = run_transform(rd, o, true); break; default: return SC_ERR_ARG; } if (s != SC_OK) From ccd4b67eff777c47fccd247c9b1a60bbf34e634f Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 12:58:58 +0200 Subject: [PATCH 018/491] fix: convert real solid geometry before public scales Co-Authored-By: OpenAI Codex --- .../src/units/topology_proto_topology_p1.rs | 11 +- crates/solidcore/src/topology/brep/p3.rs | 42 ++++-- crates/solidcore/src/topology/brep/p5.rs | 29 ++++- .../solidcore/tests/public_scale_ownership.rs | 121 ++++++++++++++++++ .../cpp/shim/topology_proto_topology_p1.cpp | 6 +- plan/units/topology.brep_p3.toml | 2 +- plan/units/topology.brep_p5.toml | 2 +- plan/units/topology.proto_topology_p1.toml | 2 +- 8 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 crates/solidcore/tests/public_scale_ownership.rs diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index 485530a8b5..ed7a9673c2 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -4415,6 +4415,7 @@ fn traversal_label(items: &[T], item: Option) -> f64 { } struct ScaleProbeFrame { + initial_types: Vec, vertices: Vec, edges: Vec, faces: Vec, @@ -4432,10 +4433,13 @@ impl ScaleProbeFrame { let faces = bp::faces(body, None)?; let shells = bp::shells(k, body, None)?; let regions = bp::regions(body, None)?; + let mut initial_types = Vec::new(); + for edge in &edges { initial_types.push(body.edge(*edge)?.curve.as_ref().map_or(0, Curve::type_code) as f64); } + for face in &faces { initial_types.push(body.face(*face)?.surface.as_ref().map_or(0, Surface::type_code) as f64); } let mut edgeuses = Vec::new(); for edge in &edges { edgeuses.extend(solidcore::topology::edge::edgeuses(body, *edge, None, None)?); } let faceuses = faces.iter().map(|face| solidcore::topology::face::p2::faceuses(body, *face)).collect::>>()?; - Ok(Self { vertices, edges, faces, shells, regions, edgeuses, faceuses }) + Ok(Self { initial_types, vertices, edges, faces, shells, regions, edgeuses, faceuses }) } } @@ -4450,7 +4454,7 @@ fn append_scale_probe(body: &Brep, frame: &ScaleProbeFrame, out: &mut Vec) for id in uses { let use_ = body.edgeuse(id)?; let vertex = eu::vertexuse(body, id)?.map(|v| body.vertexuse(v)).transpose()?.and_then(|v| v.vertex); - out.extend([traversal_label(&frame.edgeuses, Some(id)), use_.orientation as u8 as f64, + out.extend([traversal_label(&frame.edgeuses, Some(id)), f64::from(use_.orientation as u8), traversal_label(&frame.vertices, vertex), traversal_label(&frame.edgeuses, eu::mate(body, id)?), traversal_label(&frame.edgeuses, eu::radial(body, id)?), traversal_label(&frame.faces, eu::face(body, id)?)]); } @@ -4462,7 +4466,7 @@ fn append_scale_probe(body: &Brep, frame: &ScaleProbeFrame, out: &mut Vec) if let Some(id) = id { let use_ = body.faceuse(id)?; let region = eu::faceuse_region(body, id)?; - out.extend([1.0, use_.orientation as u8 as f64, traversal_label(&frame.shells, use_.shell), traversal_label(&frame.regions, region), + out.extend([1.0, f64::from(use_.orientation as u8), traversal_label(&frame.shells, use_.shell), traversal_label(&frame.regions, region), region.map(|r| body.region(r)).transpose()?.map_or(-1.0, |r| f64::from(r.is_void))]); } else { out.extend([0.0, 0.0, 0.0, 0.0, -1.0]); } } @@ -4653,6 +4657,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail }, }; let mut o = vec![s]; + if let Some(frame) = &probe_frame { o.push(frame.initial_types.len() as f64); o.extend_from_slice(&frame.initial_types); } if matches!(obj, TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. }) { let vertices = solidcore::topology::brep::p1::vertices(&brep, None).map_err(|e| harness(e.to_string()))?; let edges = solidcore::topology::brep::p1::edges(&brep, None).map_err(|e| harness(e.to_string()))?; diff --git a/crates/solidcore/src/topology/brep/p3.rs b/crates/solidcore/src/topology/brep/p3.rs index 240c82cd60..c051de5a76 100644 --- a/crates/solidcore/src/topology/brep/p3.rs +++ b/crates/solidcore/src/topology/brep/p3.rs @@ -2908,6 +2908,18 @@ fn with_nurb<'a>(k: &Kernel, s: &'a Surface) -> Result { + if x.base().base().gw_nurb().is_some() { + Some(Cow::Borrowed(x.base().base())) + } else { + let mut tmp = x.as_ref().clone(); + match tmp.make_nurb(k) { + Err(e) if e.is_unported() => return Err(e), + Ok(()) | Err(_) => {} + } + Some(Cow::Owned(tmp.base().base().clone())) + } + } _ => None, }) } @@ -2917,12 +2929,13 @@ const fn as_bspline_surface(s: &Surface) -> Option<&BSplineSurface> { SurfaceKind::BSpline(x) => Some(x), SurfaceKind::Plane(x) => Some(x.base()), SurfaceKind::SurfOfRevolution(x) => Some(x.base()), + SurfaceKind::Sphere(x) => Some(x.base().base()), _ => None, } } /// `TurnToNURBS`: makes every face surface and edge curve of the B-rep a plain -/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`) gets its B-spline +/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`, `SmSphere`) gets its B-spline /// copy (`ReplaceSurface`, which rebuilds the UV trim curves); an `SmOffsetSurface` of a /// B-spline kind is approximated (`ApproximateOffsetSurface` at ten times the B-rep /// tolerance, chord length; a failed approximation skips the face), extended by 2% of @@ -2940,9 +2953,14 @@ const fn as_bspline_surface(s: &Surface) -> Option<&BSplineSurface> { /// identity domain. pub fn turn_to_nurbs(k: &mut Kernel, b: &mut Brep, env: &mut NotifyEnv<'_>) -> Result<()> { with_caller_identities(k, b, env, None, |k, b, env| { - turn_to_nurbs_in(k, b, env, &|f| arena_identity(CacheObject::Face(f)), &|e| { - arena_identity(CacheObject::Edge(e)) - }) + turn_to_nurbs_in( + k, + b, + env, + &|f| arena_identity(CacheObject::Face(f)), + &|e| arena_identity(CacheObject::Edge(e)), + &|e| arena_identity(CacheObject::Edge(e)), + ) }) } @@ -2953,12 +2971,18 @@ pub(crate) fn turn_to_nurbs_with_geometry_owners( k: &mut Kernel, b: &mut Brep, env: &mut NotifyEnv<'_>, + geometry_owner: &dyn Fn(EntityRef) -> ObjectRef, ) -> Result<()> { let key_of = env.key_of; with_caller_identities(k, b, env, None, |k, b, env| { - turn_to_nurbs_in(k, b, env, &|f| key_of(EntityRef::Face(f)), &|e| { - key_of(EntityRef::Edge(e)) - }) + turn_to_nurbs_in( + k, + b, + env, + &|f| geometry_owner(EntityRef::Face(f)), + &|e| geometry_owner(EntityRef::Edge(e)), + &|e| key_of(EntityRef::Edge(e)), + ) }) } @@ -2969,6 +2993,7 @@ fn turn_to_nurbs_in( env: &mut NotifyEnv<'_>, face_owner: &dyn Fn(FaceId) -> ObjectRef, edge_owner: &dyn Fn(EdgeId) -> ObjectRef, + new_edge_owner: &dyn Fn(EdgeId) -> ObjectRef, ) -> Result<()> { ignore_soft(brep_notify( b, @@ -3142,10 +3167,11 @@ fn turn_to_nurbs_in( env, [Some(r), None], )?; - let edge_ref = arena_identity(CacheObject::Edge(e)); + let edge_ref = new_edge_owner(e); if let Some(c) = b.edge_mut(e)?.curve.as_mut() { let _ = c.set_owner(Some(edge_ref), None); } + b.geometry_owner_keys.insert(EntityRef::Edge(e), edge_ref); } Ok(()) } diff --git a/crates/solidcore/src/topology/brep/p5.rs b/crates/solidcore/src/topology/brep/p5.rs index bd992a4c51..fb3ed8a97b 100644 --- a/crates/solidcore/src/topology/brep/p5.rs +++ b/crates/solidcore/src/topology/brep/p5.rs @@ -2392,7 +2392,7 @@ fn transform_in( || !crate::geom::tol::are_same(s.x, s.z) || s.x < crate::math::EFF_ZERO }) { - crate::error::ignore_status(turn_to_nurbs(k, b, notifications.as_deref_mut()))?; + crate::error::ignore_status(turn_to_nurbs(k, b, notifications.as_deref_mut(), owners))?; } let mut bbox = Extent3d::new(); let vertices = brep_vertices(b)?; @@ -2520,13 +2520,28 @@ fn turn_to_nurbs( k: &mut Kernel, b: &mut Brep, notifications: Option<&mut EditNotifyEnv<'_>>, + owners: Option<&std::collections::BTreeMap>, ) -> Result<()> { - with_p3_notifications( - k, - b, - notifications, - crate::topology::brep::p3::turn_to_nurbs_with_geometry_owners, - ) + with_p3_notifications(k, b, notifications, |k, b, env| { + let key_of = env.key_of; + let mut selection = std::collections::BTreeMap::new(); + for entity in brep_faces(b)? + .into_iter() + .map(crate::EntityRef::Face) + .chain(brep_edges(b)?.into_iter().map(crate::EntityRef::Edge)) + { + selection.insert( + entity, + transform_owner_key(b, owners, entity, key_of(entity)), + ); + } + crate::topology::brep::p3::turn_to_nurbs_with_geometry_owners(k, b, env, &|entity| { + selection + .get(&entity) + .copied() + .unwrap_or_else(|| key_of(entity)) + }) + }) } fn notify_edge_pre_edit( k: &mut Kernel, diff --git a/crates/solidcore/tests/public_scale_ownership.rs b/crates/solidcore/tests/public_scale_ownership.rs new file mode 100644 index 0000000000..327acaba33 --- /dev/null +++ b/crates/solidcore/tests/public_scale_ownership.rs @@ -0,0 +1,121 @@ +//! Representation-changing public scales retain real owner filtering. +use solidcore::math::{Point3, Vec3}; +use solidcore::primitive::api_primitives::{create_box, create_sphere}; +use solidcore::topology::brep::{p1, p3}; +use solidcore::topology::proto_topology::p1::api_transform::{ + api_scale, api_translate, TransformObject, +}; +use solidcore::{Brep, EntityRef, Kernel}; + +fn samples(k: &Kernel, b: &Brep) -> solidcore::Result<(Point3, Point3)> { + let missing = || solidcore::SolidError::invalid_input("scale_test", "missing geometry"); + let edges = p1::edges(b, None)?; + let faces = p1::faces(b, None)?; + let edge = b.edge(*edges.first().ok_or_else(missing)?)?; + let face = b.face(*faces.first().ok_or_else(missing)?)?; + Ok(( + edge.curve + .as_ref() + .ok_or_else(missing)? + .evaluate_point(k, edge.interval.mid())?, + face.surface + .as_ref() + .ok_or_else(missing)? + .evaluate_point(k, face.uv_domain.evaluate(0.37, 0.61))?, + )) +} + +fn close(a: Vec3, b: Vec3) -> bool { + [(a.x, b.x), (a.y, b.y), (a.z, b.z)] + .into_iter() + .all(|(a, b)| (a - b).abs() <= 1e-9 * a.abs().max(b.abs())) +} + +#[test] +fn converted_geometry_moves_again_without_rebinding() { + let mut k = Kernel::default(); + let box_ = create_box(&mut k, Point3::new(2.0, 3.0, 5.0), 2.0, 3.0, 4.0).unwrap(); + let sphere = create_sphere(&mut k, Point3::new(2.0, 3.0, 5.0), 2.0).unwrap(); + let scale = Vec3::new(2.0, 3.0, 0.5); + let delta = Vec3::new(7.0, -2.0, 3.0); + for mut body in [box_, sphere] { + let before = samples(&k, &body).unwrap(); + api_scale(&mut k, Some(TransformObject::Brep(&mut body)), scale).unwrap(); + let after = samples(&k, &body).unwrap(); + for (a, b) in [(after.0, before.0), (after.1, before.1)] { + assert!(close( + a, + Vec3::new(b.x * scale.x, b.y * scale.y, b.z * scale.z) + )); + } + let edge = p1::edges(&body, None).unwrap()[0]; + let face = p1::faces(&body, None).unwrap()[0]; + let owners = ( + body.edge(edge).unwrap().curve.as_ref().unwrap().owner(), + body.face(face).unwrap().surface.as_ref().unwrap().owner(), + ); + assert!(owners.0.is_some() && owners.1.is_some()); + api_translate(&mut k, Some(TransformObject::Brep(&mut body)), delta).unwrap(); + let moved = samples(&k, &body).unwrap(); + assert!(close(moved.0, after.0 + delta) && close(moved.1, after.1 + delta)); + assert_eq!( + ( + body.edge(edge).unwrap().curve.as_ref().unwrap().owner(), + body.face(face).unwrap().surface.as_ref().unwrap().owner() + ), + owners + ); + } +} + +#[test] +fn conversion_filters_foreign_and_absent_owners() { + for foreign in [false, true] { + let mut k = Kernel::default(); + let mut body = create_box(&mut k, Point3::new(2.0, 3.0, 5.0), 2.0, 3.0, 4.0).unwrap(); + let mut other = create_box(&mut k, Point3::new(20.0, 30.0, 50.0), 2.0, 3.0, 4.0).unwrap(); + let serial = p3::notify_serial(&mut k.context, &mut other).unwrap(); + let edge = p1::edges(&body, None).unwrap()[0]; + let face = p1::faces(&body, None).unwrap()[0]; + let edge_owner = foreign.then(|| { + p3::topology_ref(serial, EntityRef::Edge(p1::edges(&other, None).unwrap()[0])) + }); + let face_owner = foreign.then(|| { + p3::topology_ref(serial, EntityRef::Face(p1::faces(&other, None).unwrap()[0])) + }); + body.edge_mut(edge) + .unwrap() + .curve + .as_mut() + .unwrap() + .set_owner(edge_owner, None); + body.face_mut(face) + .unwrap() + .surface + .as_mut() + .unwrap() + .set_owner(face_owner); + let before = samples(&k, &body).unwrap(); + let vertex = p1::vertices(&body, None).unwrap()[0]; + let point = body.vertex(vertex).unwrap().point; + api_scale( + &mut k, + Some(TransformObject::Brep(&mut body)), + Vec3::new(2.0, 3.0, 0.5), + ) + .unwrap(); + assert_eq!(samples(&k, &body).unwrap(), before); + assert_eq!( + body.vertex(vertex).unwrap().point, + Point3::new(point.x * 2.0, point.y * 3.0, point.z * 0.5) + ); + assert_eq!( + body.edge(edge).unwrap().curve.as_ref().unwrap().owner(), + edge_owner + ); + assert_eq!( + body.face(face).unwrap().surface.as_ref().unwrap().owner(), + face_owner + ); + } +} diff --git a/harness/cpp/shim/topology_proto_topology_p1.cpp b/harness/cpp/shim/topology_proto_topology_p1.cpp index 785148adaa..7691d9e40e 100644 --- a/harness/cpp/shim/topology_proto_topology_p1.cpp +++ b/harness/cpp/shim/topology_proto_topology_p1.cpp @@ -4864,6 +4864,7 @@ template double scale_label(const std::vector& items, T* item) { return it==items.end() ? 0.0 : (double)(std::distance(items.begin(),it)+1); } struct ScaleProbeFrame { + std::vector initial_types; std::vector vertices; std::vector edges; std::vector faces; @@ -4873,8 +4874,8 @@ struct ScaleProbeFrame { std::vector> faceuses; explicit ScaleProbeFrame(SmBrep* body) { SmTArray vs; body->GetVertices(vs); for(ULONG i=0;i es; body->GetEdges(es); for(ULONG i=0;i us; es[i]->GetEdgeuses(us); for(ULONG j=0;j fs; body->GetFaces(fs); for(ULONG i=0;iGetFaceuses(a,b); faceuses.push_back({a,b}); } + SmTArray es; body->GetEdges(es); for(ULONG i=0;iGetCurve()?(double)es[i]->GetCurve()->GetType():0.0); SmTArray us; es[i]->GetEdgeuses(us); for(ULONG j=0;j fs; body->GetFaces(fs); for(ULONG i=0;iGetSurface()?(double)fs[i]->GetSurface()->GetType():0.0); SmFaceuse *a=NULL,*b=NULL; fs[i]->GetFaceuses(a,b); faceuses.push_back({a,b}); } SmTArray ss; body->GetShells(ss); for(ULONG i=0;i rs; body->GetRegions(rs); for(ULONG i=0;i& o, bool probe=false) default: return SC_ERR_ARG; } o.push_back((double)s); + if(frame) { o.push_back((double)frame->initial_types.size()); o.insert(o.end(),frame->initial_types.begin(),frame->initial_types.end()); } if (kind == T_PRIMITIVE_BOX || kind == T_PRIMITIVE_SPHERE || kind == T_CALLER_FACE || kind == T_COPIED_BOX || kind == T_COPIED_SPHERE) { SmBrep* body=(SmBrep*)obj; SmTArray vertices; body->GetVertices(vertices); diff --git a/plan/units/topology.brep_p3.toml b/plan/units/topology.brep_p3.toml index 0584a887d0..02ce89ce0b 100644 --- a/plan/units/topology.brep_p3.toml +++ b/plan/units/topology.brep_p3.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred." +notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred. CO-PORTED in public solid scale phase two (2026-10-24): TurnToNURBS accepts producer-authorized geometry-selection keys separately from canonical notification keys and binds converted curves to the actual destination edge. Legacy arena entrypoint retains its convention. The BSpline-surface cast/materialization helpers now include Sphere through its native SurfOfRevolution inheritance, so surface conversion precedes the curve pass. Ten actual public scale cases pass exactly; no numeric/tolerance changes." diff --git a/plan/units/topology.brep_p5.toml b/plan/units/topology.brep_p5.toml index 757b5a52ba..e799647d36 100644 --- a/plan/units/topology.brep_p5.toml +++ b/plan/units/topology.brep_p5.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "UPDATE (PR #678, 2026-10-23): UpdateVertexEdgeIntervals wired to topology.brep_p1 and SqueezeFace wired to heal.stitch_p1 / heal.stitch_p2 Stitch manager; includes boolean.topology_intersector_p3 post-merge Walk harness update (PR #679). Difftest 797 cases: 795 pass, 0 fail, 2 deferred (blocked by topology.brep_p4 destructor/owning record erasure), max_rel_err=0e0. G10 (PR #637, 2026-10-23): face ChangeGeometry notification wired. difftest 797 cases, 787 pass, 0 failed, 10 deferred (topology.brep_p1 8, topology.brep_p4 2); the 2 former topology.face_p3 deferrals pass. PARTIAL (PR #194, functional source ecccb67b; official main 9a7c1ec0; Claude review fixes): all twelve owned editing control flows and pending-topology cleanup present; individual missing callees/types defer. Landed shell/region callees wired; stable shell, vertex and vertex-use record erasure remains the part 4 SmBrep destructor/storage integration gap. Native difftest: 797 cases (285 fixtures + 512 generated), 737 passed, 0 failed, 60 deferred (topology.brep_p1 10, topology.brep_p2 46, topology.brep_p4 2, topology.brep_p9 2), max_rel_err=0, no absolute floors. Meaningful successful topology-mutation paths certified: 0/12 operations (issue #210). Coverage is 409 point/tolerance-target-stage cases, 301 whole-operation refusals/empty/identity/alias no-ops, and 27 supporting notification scripts. Temporary-edge minimum-length threshold, successful topology mutation, nonidentity transforms, shared native geometry across distinct owning records and pending-reader integration remain uncertified. Sensitivity 737/737 detected; 4096 generated: 4375 cases, 4195 passed, 180 deferred, 0 failed. Formatting, both strict Clippy configurations, 1369 kernel tests, WASM, 3 native FFI tests and all twelve affected differential units pass; full green/partial sweep PASS: 205 units, 113 green, 92 partial, 0 failed. No open dependency PR bundled or other unit promoted. 14 fns; largest: SmBrep::RemoveTopologicalEdgesAndVertices, SmBrep::CombineCoincidentVertices, SmBrep::DeleteTopologicalVertex, SmBrep::GlueVertices; cycle back-edges (call into later units; stub or co-port): topology.brep_p3(8), topology.brep_p2(5), heal.stitch_p1(3), topology.brep_p9(3), heal.stitch_p2(2); unit-SCC #22 (220 units); hazards: ptr-iter; 200 upstream test entry points reach this unit Second review round (head e9df9346, merged official main with topology.brep_p1/p7 and heal.stitch_p1): isolated aliased CombineCoincidentVertices takes the native path (6 new fixtures, deferred at Notify; only the target stage refuses it, UB-191); Notify status ignored in combine as upstream; SqueezeFace unlock never hides its result; FFI guard refusals are rejected, not failed. Wired heal.stitch_p1 (GlueVertices, RemoveFaces, StitchAndOrient) and topology.brep_p7 (GlueVertices) into this unit. Calls into the now-partial topology.brep_p1 are still UNPORTED stubs pending a follow-up PR. Batch integration (batch/clean-2026-10-19): set_face_tolerance wired to topology::face::p3::set_tolerance (topology.face_p3). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 797 cases, 738 pass, 0 fail, 59 deferred (topology.brep_p1 38, topology.brep_p4 12, topology.brep_p3 7, topology.brep_p9 2); the 46 SmBrep::Notify deferrals: 1 pass, the rest relabel to brep_p1 GetVertices / UpdateVertexEdgeIntervals, brep_p4 DeleteVertex and brep_p3 TurnToNURBS. Re-run after wiring stale stubs (PR #285): 797 cases, 744 pass, 0 fail, 53 deferred, max_rel_err 0. UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) stubs SqueezeEdge, MakeWireEdgeVertex, DeleteFace, OrientTrimmedSurfaces and TurnToNURBS, and UNPORTED(topology.brep_p9) MakeShellVertex in create_wire_edge_from_curve, are wired to topology::brep::p3 / p9 through with_p3_notifications (the caller's environment, else a topology.brep_p2 ArenaNotifyState over the kernel context's callbacks lent with brep::p3::with_lent_context); make_manifold and stitch_and_orient take k: &mut Kernel. Case by case: no outcome change; the 7 TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices (797 cases, 738 pass, 0 fail, 59 deferred: topology.brep_p1 45, topology.brep_p4 12, topology.brep_p9 2). UPDATE (PR #204, Codex review of 255d52b7): a plain entry point (no environment; the _with_notifications forms given None) runs its whole edit in one environment under the kernel's identities (plain_edit = topology::brep::p3::with_kernel_callbacks: the B-rep's serial in the kernel context's domain, so two B-reps' objects never share a key in one context; the callbacks lent to the edit; nested in another lent edit it is refused before anything changes). create_wire_edge_from_curve, make_manifold, remove_faces, stitch_and_orient and squeeze_face are plain_edit wrappers over *_in bodies; with_p3_notifications no longer falls back to ArenaNotifyState. UPDATE (PR #204, Codex re-check of 6b04141d): plain_edit's environment carries a default record for every existing object from the start (brep::p3::kernel_named_edit), so the notifications sent before any p3 edit (e.g. notify_edge_merge) are delivered. UPDATE (PR #204, merge of main #339): op 13 (CreateWireEdgeFromCurve) runs now that GetInfiniteRegion is wired; its 2 fixture cases (isolated_op13_editing_*) compared the vertex-shell vertex use MakeEdgeVertexTopology deletes upstream and the arena keeps unlinked; with graph() comparing reachable records only (boolean.merge_p1) they pass. API EXTENDED in sweep.topology_sweep (PR #222): make_edge_in_face_with_uv_curve preserves polymorphic UV curve kinds, including native SmBSplineCurve-derived lines; the existing B-spline-only Rust entry point delegates without changing behavior. UPDATE (PR #526): FindAndSetInfiniteRegion wired to topology.brep_p9 FindInfiniteRegion (through the caller's query::brep::BrepQuery) and topology.brep_p1 ResetInfiniteRegion; it takes the kernel and SetRegionVoidType. Re-run: 797 cases, 746 pass, 0 fail, 51 deferred (the 2 FindInfiniteRegion deferrals pass). UPDATE (wire/brep-p5-find-vertex-sector): MakeEdgeInFace wired to topology.face_p4 FindVertexSector and topology.brep_p9 MakeEdgeInFaceWithVU; make_edge_in_face(_with_uv_curve) take the kernel and an optional notification environment (None: plain edit). 797 cases, 746 pass, 51 deferred, 0 failed (unchanged). SweepStore transforms use owner-aware brep_p1 collections and brep_p3 NURBS conversion. Fixture import binds ownership metadata to its kernel without changing geometry values; unavailable face-p3 UV-trim construction remains deferred. UPDATE (PR #441): StitchAndOrient runs its two StitchFaces passes outside the edit lease (the stitch's glue/squeeze helpers notify through their own kernel-context edits, which the nested-edit guard refused, and the swallowed error left swept B-reps unstitched); the orientation pass keeps the caller's environment. The shell-vertex glue path still stops at remove_owned_vertex (plan/findings.md). UPDATE (oursland/solidcore#1): remove_owned_vertex is Brep::remove_from_vertex_list; the 36 heal.stitch_p2 differences it exposed were topology.brep_p7 GlueEdgesGeneral leaving the glued edge on the edge list (fixed); difftest 797 cases, 785 pass, 0 failed, 12 deferred. UPDATE (wire: SmBrep::Transform into blend.hcr and the boolean.merge_p1 runner, PR #679): CombineCoincidentVertices calls the ported topology.brep_p1 UpdateVertexEdgeIntervals under the caller's notification environment (the stub is gone). difftest 797 cases, 795 pass, 0 failed, 2 deferred (topology.brep_p4 2); the 8 UpdateVertexEdgeIntervals deferrals pass." +notes = "UPDATE (PR #678, 2026-10-23): UpdateVertexEdgeIntervals wired to topology.brep_p1 and SqueezeFace wired to heal.stitch_p1 / heal.stitch_p2 Stitch manager; includes boolean.topology_intersector_p3 post-merge Walk harness update (PR #679). Difftest 797 cases: 795 pass, 0 fail, 2 deferred (blocked by topology.brep_p4 destructor/owning record erasure), max_rel_err=0e0. G10 (PR #637, 2026-10-23): face ChangeGeometry notification wired. difftest 797 cases, 787 pass, 0 failed, 10 deferred (topology.brep_p1 8, topology.brep_p4 2); the 2 former topology.face_p3 deferrals pass. PARTIAL (PR #194, functional source ecccb67b; official main 9a7c1ec0; Claude review fixes): all twelve owned editing control flows and pending-topology cleanup present; individual missing callees/types defer. Landed shell/region callees wired; stable shell, vertex and vertex-use record erasure remains the part 4 SmBrep destructor/storage integration gap. Native difftest: 797 cases (285 fixtures + 512 generated), 737 passed, 0 failed, 60 deferred (topology.brep_p1 10, topology.brep_p2 46, topology.brep_p4 2, topology.brep_p9 2), max_rel_err=0, no absolute floors. Meaningful successful topology-mutation paths certified: 0/12 operations (issue #210). Coverage is 409 point/tolerance-target-stage cases, 301 whole-operation refusals/empty/identity/alias no-ops, and 27 supporting notification scripts. Temporary-edge minimum-length threshold, successful topology mutation, nonidentity transforms, shared native geometry across distinct owning records and pending-reader integration remain uncertified. Sensitivity 737/737 detected; 4096 generated: 4375 cases, 4195 passed, 180 deferred, 0 failed. Formatting, both strict Clippy configurations, 1369 kernel tests, WASM, 3 native FFI tests and all twelve affected differential units pass; full green/partial sweep PASS: 205 units, 113 green, 92 partial, 0 failed. No open dependency PR bundled or other unit promoted. 14 fns; largest: SmBrep::RemoveTopologicalEdgesAndVertices, SmBrep::CombineCoincidentVertices, SmBrep::DeleteTopologicalVertex, SmBrep::GlueVertices; cycle back-edges (call into later units; stub or co-port): topology.brep_p3(8), topology.brep_p2(5), heal.stitch_p1(3), topology.brep_p9(3), heal.stitch_p2(2); unit-SCC #22 (220 units); hazards: ptr-iter; 200 upstream test entry points reach this unit Second review round (head e9df9346, merged official main with topology.brep_p1/p7 and heal.stitch_p1): isolated aliased CombineCoincidentVertices takes the native path (6 new fixtures, deferred at Notify; only the target stage refuses it, UB-191); Notify status ignored in combine as upstream; SqueezeFace unlock never hides its result; FFI guard refusals are rejected, not failed. Wired heal.stitch_p1 (GlueVertices, RemoveFaces, StitchAndOrient) and topology.brep_p7 (GlueVertices) into this unit. Calls into the now-partial topology.brep_p1 are still UNPORTED stubs pending a follow-up PR. Batch integration (batch/clean-2026-10-19): set_face_tolerance wired to topology::face::p3::set_tolerance (topology.face_p3). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 797 cases, 738 pass, 0 fail, 59 deferred (topology.brep_p1 38, topology.brep_p4 12, topology.brep_p3 7, topology.brep_p9 2); the 46 SmBrep::Notify deferrals: 1 pass, the rest relabel to brep_p1 GetVertices / UpdateVertexEdgeIntervals, brep_p4 DeleteVertex and brep_p3 TurnToNURBS. Re-run after wiring stale stubs (PR #285): 797 cases, 744 pass, 0 fail, 53 deferred, max_rel_err 0. UPDATE (PR #204, topology.brep_p3): the UNPORTED(topology.brep_p3) stubs SqueezeEdge, MakeWireEdgeVertex, DeleteFace, OrientTrimmedSurfaces and TurnToNURBS, and UNPORTED(topology.brep_p9) MakeShellVertex in create_wire_edge_from_curve, are wired to topology::brep::p3 / p9 through with_p3_notifications (the caller's environment, else a topology.brep_p2 ArenaNotifyState over the kernel context's callbacks lent with brep::p3::with_lent_context); make_manifold and stitch_and_orient take k: &mut Kernel. Case by case: no outcome change; the 7 TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices (797 cases, 738 pass, 0 fail, 59 deferred: topology.brep_p1 45, topology.brep_p4 12, topology.brep_p9 2). UPDATE (PR #204, Codex review of 255d52b7): a plain entry point (no environment; the _with_notifications forms given None) runs its whole edit in one environment under the kernel's identities (plain_edit = topology::brep::p3::with_kernel_callbacks: the B-rep's serial in the kernel context's domain, so two B-reps' objects never share a key in one context; the callbacks lent to the edit; nested in another lent edit it is refused before anything changes). create_wire_edge_from_curve, make_manifold, remove_faces, stitch_and_orient and squeeze_face are plain_edit wrappers over *_in bodies; with_p3_notifications no longer falls back to ArenaNotifyState. UPDATE (PR #204, Codex re-check of 6b04141d): plain_edit's environment carries a default record for every existing object from the start (brep::p3::kernel_named_edit), so the notifications sent before any p3 edit (e.g. notify_edge_merge) are delivered. UPDATE (PR #204, merge of main #339): op 13 (CreateWireEdgeFromCurve) runs now that GetInfiniteRegion is wired; its 2 fixture cases (isolated_op13_editing_*) compared the vertex-shell vertex use MakeEdgeVertexTopology deletes upstream and the arena keeps unlinked; with graph() comparing reachable records only (boolean.merge_p1) they pass. API EXTENDED in sweep.topology_sweep (PR #222): make_edge_in_face_with_uv_curve preserves polymorphic UV curve kinds, including native SmBSplineCurve-derived lines; the existing B-spline-only Rust entry point delegates without changing behavior. UPDATE (PR #526): FindAndSetInfiniteRegion wired to topology.brep_p9 FindInfiniteRegion (through the caller's query::brep::BrepQuery) and topology.brep_p1 ResetInfiniteRegion; it takes the kernel and SetRegionVoidType. Re-run: 797 cases, 746 pass, 0 fail, 51 deferred (the 2 FindInfiniteRegion deferrals pass). UPDATE (wire/brep-p5-find-vertex-sector): MakeEdgeInFace wired to topology.face_p4 FindVertexSector and topology.brep_p9 MakeEdgeInFaceWithVU; make_edge_in_face(_with_uv_curve) take the kernel and an optional notification environment (None: plain edit). 797 cases, 746 pass, 51 deferred, 0 failed (unchanged). SweepStore transforms use owner-aware brep_p1 collections and brep_p3 NURBS conversion. Fixture import binds ownership metadata to its kernel without changing geometry values; unavailable face-p3 UV-trim construction remains deferred. UPDATE (PR #441): StitchAndOrient runs its two StitchFaces passes outside the edit lease (the stitch's glue/squeeze helpers notify through their own kernel-context edits, which the nested-edit guard refused, and the swallowed error left swept B-reps unstitched); the orientation pass keeps the caller's environment. The shell-vertex glue path still stops at remove_owned_vertex (plan/findings.md). UPDATE (oursland/solidcore#1): remove_owned_vertex is Brep::remove_from_vertex_list; the 36 heal.stitch_p2 differences it exposed were topology.brep_p7 GlueEdgesGeneral leaving the glued edge on the edge list (fixed); difftest 797 cases, 785 pass, 0 failed, 12 deferred. UPDATE (wire: SmBrep::Transform into blend.hcr and the boolean.merge_p1 runner, PR #679): CombineCoincidentVertices calls the ported topology.brep_p1 UpdateVertexEdgeIntervals under the caller's notification environment (the stub is gone). difftest 797 cases, 795 pass, 0 failed, 2 deferred (topology.brep_p4 2); the 8 UpdateVertexEdgeIntervals deferrals pass. Public solid scale phase two (2026-10-24): conversion receives the same proven producer/canonical geometry selection as the later transform, without owner rewriting. Native conversion now precedes both geometry passes for actual constructed/copied box/sphere scales. Ten real nonuniform/signed/reflected scale cases pass exactly; both reflection corrections independently detected by controlled omissions (3 failures each). Two Rust controls retain foreign/None owner filtering and verify a later transform moves converted geometry without rebinding." diff --git a/plan/units/topology.proto_topology_p1.toml b/plan/units/topology.proto_topology_p1.toml index ae3e070516..d8d2a8e0f5 100644 --- a/plan/units/topology.proto_topology_p1.toml +++ b/plan/units/topology.proto_topology_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "green" claimed_by = "" claimed_at = "" -notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures." +notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures. Public solid scale phase two (2026-10-24): full runner 1891 passes, 0 failures/deferred. Ten real constructed/copied box/sphere cases cover positive nonuniform scaling, odd/even/all-negative reflection, ScaleByPt and unchanged zero-axis rejection. Native baseline had 9 failures; all10 now match exactly. Additive kind4 records actual pre/post geometry types and retained pre-operation topology identities for directed edgeuse order/mates/radials/vertices/faces and fixed faceuse shell/region membership. Controlled omission of edgeuse reversal and shell swapping each kills the 3 odd-reflection cases. Source-copy preservation covers sampled geometry/tolerances only; no warmed-cache claim." From 67ade46969cda56ce9756ea30bbf41a3ffbcdafb Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 13:40:52 +0200 Subject: [PATCH 019/491] test: probe public cone cylinder and torus scale conversion Co-Authored-By: OpenAI Codex --- .../src/units/topology_proto_topology_p1.rs | 13 +- .../src/units/topology_proto_topology_p1.rs | 9 +- .../public_scale_cone_torus.json | 262 ++++++++++++++++++ .../cpp/shim/topology_proto_topology_p1.cpp | 16 +- 4 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 fixtures/topology.proto_topology_p1/public_scale_cone_torus.json diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index ed7a9673c2..0785d5433e 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -244,6 +244,9 @@ pub(super) enum TObjIn { Brep, PrimitiveBox { origin: [f64; 3], size: [f64; 3] }, PrimitiveSphere { origin: [f64; 3], radius: f64 }, + PrimitiveCylinder { origin: [f64; 3], radius: f64, height: f64 }, + PrimitiveCone { origin: [f64; 3], radii: [f64; 2], height: f64 }, + PrimitiveTorus { origin: [f64; 3], radii: [f64; 2] }, CallerFace { origin: [f64; 3], size: [f64; 2] }, CopiedBox { origin: [f64; 3], size: [f64; 3] }, CopiedSphere { origin: [f64; 3], radius: f64 }, @@ -462,6 +465,9 @@ pub(super) fn cpp_input(s: &Script) -> Vec { v.extend_from_slice(origin); v.push(*radius); } + TObjIn::PrimitiveCylinder { origin, radius, height } => { v.push(tobj::PRIMITIVE_CYLINDER as f64); v.extend_from_slice(origin); v.extend([*radius, *height]); } + TObjIn::PrimitiveCone { origin, radii, height } => { v.push(tobj::PRIMITIVE_CONE as f64); v.extend_from_slice(origin); v.extend_from_slice(radii); v.push(*height); } + TObjIn::PrimitiveTorus { origin, radii } => { v.push(tobj::PRIMITIVE_TORUS as f64); v.extend_from_slice(origin); v.extend_from_slice(radii); } TObjIn::CallerFace { origin, size } => { v.push(tobj::CALLER_FACE as f64); v.extend_from_slice(origin); @@ -4577,6 +4583,9 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail TObjIn::PrimitiveSphere { origin, radius } | TObjIn::CopiedSphere { origin, radius } => st(solidcore::primitive::api_primitives::create_sphere( &mut k, v3(*origin), *radius, ))?.map_err(|code| harness(format!("sphere construction status {code}")))?, + TObjIn::PrimitiveCylinder { origin, radius, height } => st(solidcore::primitive::api_primitives::create_cylinder(&mut k, v3(*origin), *radius, *height))?.map_err(|code| harness(format!("cylinder construction status {code}")))?, + TObjIn::PrimitiveCone { origin, radii: [base, top], height } => st(solidcore::primitive::api_primitives::create_cone(&mut k, v3(*origin), *base, *top, *height))?.map_err(|code| harness(format!("cone construction status {code}")))?, + TObjIn::PrimitiveTorus { origin, radii: [major, minor] } => st(solidcore::primitive::api_primitives::create_torus(&mut k, v3(*origin), *major, *minor))?.map_err(|code| harness(format!("torus construction status {code}")))?, TObjIn::CallerFace { origin, size: [width, height] } => { let mut body = st(solidcore::topology::brep::p3::new_brep(&mut k, solidcore::geom::tol::USE_DEFAULT, solidcore::geom::tol::USE_DEFAULT))? .map_err(|code| harness(format!("caller body status {code}")))?; @@ -4629,7 +4638,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail TObjIn::Plane { .. } | TObjIn::Bsurf { .. } => { surface.as_mut().map(TransformObject::Surface) } - TObjIn::Brep | TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. } => Some(TransformObject::Brep(&mut brep)), + TObjIn::Brep | TObjIn::PrimitiveCylinder { .. } | TObjIn::PrimitiveCone { .. } | TObjIn::PrimitiveTorus { .. } | TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. } => Some(TransformObject::Brep(&mut brep)), TObjIn::Poly | TObjIn::PolyPts { .. } => poly_target .as_mut() .map(|t| TransformObject::PolyBrep(t as &mut dyn PolyBrepTransform)), @@ -4658,7 +4667,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail }; let mut o = vec![s]; if let Some(frame) = &probe_frame { o.push(frame.initial_types.len() as f64); o.extend_from_slice(&frame.initial_types); } - if matches!(obj, TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. }) { + if matches!(obj, TObjIn::PrimitiveCylinder { .. } | TObjIn::PrimitiveCone { .. } | TObjIn::PrimitiveTorus { .. } | TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. }) { let vertices = solidcore::topology::brep::p1::vertices(&brep, None).map_err(|e| harness(e.to_string()))?; let edges = solidcore::topology::brep::p1::edges(&brep, None).map_err(|e| harness(e.to_string()))?; let faces = solidcore::topology::brep::p1::faces(&brep, None).map_err(|e| harness(e.to_string()))?; diff --git a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs index 6c39e6acec..73a0753f0c 100644 --- a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs +++ b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs @@ -431,6 +431,9 @@ pub mod tobj { pub const CALLER_FACE: i64 = 12; pub const COPIED_BOX: i64 = 13; pub const COPIED_SPHERE: i64 = 14; + pub const PRIMITIVE_CYLINDER: i64 = 15; + pub const PRIMITIVE_CONE: i64 = 16; + pub const PRIMITIVE_TORUS: i64 = 17; } const MAX_SLOTS: i64 = 32; @@ -834,9 +837,11 @@ fn validate_proto(rd: &mut Reader<'_>) -> Result<(), FfiError> { } fn validate_transform(rd: &mut Reader<'_>, probe: bool) -> Result<(), FfiError> { - let object = rd.int(0, tobj::COPIED_SPHERE)?; - if probe && !matches!(object, tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE) { return Err(bad("scale probe requires constructed solid")); } + let object = rd.int(0, tobj::PRIMITIVE_TORUS)?; + if probe && !matches!(object, tobj::PRIMITIVE_CYLINDER | tobj::PRIMITIVE_CONE | tobj::PRIMITIVE_TORUS | tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE) { return Err(bad("scale probe requires constructed solid")); } match object { + tobj::PRIMITIVE_CYLINDER | tobj::PRIMITIVE_TORUS => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("primitive dimension")); } } } + tobj::PRIMITIVE_CONE => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("cone dimension")); } } } tobj::CALLER_FACE => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("face dimension")); } } } tobj::PRIMITIVE_BOX | tobj::COPIED_BOX => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("box dimension")); } } } tobj::PRIMITIVE_SPHERE | tobj::COPIED_SPHERE => { rd.vec3()?; if rd.finite()?.abs() <= 1e-12 { return Err(bad("sphere radius")); } } diff --git a/fixtures/topology.proto_topology_p1/public_scale_cone_torus.json b/fixtures/topology.proto_topology_p1/public_scale_cone_torus.json new file mode 100644 index 0000000000..3330724847 --- /dev/null +++ b/fixtures/topology.proto_topology_p1/public_scale_cone_torus.json @@ -0,0 +1,262 @@ +{ + "cases": [ + { + "name": "public_scale_cylinder_identity", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cylinder": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2, + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + 1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_cylinder_nonuniform", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cylinder": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2, + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_cylinder_reflection", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cylinder": { + "origin": [ + 2, + 3, + 5 + ], + "radius": 2, + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + -1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_cone_identity", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cone": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 2, + 1 + ], + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + 1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_cone_nonuniform", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cone": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 2, + 1 + ], + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_cone_reflection", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_cone": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 2, + 1 + ], + "height": 3 + } + }, + "op": { + "scale": { + "s": [ + -1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_torus_identity", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_torus": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 3, + 1 + ] + } + }, + "op": { + "scale": { + "s": [ + 1, + 1, + 1 + ] + } + } + } + }, + { + "name": "public_scale_torus_nonuniform", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_torus": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 3, + 1 + ] + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_scale_torus_reflection", + "upstream_ref": "source/SM_API/src/SmApiPrimitives.cpp;source/SMLib/src/SmBrep.cpp:Transform;TurnToNURBS", + "inputs": { + "script": "scale_probe", + "obj": { + "primitive_torus": { + "origin": [ + 2, + 3, + 5 + ], + "radii": [ + 3, + 1 + ] + } + }, + "op": { + "scale": { + "s": [ + -1, + 1, + 1 + ] + } + } + } + } + ] +} diff --git a/harness/cpp/shim/topology_proto_topology_p1.cpp b/harness/cpp/shim/topology_proto_topology_p1.cpp index 7691d9e40e..95d6978eb8 100644 --- a/harness/cpp/shim/topology_proto_topology_p1.cpp +++ b/harness/cpp/shim/topology_proto_topology_p1.cpp @@ -4849,6 +4849,9 @@ enum TObj : int64_t T_CALLER_FACE = 12, T_COPIED_BOX = 13, T_COPIED_SPHERE = 14, + T_PRIMITIVE_CYLINDER = 15, + T_PRIMITIVE_CONE = 16, + T_PRIMITIVE_TORUS = 17, }; // SmCrvOnSurf's UV-line constructor is protected. @@ -4902,7 +4905,7 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) { const SmContext& ctx = the_context(); int64_t kind = rd.idx(); - if(probe && kind!=T_PRIMITIVE_BOX && kind!=T_PRIMITIVE_SPHERE && kind!=T_COPIED_BOX && kind!=T_COPIED_SPHERE) return SC_ERR_ARG; + if(probe && kind!=T_PRIMITIVE_CYLINDER && kind!=T_PRIMITIVE_CONE && kind!=T_PRIMITIVE_TORUS && kind!=T_PRIMITIVE_BOX && kind!=T_PRIMITIVE_SPHERE && kind!=T_COPIED_BOX && kind!=T_COPIED_SPHERE) return SC_ERR_ARG; SmObject* obj = NULL; SmBrep* source = NULL; std::vector source_before; @@ -4946,6 +4949,17 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) } obj = body; break; } + case T_PRIMITIVE_CYLINDER: + case T_PRIMITIVE_CONE: + case T_PRIMITIVE_TORUS: + { + SmPoint3d origin=rd.vec(); double a=rd.num(), b=rd.num(); SmBrep* body=NULL; SmApiStatus created; + if(kind==T_PRIMITIVE_CYLINDER) created=SmApiCreateCylinder(origin,a,b,body); + else if(kind==T_PRIMITIVE_TORUS) created=SmApiCreateTorus(origin,a,b,body); + else { double height=rd.num(); created=SmApiCreateCone(origin,a,b,height,body); } + if(created!=SM_SUCCESS || !body) { o.push_back((double)created); delete body; return SC_OK; } + obj=body; break; + } case T_CALLER_FACE: { SmPoint3d origin=rd.vec(); double width=rd.num(), height=rd.num(); From fd2cb79792d2d33160f6c09d5e08fb488dee9636 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 13:47:47 +0200 Subject: [PATCH 020/491] test: align constructed scale records and cone admission Co-Authored-By: OpenAI Codex --- crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs | 2 +- harness/cpp/shim/topology_proto_topology_p1.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs index 73a0753f0c..665600ace0 100644 --- a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs +++ b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs @@ -841,7 +841,7 @@ fn validate_transform(rd: &mut Reader<'_>, probe: bool) -> Result<(), FfiError> if probe && !matches!(object, tobj::PRIMITIVE_CYLINDER | tobj::PRIMITIVE_CONE | tobj::PRIMITIVE_TORUS | tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE) { return Err(bad("scale probe requires constructed solid")); } match object { tobj::PRIMITIVE_CYLINDER | tobj::PRIMITIVE_TORUS => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("primitive dimension")); } } } - tobj::PRIMITIVE_CONE => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("cone dimension")); } } } + tobj::PRIMITIVE_CONE => { rd.vec3()?; let base=rd.finite()?; let top=rd.finite()?; let height=rd.finite()?; if base<0.0 || top<0.0 || (base<=1e-12 && top<=1e-12) || height<=1e-12 { return Err(bad("cone dimension")); } } tobj::CALLER_FACE => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("face dimension")); } } } tobj::PRIMITIVE_BOX | tobj::COPIED_BOX => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("box dimension")); } } } tobj::PRIMITIVE_SPHERE | tobj::COPIED_SPHERE => { rd.vec3()?; if rd.finite()?.abs() <= 1e-12 { return Err(bad("sphere radius")); } } diff --git a/harness/cpp/shim/topology_proto_topology_p1.cpp b/harness/cpp/shim/topology_proto_topology_p1.cpp index 95d6978eb8..7f85a152c6 100644 --- a/harness/cpp/shim/topology_proto_topology_p1.cpp +++ b/harness/cpp/shim/topology_proto_topology_p1.cpp @@ -5074,7 +5074,7 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) } o.push_back((double)s); if(frame) { o.push_back((double)frame->initial_types.size()); o.insert(o.end(),frame->initial_types.begin(),frame->initial_types.end()); } - if (kind == T_PRIMITIVE_BOX || kind == T_PRIMITIVE_SPHERE || kind == T_CALLER_FACE || kind == T_COPIED_BOX || kind == T_COPIED_SPHERE) { + if (kind == T_PRIMITIVE_CYLINDER || kind == T_PRIMITIVE_CONE || kind == T_PRIMITIVE_TORUS || kind == T_PRIMITIVE_BOX || kind == T_PRIMITIVE_SPHERE || kind == T_CALLER_FACE || kind == T_COPIED_BOX || kind == T_COPIED_SPHERE) { SmBrep* body=(SmBrep*)obj; SmTArray vertices; body->GetVertices(vertices); SmTArray edges; body->GetEdges(edges); From 9de43ff5ac12e22b6f431a07aa6c194a268a718d Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 13:49:53 +0200 Subject: [PATCH 021/491] fix: convert inherited cone and torus surfaces before solid scaling Co-Authored-By: OpenAI Codex --- crates/solidcore/src/topology/brep/p3.rs | 28 +++++++++++++++++++++- plan/units/topology.brep_p3.toml | 2 +- plan/units/topology.proto_topology_p1.toml | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/solidcore/src/topology/brep/p3.rs b/crates/solidcore/src/topology/brep/p3.rs index c051de5a76..4edea3c21a 100644 --- a/crates/solidcore/src/topology/brep/p3.rs +++ b/crates/solidcore/src/topology/brep/p3.rs @@ -2908,6 +2908,30 @@ fn with_nurb<'a>(k: &Kernel, s: &'a Surface) -> Result { + if x.rev().base().gw_nurb().is_some() { + Some(Cow::Borrowed(x.rev().base())) + } else { + let mut tmp = x.as_ref().clone(); + match tmp.make_nurb(k) { + Err(e) if e.is_unported() => return Err(e), + Ok(()) | Err(_) => {} + } + Some(Cow::Owned(tmp.rev().base().clone())) + } + } + SurfaceKind::Torus(x) => { + if x.base().base().gw_nurb().is_some() { + Some(Cow::Borrowed(x.base().base())) + } else { + let mut tmp = x.as_ref().clone(); + match tmp.make_nurb(k) { + Err(e) if e.is_unported() => return Err(e), + Ok(()) | Err(_) => {} + } + Some(Cow::Owned(tmp.base().base().clone())) + } + } SurfaceKind::Sphere(x) => { if x.base().base().gw_nurb().is_some() { Some(Cow::Borrowed(x.base().base())) @@ -2929,13 +2953,15 @@ const fn as_bspline_surface(s: &Surface) -> Option<&BSplineSurface> { SurfaceKind::BSpline(x) => Some(x), SurfaceKind::Plane(x) => Some(x.base()), SurfaceKind::SurfOfRevolution(x) => Some(x.base()), + SurfaceKind::Cone(x) => Some(x.rev().base()), + SurfaceKind::Torus(x) => Some(x.base().base()), SurfaceKind::Sphere(x) => Some(x.base().base()), _ => None, } } /// `TurnToNURBS`: makes every face surface and edge curve of the B-rep a plain -/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`, `SmSphere`) gets its B-spline +/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`, `SmSphere`, `SmCone`, `SmTorus`) gets its B-spline /// copy (`ReplaceSurface`, which rebuilds the UV trim curves); an `SmOffsetSurface` of a /// B-spline kind is approximated (`ApproximateOffsetSurface` at ten times the B-rep /// tolerance, chord length; a failed approximation skips the face), extended by 2% of diff --git a/plan/units/topology.brep_p3.toml b/plan/units/topology.brep_p3.toml index 02ce89ce0b..2c7627ca63 100644 --- a/plan/units/topology.brep_p3.toml +++ b/plan/units/topology.brep_p3.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred. CO-PORTED in public solid scale phase two (2026-10-24): TurnToNURBS accepts producer-authorized geometry-selection keys separately from canonical notification keys and binds converted curves to the actual destination edge. Legacy arena entrypoint retains its convention. The BSpline-surface cast/materialization helpers now include Sphere through its native SurfOfRevolution inheritance, so surface conversion precedes the curve pass. Ten actual public scale cases pass exactly; no numeric/tolerance changes." +notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred. CO-PORTED in public solid scale phase two (2026-10-24): TurnToNURBS accepts producer-authorized geometry-selection keys separately from canonical notification keys and binds converted curves to the actual destination edge. Legacy arena entrypoint retains its convention. The BSpline-surface cast/materialization helpers now include Sphere through its native SurfOfRevolution inheritance, so surface conversion precedes the curve pass. Ten actual public scale cases pass exactly; no numeric/tolerance changes. UPDATE (public Cone/Torus scale probes, 2026-10-24): inherited B-spline cast/materialization includes Cone and Torus, matching SmSurfOfRevolution inheritance. Nine real public cylinder/cone/torus identity/nonuniform/reflection fixtures pass exactly after six conversion-order mismatches (retained UV trims) were fixed. No cone/p2 reconstruction or numeric/tolerance changes." diff --git a/plan/units/topology.proto_topology_p1.toml b/plan/units/topology.proto_topology_p1.toml index d8d2a8e0f5..db1ce3d19c 100644 --- a/plan/units/topology.proto_topology_p1.toml +++ b/plan/units/topology.proto_topology_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "green" claimed_by = "" claimed_at = "" -notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures. Public solid scale phase two (2026-10-24): full runner 1891 passes, 0 failures/deferred. Ten real constructed/copied box/sphere cases cover positive nonuniform scaling, odd/even/all-negative reflection, ScaleByPt and unchanged zero-axis rejection. Native baseline had 9 failures; all10 now match exactly. Additive kind4 records actual pre/post geometry types and retained pre-operation topology identities for directed edgeuse order/mates/radials/vertices/faces and fixed faceuse shell/region membership. Controlled omission of edgeuse reversal and shell swapping each kills the 3 odd-reflection cases. Source-copy preservation covers sampled geometry/tolerances only; no warmed-cache claim." +notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures. Public solid scale phase two (2026-10-24): full runner 1891 passes, 0 failures/deferred. Ten real constructed/copied box/sphere cases cover positive nonuniform scaling, odd/even/all-negative reflection, ScaleByPt and unchanged zero-axis rejection. Native baseline had 9 failures; all10 now match exactly. Additive kind4 records actual pre/post geometry types and retained pre-operation topology identities for directed edgeuse order/mates/radials/vertices/faces and fixed faceuse shell/region membership. Controlled omission of edgeuse reversal and shell swapping each kills the 3 odd-reflection cases. Source-copy preservation covers sampled geometry/tolerances only; no warmed-cache claim. UPDATE (public Cone/Torus scale probes, 2026-10-24): nine additive real constructed cylinder/cone/torus cases use ScaleProbe retained runtime types and directed topology. All nine pass exactly, as do all ten prior box/sphere scale probes; no owner rebinding or tolerance changes." From 00e3918cccd2213916cc7c770f3684023d16fc7a Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 14:06:30 +0200 Subject: [PATCH 022/491] difftest: reproduce public extrusion scale geometry mismatch Construct real finite extrusion faces with materialized B-spline data through the public face API; retain type and complete geometry records. Co-Authored-By: OpenAI Codex --- .../src/units/topology_proto_topology_p1.rs | 33 +++- .../src/units/topology_proto_topology_p1.rs | 8 +- .../public_extrusion_transform.json | 154 ++++++++++++++++++ .../cpp/shim/topology_proto_topology_p1.cpp | 28 +++- 4 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 fixtures/topology.proto_topology_p1/public_extrusion_transform.json diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index ed7a9673c2..bd2e275892 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -245,6 +245,7 @@ pub(super) enum TObjIn { PrimitiveBox { origin: [f64; 3], size: [f64; 3] }, PrimitiveSphere { origin: [f64; 3], radius: f64 }, CallerFace { origin: [f64; 3], size: [f64; 2] }, + ExtrusionFace { origin: [f64; 3], size: [f64; 2] }, CopiedBox { origin: [f64; 3], size: [f64; 3] }, CopiedSphere { origin: [f64; 3], radius: f64 }, /// An empty `SmPolyBrep`. @@ -462,6 +463,11 @@ pub(super) fn cpp_input(s: &Script) -> Vec { v.extend_from_slice(origin); v.push(*radius); } + TObjIn::ExtrusionFace { origin, size } => { + v.push(tobj::EXTRUSION_FACE as f64); + v.extend(origin); + v.extend(size); + } TObjIn::CallerFace { origin, size } => { v.push(tobj::CALLER_FACE as f64); v.extend_from_slice(origin); @@ -4577,7 +4583,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail TObjIn::PrimitiveSphere { origin, radius } | TObjIn::CopiedSphere { origin, radius } => st(solidcore::primitive::api_primitives::create_sphere( &mut k, v3(*origin), *radius, ))?.map_err(|code| harness(format!("sphere construction status {code}")))?, - TObjIn::CallerFace { origin, size: [width, height] } => { + TObjIn::CallerFace { origin, size: [width, height] } | TObjIn::ExtrusionFace { origin, size: [width, height] } => { let mut body = st(solidcore::topology::brep::p3::new_brep(&mut k, solidcore::geom::tol::USE_DEFAULT, solidcore::geom::tol::USE_DEFAULT))? .map_err(|code| harness(format!("caller body status {code}")))?; let keys = |entity| { @@ -4590,9 +4596,26 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail let this_face = |face| keys(EntityRef::Face(face)); let mut marks = solidcore::topology::topology_traverser::Marks::default(); let mut fenv = solidcore::topology::brep::p8::FaceBuildEnv { env: &mut env, marks: &mut marks, this_face: &this_face }; + let (surface, domain) = if matches!(obj, TObjIn::ExtrusionFace { .. }) { + let [x, y, z] = *origin; + let generator = build_curve(&CurveIn::Bspline(BSplineIn { + dim: 3, oob: false, p: 2, + cpts: vec![[x, y, z, 1.0], [x + *width / 2.0, y + 1.0, z, 1.0], [x + *width, y, z, 1.0]], + u: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], + }))?; + let domain = Extent2d::from_bounds(0.0, 0.0, 1.0, *height); + let mut extrusion = st(solidcore::surface::surf_of_extrusion::p1::SurfOfExtrusion::new( + &k, generator, v3(*origin), Vec3::new(0.0, 0.0, 1.0), domain, false))? + .map_err(|code| harness(format!("extrusion construction status {code}")))?; + st(extrusion.make_nurb(&k))? + .map_err(|code| harness(format!("extrusion NURB status {code}")))?; + (Surface::from(extrusion), domain) + } else { + (Surface::from(Plane::from_normal(v3(*origin), Vec3::new(0.0, 0.0, 1.0))), + Extent2d::from_bounds(0.0, 0.0, *width, *height)) + }; st(solidcore::topology::brep::p8::create_face_from_surface(&mut k, &mut body, - Some(Surface::from(Plane::from_normal(v3(*origin), Vec3::new(0.0, 0.0, 1.0)))), - &Extent2d::from_bounds(0.0, 0.0, *width, *height), &mut fenv))? + Some(surface), &domain, &mut fenv))? .map_err(|code| harness(format!("caller face status {code}")))?; body } @@ -4629,7 +4652,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail TObjIn::Plane { .. } | TObjIn::Bsurf { .. } => { surface.as_mut().map(TransformObject::Surface) } - TObjIn::Brep | TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. } => Some(TransformObject::Brep(&mut brep)), + TObjIn::Brep | TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::ExtrusionFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. } => Some(TransformObject::Brep(&mut brep)), TObjIn::Poly | TObjIn::PolyPts { .. } => poly_target .as_mut() .map(|t| TransformObject::PolyBrep(t as &mut dyn PolyBrepTransform)), @@ -4658,7 +4681,7 @@ fn run_transform(obj: &TObjIn, op: &TOpIn, probe: bool) -> Result, Fail }; let mut o = vec![s]; if let Some(frame) = &probe_frame { o.push(frame.initial_types.len() as f64); o.extend_from_slice(&frame.initial_types); } - if matches!(obj, TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. }) { + if matches!(obj, TObjIn::PrimitiveBox { .. } | TObjIn::PrimitiveSphere { .. } | TObjIn::CallerFace { .. } | TObjIn::ExtrusionFace { .. } | TObjIn::CopiedBox { .. } | TObjIn::CopiedSphere { .. }) { let vertices = solidcore::topology::brep::p1::vertices(&brep, None).map_err(|e| harness(e.to_string()))?; let edges = solidcore::topology::brep::p1::edges(&brep, None).map_err(|e| harness(e.to_string()))?; let faces = solidcore::topology::brep::p1::faces(&brep, None).map_err(|e| harness(e.to_string()))?; diff --git a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs index 6c39e6acec..1018dd1e38 100644 --- a/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs +++ b/crates/solidcore-ffi/src/units/topology_proto_topology_p1.rs @@ -431,6 +431,7 @@ pub mod tobj { pub const CALLER_FACE: i64 = 12; pub const COPIED_BOX: i64 = 13; pub const COPIED_SPHERE: i64 = 14; + pub const EXTRUSION_FACE: i64 = 18; } const MAX_SLOTS: i64 = 32; @@ -834,10 +835,11 @@ fn validate_proto(rd: &mut Reader<'_>) -> Result<(), FfiError> { } fn validate_transform(rd: &mut Reader<'_>, probe: bool) -> Result<(), FfiError> { - let object = rd.int(0, tobj::COPIED_SPHERE)?; - if probe && !matches!(object, tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE) { return Err(bad("scale probe requires constructed solid")); } + let object = rd.int(0, tobj::EXTRUSION_FACE)?; + if probe && !matches!(object, tobj::PRIMITIVE_BOX | tobj::PRIMITIVE_SPHERE | tobj::COPIED_BOX | tobj::COPIED_SPHERE | tobj::EXTRUSION_FACE) { return Err(bad("scale probe requires constructed solid")); } match object { - tobj::CALLER_FACE => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("face dimension")); } } } + 15..=17 => return Err(bad("reserved transform object")), + tobj::CALLER_FACE | tobj::EXTRUSION_FACE => { rd.vec3()?; for _ in 0..2 { if rd.finite()? <= 1e-12 { return Err(bad("face dimension")); } } } tobj::PRIMITIVE_BOX | tobj::COPIED_BOX => { rd.vec3()?; for _ in 0..3 { if rd.finite()? <= 1e-12 { return Err(bad("box dimension")); } } } tobj::PRIMITIVE_SPHERE | tobj::COPIED_SPHERE => { rd.vec3()?; if rd.finite()?.abs() <= 1e-12 { return Err(bad("sphere radius")); } } tobj::POLY_PTS => { diff --git a/fixtures/topology.proto_topology_p1/public_extrusion_transform.json b/fixtures/topology.proto_topology_p1/public_extrusion_transform.json new file mode 100644 index 0000000000..210992a5bb --- /dev/null +++ b/fixtures/topology.proto_topology_p1/public_extrusion_transform.json @@ -0,0 +1,154 @@ +{ + "cases": [ + { + "name": "public_extrusion_identity", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "translate": { + "v": [ + 0, + 0, + 0 + ] + } + } + } + }, + { + "name": "public_extrusion_translate", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "translate": { + "v": [ + 7, + -2, + 3 + ] + } + } + } + }, + { + "name": "public_extrusion_uniform", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "scale": { + "s": [ + 2, + 2, + 2 + ] + } + } + } + }, + { + "name": "public_extrusion_anisotropic", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "scale": { + "s": [ + 2, + 3, + 0.5 + ] + } + } + } + }, + { + "name": "public_extrusion_signed", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "scale_by_pt": { + "s": [ + -2, + 3, + 0.5 + ], + "r": [ + 1, + 2, + 3 + ] + } + } + } + } + ] +} diff --git a/harness/cpp/shim/topology_proto_topology_p1.cpp b/harness/cpp/shim/topology_proto_topology_p1.cpp index 7691d9e40e..5ea82cb866 100644 --- a/harness/cpp/shim/topology_proto_topology_p1.cpp +++ b/harness/cpp/shim/topology_proto_topology_p1.cpp @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -4847,6 +4848,7 @@ enum TObj : int64_t T_PRIMITIVE_BOX = 10, T_PRIMITIVE_SPHERE = 11, T_CALLER_FACE = 12, + T_EXTRUSION_FACE = 18, T_COPIED_BOX = 13, T_COPIED_SPHERE = 14, }; @@ -4902,7 +4904,7 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) { const SmContext& ctx = the_context(); int64_t kind = rd.idx(); - if(probe && kind!=T_PRIMITIVE_BOX && kind!=T_PRIMITIVE_SPHERE && kind!=T_COPIED_BOX && kind!=T_COPIED_SPHERE) return SC_ERR_ARG; + if(probe && kind!=T_PRIMITIVE_BOX && kind!=T_PRIMITIVE_SPHERE && kind!=T_COPIED_BOX && kind!=T_COPIED_SPHERE && kind!=T_EXTRUSION_FACE) return SC_ERR_ARG; SmObject* obj = NULL; SmBrep* source = NULL; std::vector source_before; @@ -4947,11 +4949,29 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) obj = body; break; } case T_CALLER_FACE: + case T_EXTRUSION_FACE: { SmPoint3d origin=rd.vec(); double width=rd.num(), height=rd.num(); SmBrep* body=new(ctx) SmBrep(); SmFace* face=NULL; - SmPlane* surface=new(ctx) SmPlane(origin,SmVector3d(0,0,1),&ctx); - SmStatus created=body->CreateFaceFromSurface(surface,SmExtent2d(0,0,width,height),face); + SmSurface* surface=NULL; + SmExtent2d domain(0,0,width,height); + if(kind==T_EXTRUSION_FACE) { + gw_CURVE* nurb=sm_AllocateNurbCurve(2,2,5); + for(int i=0;i<3;++i) { + nurb->pol->Pw[i].x=origin.x+width*i/2.0; + nurb->pol->Pw[i].y=origin.y+(i==1?1.0:0.0); + nurb->pol->Pw[i].z=origin.z; + nurb->pol->Pw[i].w=1.0; + } + for(int i=0;i<6;++i) nurb->knt->U[i]=(i<3?0.0:1.0); + SmBSplineCurve* generator=new(ctx) SmBSplineCurve(nurb,3,FALSE); + domain=SmExtent2d(0,0,1,height); + SmSurfOfExtrusion* extrusion=new(ctx) SmSurfOfExtrusion(generator,origin,SmVector3d(0,0,1),domain,FALSE); + SmStatus made=extrusion->MakeNurb(); + if(made!=SM_SUCCESS) { o.push_back((double)made); delete extrusion; delete body; return SC_OK; } + surface=extrusion; + } else surface=new(ctx) SmPlane(origin,SmVector3d(0,0,1),&ctx); + SmStatus created=body->CreateFaceFromSurface(surface,domain,face); if(created!=SM_SUCCESS || !face) { o.push_back((double)created); delete body; return SC_OK; } obj=body; break; } @@ -5060,7 +5080,7 @@ int32_t run_transform(Rd& rd, std::vector& o, bool probe=false) } o.push_back((double)s); if(frame) { o.push_back((double)frame->initial_types.size()); o.insert(o.end(),frame->initial_types.begin(),frame->initial_types.end()); } - if (kind == T_PRIMITIVE_BOX || kind == T_PRIMITIVE_SPHERE || kind == T_CALLER_FACE || kind == T_COPIED_BOX || kind == T_COPIED_SPHERE) { + if (kind == T_PRIMITIVE_BOX || kind == T_PRIMITIVE_SPHERE || kind == T_CALLER_FACE || kind == T_EXTRUSION_FACE || kind == T_COPIED_BOX || kind == T_COPIED_SPHERE) { SmBrep* body=(SmBrep*)obj; SmTArray vertices; body->GetVertices(vertices); SmTArray edges; body->GetEdges(edges); From 9f7ebd81a120613fef2419f4a19e2a1e040ea924 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 14:11:00 +0200 Subject: [PATCH 023/491] fix: convert inherited extrusion surfaces before public scaling Treat SurfOfExtrusion as its native BSplineSurface base in TurnToNURBS. Seven real face cases now match native geometry, type and topology; retain the separate lazy face-construction provider gap in findings. Co-Authored-By: OpenAI Codex --- crates/solidcore/src/topology/brep/p3.rs | 18 +++++- .../public_extrusion_transform.json | 58 +++++++++++++++++++ plan/findings.md | 18 ++++++ plan/units/topology.brep_p3.toml | 2 +- plan/units/topology.proto_topology_p1.toml | 2 +- 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/crates/solidcore/src/topology/brep/p3.rs b/crates/solidcore/src/topology/brep/p3.rs index c051de5a76..b569c83b97 100644 --- a/crates/solidcore/src/topology/brep/p3.rs +++ b/crates/solidcore/src/topology/brep/p3.rs @@ -2908,6 +2908,18 @@ fn with_nurb<'a>(k: &Kernel, s: &'a Surface) -> Result { + if x.base().gw_nurb().is_some() { + Some(Cow::Borrowed(x.base())) + } else { + let mut tmp = x.as_ref().clone(); + match tmp.make_nurb(k) { + Err(e) if e.is_unported() => return Err(e), + Ok(()) | Err(_) => {} + } + Some(Cow::Owned(tmp.base().clone())) + } + } SurfaceKind::Sphere(x) => { if x.base().base().gw_nurb().is_some() { Some(Cow::Borrowed(x.base().base())) @@ -2929,14 +2941,16 @@ const fn as_bspline_surface(s: &Surface) -> Option<&BSplineSurface> { SurfaceKind::BSpline(x) => Some(x), SurfaceKind::Plane(x) => Some(x.base()), SurfaceKind::SurfOfRevolution(x) => Some(x.base()), + SurfaceKind::SurfOfExtrusion(x) => Some(x.base()), SurfaceKind::Sphere(x) => Some(x.base().base()), _ => None, } } /// `TurnToNURBS`: makes every face surface and edge curve of the B-rep a plain -/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`, `SmSphere`) gets its B-spline -/// copy (`ReplaceSurface`, which rebuilds the UV trim curves); an `SmOffsetSurface` of a +/// B-spline: a B-spline kind (`SmPlane`, `SmSurfOfRevolution`, `SmSphere`, +/// `SmSurfOfExtrusion`) gets its B-spline copy (`ReplaceSurface`, which rebuilds the +/// UV trim curves); an `SmOffsetSurface` of a /// B-spline kind is approximated (`ApproximateOffsetSurface` at ten times the B-rep /// tolerance, chord length; a failed approximation skips the face), extended by 2% of /// its box diagonal (`CreateExtendedSurface`, G1) and replaces the face's surface; a diff --git a/fixtures/topology.proto_topology_p1/public_extrusion_transform.json b/fixtures/topology.proto_topology_p1/public_extrusion_transform.json index 210992a5bb..c69ed1bc6e 100644 --- a/fixtures/topology.proto_topology_p1/public_extrusion_transform.json +++ b/fixtures/topology.proto_topology_p1/public_extrusion_transform.json @@ -149,6 +149,64 @@ } } } + }, + { + "name": "public_extrusion_odd_reflection", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "scale": { + "s": [ + -1, + -1, + -1 + ] + } + } + } + }, + { + "name": "public_extrusion_even_reflection", + "upstream_ref": "source/SMLib/src/SmBrep.cpp:TurnToNURBS;source/SMLib/src/SmSurfOfExtrusion.cpp", + "inputs": { + "script": "scale_probe", + "obj": { + "extrusion_face": { + "origin": [ + 2, + 3, + 5 + ], + "size": [ + 4, + 3 + ] + } + }, + "op": { + "scale": { + "s": [ + -1, + -1, + 1 + ] + } + } + } } ] } diff --git a/plan/findings.md b/plan/findings.md index 6324a3f263..7c40989aea 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18738,3 +18738,21 @@ entry's reason: - **Fix.** The same as the entry above: thread `&mut Brep` through `ClassifyLoops` to the bisector. When that lands, re-run these nine cases and remove their known-mismatch entries. + +### 2026-10-24 — Public scale of a real extrusion face skipped inherited B-spline conversion + +`topology.proto_topology_p1`: a finite `SurfOfExtrusion` with a genuine quadratic +B-spline generator, materialized NURB and public `CreateFaceFromSurface` topology +passed identity, translation and uniform scale, but failed anisotropic scale and +signed `ScaleByPt`. One interior surface coordinate stayed 2.68 in Rust while native +returned 5.36 / -2.36. Native `SmSurfOfExtrusion` inherits `SmBSplineSurface`, so +`SmBrep::TurnToNURBS` copies it before transforming. Adding that inherited kind to +`brep::p3::{as_bspline_surface,with_nurb}` fixes all seven real-face cases, including +odd/even reflections, without changing owner identities. + +Separate remaining provider gap: without explicit `MakeNurb` on either side before +face creation, all five initial extrusion scripts stop at Rust face status 1006. +`surface::surface::p6::create_iso_parametric_curve` reads the extrusion's empty base +instead of obtaining its lazy NURB through the native virtual path. These transform +fixtures explicitly materialize the real surface on both sides; they do not fix or +claim coverage of that lazy-construction path. diff --git a/plan/units/topology.brep_p3.toml b/plan/units/topology.brep_p3.toml index 02ce89ce0b..03309f3224 100644 --- a/plan/units/topology.brep_p3.toml +++ b/plan/units/topology.brep_p3.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_tsurf_cyl_creation_2d", "prog_test:my_test_tsurf_cyl status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred. CO-PORTED in public solid scale phase two (2026-10-24): TurnToNURBS accepts producer-authorized geometry-selection keys separately from canonical notification keys and binds converted curves to the actual destination edge. Legacy arena entrypoint retains its convention. The BSpline-surface cast/materialization helpers now include Sphere through its native SurfOfRevolution inheritance, so surface conversion precedes the curve pass. Ten actual public scale cases pass exactly; no numeric/tolerance changes." +notes = "PR #661 (2026-10-23): the runner no longer reads the C++ tracker to choose a deferral (geometry_edit_deferral removed); the 6 TurnToNURBS geometry-edit-notify cases (nurbs_off_Mouse_0.1, nurbs_off_Mouse_-0.05, nurbs_off_toruswvy_0.1, gen0138, gen0164, gen0241) are listed in crates/difftest/known_mismatches.toml (upstream curves/surfaces notify their own PRE/POST_EDIT, the port does not; follow-up: port those notifications, see findings.md). difftest 897 cases, 853 pass, 0 failed, 38 deferred (query.face_p1 38), 6 known_mismatch. PR #637 (2026-10-23): ReplaceSurface's face ChangeGeometry notification wired with surface identities. difftest 897 cases, 853 pass, 0 failed, 44 deferred (query.face_p1 38, unmodelled:geometry-edit-notify 6: TurnToNURBS cases whose C++ tracker records geometry PRE/POST_EDIT notifications the port does not send, see findings.md); of the 47 former topology.face_p3 deferrals 41 pass. CO-PORTED in topology.brep_p10 (PR #130): SmBrep::SmBrep, the topology part (infinite void region heading the region list, m_pInfiniteRegion; m_bEditingEnabled is topology.brep_p1's field) -> topology::Brep::new_with_infinite_region. PARTIAL (PR #204): all 11 fns ported (stub at each unported call, tails ported). difftest 897 cases (497 fixtures from TestSmBrepCopy/TestSmErrorCodes/TestSmBrep/smOffsetTest/my_test_topology and SmBrep.cpp members on the 15 pt_TestFiles B-reps + network, incl. 30 TurnToNURBS cases on offset-wrapped faces; 400 generated; no face op on a B-rep without faces), 536 pass bit-identical, 0 failed, 361 deferred (topology.face_p3 161: SmFace::Notify; topology.face_p4 69: 39 SmFace::GetBrep via SwapUV, 30 SmFace::GetEdges in the offset cases after the approximation and extension; query.brep 49: CalculateBoundingBox; surface.sphere 33: SmSphere::IsNurbSurfaceSphere and surface.surface_p2 8: SmCone as SmSurface in the analytics copy (after main's surface.cone_p2 #153; they deferred at SmCone::IsNurbSurfaceCone before); topology.brep_p9 41: MakeShellVertex); --perturb SENSITIVE 536/536; with_values 536, 0 both-errored. Every op output (Construct, Copy, Merge, CopyVertices, DeleteFace, Wire, Orient, Squeeze, both TurnNurbs ops) ends with the B-rep counts, a shell digest (per region void/infinite/nshells, per shell type/GetSize/has_head) and the full topology graph (the fixture walk after the op: every owner / adjacency link as a walk index: region shells, shell type/size/region/head, face-use face/mate/shell/next/last/orientation/loop uses, face use, loop-use loop/mate/face use/start/orientation, loop use, edge-use edge/owner/cw/ccw/vertex use/next/last/orientation, edge-use and vertex-use lists); DeleteFace, Wire, Orient, Squeeze and both TurnNurbs ops run under SmTrackTopologyChanges on both sides, comparing its Rm/Add/Chg lists and every event, each object by its saved identity (never read): topology by walk index (C++: SmObject base addresses registered from the walks before and after the op), the B-rep, an edge's pre-op curve as that edge, else an edge's curve after the op as that edge (post-op walk index), other geometry numbered by first appearance in the output (C++: seen live as geometry by a user notify callback; a swap of two such transient objects within one event would not show, the documented boundary; unit test swapped_geometry_changes_the_output). Identities (module Deviations; overseer ruling on the 9129d9f2 reviews): topology keeps the NotifyEnv contract: the caller's env.key_of names b's objects, the caller's source_key those of a B-rep an edit reads (MergeBrep, CopyVertices, the copy constructors; restored after 2dc57273), and Brep::key each B-rep (keeping them distinct per object is the caller's part, as upstream's objects are the caller's); the records and attributes a caller keeps under them are used as they are, an object without a record gets the default one (edits seed with or_default, brep::p3::seed_records). Kernel-issued identities only for objects with no caller key: a curve = (domain, counter), issued once per context domain and kept on the Curve (GEOMETRY_KEY_BASE = 0b11 << 62; IdentitySlot, CO-PORTED into geom.context / curve.curve_p2); callers without notification state (brep::p3::with_kernel_callbacks, the healer's tracked_edit) name topology by (B-rep serial, kind, arena index) (brep::p3::topology_ref, TOPOLOGY_KEY_BASE = 0b10 << 62; the serial kept on the Brep, Brep::notify_serial, CO-PORTED into topology.brep_p1); both issued by the one context the notifications go through (env.context, else k.context), no public setter, a clone starts empty. Every edit refuses (InvalidInput) a caller key in the kernel range (>= KERNEL_KEY_BASE) before it changes anything, unless it is the object's own kernel identity (brep::p3::check_keys). The lending adapters (with_kernel_callbacks, tracked_edit; shared brep::p3::with_lent_context / kernel_named_edit) are not nestable: a nested one is refused before anything changes (Context::lend_notify; a lent context issues nothing). Boundary (Deviation): a kernel identity is unique and stable only within one domain, so every store keyed by identities (records, attributes, a tracker, a callback's state) belongs to one domain; shared across two contexts, or across a take_notify_lease that is not given back (a new domain), two curves or two kernel-named B-reps' objects may share a key; an object re-entering a domain is renamed, never aliased. Closing it needs a process-wide address space, which the no-global-state rule excludes. MakeWireEdge(Vertex) take the caller's unowned curve by value (upstream binds that object). Regressions: one_context_issues_the_identities_of_edits_in_two_kernels (R1, curves), caller_keys_cannot_take_a_kernel_identity (setter), copies_and_merges_carry_the_callers_records_and_attributes (C-M1 / C-M2), nested_lent_edits_are_refused and heal_data_p3's nested_tracked_edits_are_refused (C-M3 / P4), identities_are_unique_only_within_one_domain (P2b, the boundary), kernel_callbacks_keep_their_identity_domain, bound_curves_never_share_a_key, curve_keys_belong_to_their_context, curve_identities_survive_a_fresh_healer / _a_cloned_healer. Mutations: detaching every copied face use in MergeBrep fails 101 cases; corrupting the recorded ChangeGeometry curve fails 55. Boundary: no compared op sends a geometry-caller PostEdit / Split / Merge, so Execute's owner walk (the runner's tracker knows each pre-op curve's owner edge) is covered by its Rust regression only. Success paths (384): 35 constructors, 79 copies (copy constructor / SmApiBrepCopy), 43 MergeBrep, 49 MakeWireEdge(Vertex), 10 completed SqueezeEdge (network wires), 94 OrientTrimmedSurfaces that swap nothing, 47 TurnToNURBS (nothing converts), 27 CopyVertices([]); error statuses (152): 82 DeleteFace (editing disabled), 69 SqueezeEdge guards, 1 SmApiBrepCopy(NULL) (committed #[ignore] test classify_passes). No passing case for a completed DeleteFace, a swapping or vote-deciding Orient, or a converting TurnToNURBS (face_p3/face_p4/query.brep). --fuzz 2000 FUZZ-PASS; valgrind clean (status/topology.brep_p3-evidence). Wired: topology.shell (region void flag, GetShells, region/shell/face-use lists, TransferChildrenTo, GetSize; CO-PORTED region PostInsert, single face-use Remove/PostInsert, pub GetSize into topology::shell), topology.edgeuse_p3 (NormalizedEvaluate), surface.offset_surface (TurnToNURBS offset arm). Back-edges wired: sweep.api_brep/api_surfaces (SmBrep()), heal.heal_data_p3 (SqueezeEdge, DeleteFace under a real SmTrackTopologyChanges; SmTopologyChangeCallback::Execute CO-PORTED into topology.topology), IntersectorBrep::brep_delete_face, ProtoBrep::brep_squeeze_edge; every edit seeds the SmTopology records of its objects and of a source B-rep it copies or merges from that the caller keeps none for (the copy cases' Construction / Copy notifications name the source's). Upstream: WR-168. Was: 11 fns; largest: SmBrep::MergeBrep, SmBrep::OrientTrimmedSurfaces, SmBrep::DeleteFace, SmBrep::SqueezeEdge; entry points: SmApiBrepCopy; cycle back-edges (call into later units; stub or co-port): blend.fillet_geom_p1(2), query.brep(1); unit-SCC #22 (220 units); hazards: ptr-hash; 200 upstream test entry points reach this unit UPDATE (PR #204, scope-back + #149 da779c51 merge, main through 95b823ed): identities scoped back per the overseer ruling on the 9129d9f2 reviews (caller key_of / source_key / Brep::key for topology; kernel-issued IdentitySlot identities only for curves and stateless callers; nested lent edits refused with InvalidInput before anything changes; the identity-domain boundary in Deviations). Stubs wired: SmFace::Notify -> topology::face::p3::notify (no trim cache), SmBrep::MakeShellVertex -> topology::brep::p9::make_shell_vertex (CopyVertices). 897 cases, 664 pass, 0 fail, 233 deferred (topology.face_p4 126, query.brep 49, surface.sphere 33, topology.face_p3 17 via face p1's SmFace::Notify stub, surface.surface_p2 8), max_rel_err 0; case by case vs the committed report: 128 deferred -> pass (87 SmFace::Notify, 41 MakeShellVertex), 57 relabel SmFace::Notify -> face_p4 GetVertices, no other outcome change; --perturb 664/664; --fuzz 2000 FUZZ-PASS. UPDATE (PR #204, Codex review of 255d52b7): (1) the reserved-key check also covers the objects an edit may allocate (brep::p3::prospective_entities: the next ids of every kind, arenas only append, within allocation_window = the B-rep's object count + 3 x a read source's + 16), so a new object cannot take a curve's or a kernel-named object's key (regression new_objects_cannot_take_a_reserved_key); (2) a copy is named by the caller's destination mapper (Brep::key = env.key_of(Brep) before anything is notified; regression copies_take_their_own_caller_identity), and new_brep reports its construction under the new B-rep's own kernel identity (enrolled, as with_kernel_callbacks later names it; Brep::key stays the caller's; regression new_breps_construct_under_their_own_identities); (3) topology.brep_p5's plain entry points run their whole edit under the kernel's identities (p5::plain_edit; regression plain_p5_edits_of_two_breps_never_share_a_key, also covering a nested plain p5 edit refused before anything changes); (4) merge_p3 / merge_p4's SmBrep, copy constructor and MergeBrep wired (cfb7bed2). UPDATE (PR #204, Opus review of 255d52b7): #283's five UNPORTED(topology.brep_p3) back-edges are all wired: boolean/merge/p3.rs empty_brep (brep::p3::new_brep), copy_brep (brep::p3::copy_brep) and combine_brep (brep::p3::merge_brep), boolean/merge/p4.rs new_component_brep (Brep::new_with_infinite_region; kernel held shared), and the boolean.merge_p3 difftest runner's empty_brep (brep::p3::new_brep). Context::take_notify_lease / restore_notify_lease are refused (Result) while a crate edit has lent the context (the nested-lend refusal cannot be bypassed through them; regression in nested_lent_edits_are_refused); the lending helpers use the crate-internal take_lease / install_lease. kernel_named_edit's doc says flags are not kept between such edits. UPDATE (PR #204, Codex re-check of 6b04141d): M1: CopyVertices' key check covers three new objects per input-list entry, repeated entries included (allocation_window takes the input count; with_caller_identities_for), so Codex's repro (one shell vertex copied 30 times, the 30th new vertex keyed like a curve) is refused before anything changes (regression copy_vertices_checks_a_key_for_every_entry). M3: kernel_named_edit seeds a default SmTopology record for every existing object of the B-rep when its environment is made, so a plain part-5 edit's first notifications (DeleteTopologicalVertex's edge MergeInBrep on the A--B--C wire, sent before any p3 edit) reach the callback (regression plain_p5_edits_deliver_their_first_notifications; fails without the seeding). Gate on e326597e (main through 7b57d8de, #351): 897 cases, 684 pass, 0 fail, 213 deferred; case by case 20 surface.sphere deferrals pass (main's CopyAndAddAnalytics sphere wiring) and 13 relabel to surface.surface_p2; --perturb 684/684; FUZZ-PASS 2000; valgrind clean on 33 filters; 35 touched units 0 failed; difftest cpp self-tests 180 pass; green 237 units, 0 failed. UPDATE (PR #204, merge of main 7eb93aa4, #364): SmFace::SwapUV in OrientTrimmedSurfaces notifies PRE/POST_EDIT through the edit's environment (topology::face::p1::swap_uv_with_notifications), with (this, NULL, NULL) as upstream (SM_NO_GET_BREP is a null pointer); the 31 orient *_swap0 cases, in which SwapUV runs, defer under harness:surface-notify-identity (upstream's surface SwapUV notifies the surface's own edit through SetCanonical, SmBSplineSurface.cpp:4585/4592, and a Rust Surface has no notify IdentitySlot; follow-up: co-port it, then un-defer; plan/findings.md). Report: 897 cases, 684 pass, 0 fail, 213 deferred (harness:surface-notify-identity 31, query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case no outcome change, 30 face_p4 -> face_p3 and 8 face_p4 -> query.brep relabels. boolean.merge_p2's MergeBrep::orient_trimmed_surfaces is wired (a required method); its MakeWireEdge stays an integration gap (see boolean.merge_p2). Re-run: topology.brep_p7 316/240/76 (3 deferred -> pass, 30 relabels; its 3 MakeWireEdge deferrals are the merge_p2 gap), boolean.merge_p3 387/222/165 (37 relabels to topology.brep_p4 StitchFaces); brep_p5, merge_p1, merge_p2, face_p1 unchanged. UPDATE (PR #204, re-check of 766472a1): harness:surface-notify-identity is removed. Orient outputs split into out (always compared), swapped (each face's SwapUV POST_EDIT count from that side's own events, always compared) and tracker (left out only by a side whose events show a swap; the surface's SetCanonical edit is not notifiable in Rust yet), from a trailer the shim and runner both emit. The 31 *_swap0 cases now pass on out + swapped (97 to 1162 values each). Report: 897 cases, 715 pass, 0 fail, 182 deferred (query.brep 57, surface.surface_p2 21, topology.face_p3 47, topology.face_p4 57); case by case only those 31 deferred -> pass. boolean.merge_p2 unchanged (45/30/0/15). MakeWireEdge: MergeContext::Shared carries &mut Kernel and is wireable; left for the follow-up with Split. UPDATE (PR #204, main merge 2026-10-21 + SmFace::GetVertices wired to topology::face::p4::vertices with upstream defaults): 897 cases, 761 pass (max_rel_err 0), 0 fail, 136 deferred (was 715 / 182). SweepStore constructors/copies bind geometry to NotifyEnv topology owners. turn_to_nurbs_with_geometry_owners supports caller-key transforms; the original wrapper and WR-168 deviation are retained. Runner/shim (#556, 2026-10-23): DeleteFace records also compare the number of SM_NO_DESTRUCTION notifications of edges (~SmEdge) the op sends, after the tracker (13 of the 90 delface cases destroy an edge through DeleteEdge; without the edge destruction path they fail). 897 cases, 812 passes, 0 failed, 85 deferred, unchanged. DeleteFace destructors (#556, PR #631): DeleteFace now deletes what upstream deletes, namely the easy branch's edge uses (previously only unlinked), the other shell of a region merge, the emptied shell and the last new shell of the nesting pass, each through its destroy helper; destroy_face also clears the face's attributes. The DeleteFace trailer counts SM_NO_DESTRUCTION per caller kind (edge, edge use, face, shell): all 90 delface cases agree (23 failed with only the brep_p4 changes). 897 cases, 812 passes, 0 failed, 85 deferred. CO-PORTED in public solid scale phase two (2026-10-24): TurnToNURBS accepts producer-authorized geometry-selection keys separately from canonical notification keys and binds converted curves to the actual destination edge. Legacy arena entrypoint retains its convention. The BSpline-surface cast/materialization helpers now include Sphere through its native SurfOfRevolution inheritance, so surface conversion precedes the curve pass. Ten actual public scale cases pass exactly; no numeric/tolerance changes. Public extrusion transform followup (2026-10-24): TurnToNURBS now recognizes SurfOfExtrusion as its native BSplineSurface base and obtains its actual NURB before copying, with existing ordinary-status and Unported semantics. Seven real public-face scale/control cases pass exactly after the 3-pass/2-fail valid baseline; no identity rebinding. Explicit fixture MakeNurb does not fix the separate lazy iso-curve provider gap." diff --git a/plan/units/topology.proto_topology_p1.toml b/plan/units/topology.proto_topology_p1.toml index d8d2a8e0f5..89dea39816 100644 --- a/plan/units/topology.proto_topology_p1.toml +++ b/plan/units/topology.proto_topology_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "green" claimed_by = "" claimed_at = "" -notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures. Public solid scale phase two (2026-10-24): full runner 1891 passes, 0 failures/deferred. Ten real constructed/copied box/sphere cases cover positive nonuniform scaling, odd/even/all-negative reflection, ScaleByPt and unchanged zero-axis rejection. Native baseline had 9 failures; all10 now match exactly. Additive kind4 records actual pre/post geometry types and retained pre-operation topology identities for directed edgeuse order/mates/radials/vertices/faces and fixed faceuse shell/region membership. Controlled omission of edgeuse reversal and shell swapping each kills the 3 odd-reflection cases. Source-copy preservation covers sampled geometry/tolerances only; no warmed-cache claim." +notes = "PARTIAL: current numbers (after PR #144; fresh run in PR #154): difftest cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0, --perturb SENSITIVE 1765/1765. Original port (history, superseded by the numbers above): difftest 1601 cases (14 upstream-derived fixtures from SM_API_test TestSmGeneral.cpp transforms, 87 edge cases built in code: list walks, assemblies, the proto life cycle at unit/huge/tiny scale, manager destructor, every API call on line/B-spline/circle/plane/composite/crv-on-surf/other/null; 1500 generated list/proto/transform scripts), 1446 pass bit-identical (max_rel_err 0), 155 deferred (surface.surface_point_solve_p3 56, topology.edge 29, topology.face_p2 26, topology.vertex 25, topology.brep_p5 10, query.poly 9; SmProtoVertex::ContainsPoint wired by query.face_p2); --perturb SENSITIVE. Upstream defects found: UB-98 (SmCopyBrepMap release SM_ASSERT null deref), HANG-12 (DropPoint after zero-length Reparameterize; Rust curve point solve loops too), WR-73 (RemoveCoincidentNoEdgeProtoVertex returns SM_SUCCESS as TRUE, kept for parity). Wired UNPORTED(topology.proto_topology_p1) in topology.topology (SmTopology destructor -> destroy_topology; stays green 657/657) and the topology.edgeuse_p1 co-port of SmOwningTopology::IsKindOf (unchanged 2449 pass/273 deferred). Plan notes: 180 fns; largest: sm_AreOverlapping, SmProtoTopologyManager::CanCombineEdgeDefinitions, SmProtoTopologyManager::RemoveCoincidentNoEdgeProtoVertex, sm_TransformIndependentObject; entry points: SmApiRotate, SmApiScale, SmApiScaleByPt, SmApiTransform, SmApiTranslate; cycle back-edges (call into later units; stub or co-port): query.poly(9), topology.edge(7), query.face_p2(4), blend.fillet_geom_p1(3), topology.edgeuse_p1(3), topology.vertex(3), topology.shell(2), boolean.topology_intersector_p1(1), topology.brep_p1(1), topology.brep_p2(1), topology.brep_p3(1), topology.brep_p4(1), topology.brep_p5(1), topology.face_p4(1), topology.vertexuse(1); unit-SCC #22 (220 units); 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmOwningTopology::IsKindOf -> topology::edgeuse::p1::owning_topology_is_kind_of; CO-PORTED in query.poly (PR #73): SmOwningTopology::IsKindOf -> query::poly::owning_topology_is_kind_of (private); SmOwningTopology PostInsert/Remove/GetAll list semantics are re-implemented for poly lists inside query::poly::PolyBrep (Vec / intrusive edge links), not a general port. Re-run after merging PR #106 (surface.surface_point_solve_p3 wired, 2026-10-15): 1601 cases, 1494 pass, 107 deferred (query.poly 9, topology.brep_p5 10, topology.edge 33, topology.face_p2 28, topology.vertex 27), 0 failed, max_rel_err 0. [PR #114: the proto-script engine (shim, ffi validation, runner) also carries the ops 91-108 of topology.proto_topology_p2; this unit's generator and report are unchanged] PR #144 (2026-10-16): wired this unit's UNPORTED(topology.vertex/topology.edge/query.poly) stubs: sm_AreConnected, SmProtoVertex::GetFinalVertex and sm_FindEDOnDegenFace now walk the B-rep through the ProtoTopology trait (Brep over EntityRef via topology.vertex/vertexuse/edge/edgeuse_p1; sm_FindEDOnDegenFace stops at UNPORTED(topology.face_p4) SmFace::GetEdges for a shared face), SmApiTransform moves poly B-reps through query.poly PolyBrep::transform (transform scripts gain poly B-reps with 1-4 vertices, tobj 9, reporting tolerance and vertices; 36 new edge cases). difftest: cases=1637 passed=1545 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25: all from intersect.point_class / curve_class stubs or SmBrep::Transform), max_rel_err 0; --perturb SENSITIVE. topology.proto_topology_p2 re-run: 1533 cases, 1247 pass, 286 deferred, 0 failed. After merging main with topology.proto_topology_p3 (PR #121): ProtoBrep drops its own vertex_point / edge_faces / edge_other_vertex in favour of those of its supertrait ProtoTopology (one difftest Pool impl, a VertexAt vertex reports its point); p1 unchanged (1637/1545/92), topology.proto_topology_p3 re-run: cases=3013 passed=2638 failed=0 deferred=375 (query.brep 70, topology.edge 95, topology.face_p2 94, topology.vertex 116), --perturb SENSITIVE. Review fixes (PR #144, 2026-10-16): removed a no-op drop (clippy drop_non_drop under --features difftest/cpp), restored the Deviations note that sm_FindEDOnDegenFace is unreachable from CanCombineEdgeDefinitions, added fixtures review_poly_transform.json (216 poly B-rep transforms) and review_final_vertex.json (4 GetFinalVertex / AreConnected / CanCombine scripts at scales 1, 1e8, 1e-8, 1e3): cases=1857 passed=1765 failed=0 deferred=92 (topology.brep_p5 10, topology.edge 28, topology.face_p2 29, topology.vertex 25), max_rel_err 0; --perturb SENSITIVE (1765/1765); p2 1533/1247/286 and p3 3013/2638/375 unchanged, 0 failed. The real-B-rep walks of AreConnected / GetFinalVertex / FindEDOnDegenFace are checked only by the unit test brep_walks (plan/findings.md).; CO-PORTED in topology.shell (PR #154): SmOwningTopology::Remove + PostInsert of a face use on the typed Brep face-use list -> topology::shell::move_faceuse (used by SmShell::TransferChildrenTo and SmTopologyTraverser::FaceuseShellReplacement); SmOwningTopology::Remove of a shell from its region -> topology::shell::region_remove_shell (inline in SmOwningTopology.h; no caller left on main) UPDATE after topology.brep_p2 (PR #149): ProtoTopologyManager::new's tolerance fallback (approx tol below SM_EFF_ZERO) wired to Brep::tolerance and brep::p2::min_edge_bbox_size; new() takes each B-rep as (handle, &Brep) and the runner passes two empty Breps like the shim; 5 edge cases added (approx_tol 0, -1e-3, -1, 1e-13, SM_EFF_ZERO; the empty B-reps let only the tolerance half win, the edge-size half is covered by the unit test manager_tolerance_falls_back_to_the_breps): 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged blockers); p2/p3 and query.face_p2 unchanged. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 1862 cases, 1770 pass, 0 fail, 92 deferred (unchanged). Re-run in PR #149 (review 5578349117 round, merged with main through fb58ea76; topology.brep_p2's UNPORTED stubs wired): 1862 cases, 1770 pass, 0 fail, 92 deferred; the 10 Notify deferrals relabel to topology.brep_p1 GetVertices (8) and topology.brep_p3 TurnToNURBS (2). UPDATE (PR #204, topology.brep_p3): ProtoBrep (topology.proto_topology_p2) gained brep_squeeze_edge; this runner's Pool of bare stand-ins implements it by deferring under harness:proto-pool-brep-edit (no B-rep to edit; a harness gap, documented in the runner header, not a plan unit) and passes &mut Kernel to glue_coincident_brep_vertices; no case reaches it: 1862 cases, 1770 pass, 0 failed, 92 deferred, case-by-case unchanged. UPDATE (PR #204, topology.brep_p3, wiring of topology.brep_p5's TurnToNURBS stub): case by case, the 2 SmBrep::TurnToNURBS deferrals relabel to topology.brep_p1 GetVertices; no outcome change (1862 cases, 1770 pass, 0 fail, 92 deferred). Shim batch 2 (topology stand-ins are records of one B-rep in the runner (TopoRecords; ClassGeometry topology_entity/handle, vertex point, zone tol hooks), point_class FindObjPoint3d resolves vertex/edge (#285 hunk) and face handles through them (stacked on #295)): 1862 cases, 1852 pass, 10 deferred (topology.brep_p1 8, topology.brep_p3 2), --perturb SENSITIVE 1852/1852 (was 1770/92). GREEN (2026-10-23, PR #600): difftest PASS topology.proto_topology_p1 cases=1862 passed=1862 failed=0 deferred=0 max_rel_err=0e0 with_values=1862 no_values=0 vacuous=0; the former 10 topology.proto_topology_p1 deferrals (topology.brep_p1 8, topology.brep_p3 2) no longer occur on main 38b0745b3; local rerun and CI run 39935703236 agree. Constructed-solid public transforms (2026-10-24): 1877 cases, 1877 pass, 0 fail/deferred. Fifteen real box/sphere/caller-keyed face cases compare immediate owning geometry and tolerances for identity, translation, rotation, uniform scale and ScaleByPt; all15 bit-identical. Baseline actual translation failures exposed vertex-only movement. Geometry selection now uses recorded constructor/setter provenance without rewriting owners; supplied notification environments retain existing behavior. API copy-brep direct bindings and shared geometry aliases remain outside this bounded proof. Public copy followed by transform (2026-10-24): 1881 passes, 0 failures/deferred. Four real SmApiBrepCopy box/sphere cases compare immediate geometry after identity/translation plus unchanged source records. Before the producer fix both translation cases failed while identity passed; copied face/edge bindings now record their actual caller owner keys. No owner rewriting or Clone behavior changes. Two Rust controls cover distinct canonical callbacks under separate caller domains and preserve foreign/None owner filtering. Adjacent B-rep p3 853 pass/38 deferred/6 known mismatches; p5 795 pass/2 deferred, no failures. Public solid scale phase two (2026-10-24): full runner 1891 passes, 0 failures/deferred. Ten real constructed/copied box/sphere cases cover positive nonuniform scaling, odd/even/all-negative reflection, ScaleByPt and unchanged zero-axis rejection. Native baseline had 9 failures; all10 now match exactly. Additive kind4 records actual pre/post geometry types and retained pre-operation topology identities for directed edgeuse order/mates/radials/vertices/faces and fixed faceuse shell/region membership. Controlled omission of edgeuse reversal and shell swapping each kills the 3 odd-reflection cases. Source-copy preservation covers sampled geometry/tolerances only; no warmed-cache claim. Public extrusion transforms (2026-10-24): 1898 passes, 0 failures/deferred. Seven real finite quadratic-generator extrusion faces use explicit MakeNurb on both sides before public face creation, retain actual extrusion runtime type and complete geometry/topology records, and cover identity/translation/uniform/anisotropic/signed ScaleByPt plus odd/even reflection. The valid baseline passed 3 controls and failed anisotropic/signed surface samples; adding inherited SurfOfExtrusion to the B-rep conversion helpers fixes all7 exactly. Lazy extrusion face construction remains a separate p6 provider gap documented in findings; no owner-key changes." From e425ec2f27d1b30e778b18fde5c35ab7e445c827 Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 14:12:38 +0200 Subject: [PATCH 024/491] test: retain scale probe lint cleanliness after main import merge Co-Authored-By: OpenAI Codex --- crates/difftest/src/units/topology_proto_topology_p1.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/difftest/src/units/topology_proto_topology_p1.rs b/crates/difftest/src/units/topology_proto_topology_p1.rs index e45ba9d021..10227e3d46 100644 --- a/crates/difftest/src/units/topology_proto_topology_p1.rs +++ b/crates/difftest/src/units/topology_proto_topology_p1.rs @@ -4830,7 +4830,7 @@ struct ScaleProbeFrame { initial_types: Vec, vertices: Vec, edges: Vec, - faces: Vec, + faces: Vec, shells: Vec, regions: Vec, edgeuses: Vec, From 7cca1794e347cb49531d4781873ecb1c6ba039dd Mon Sep 17 00:00:00 2001 From: Thijs Damsma Date: Sat, 24 Oct 2026 15:06:13 +0200 Subject: [PATCH 025/491] wire: trimmed surface cache history (drop the p7 tolerance-growth deferral) The mutable face queries of intersect::curve_class::p7 deferred on `smsurf_GetSurfaceCache (trimmed surface cache history: tolerance growth)` whenever a query raised a vertex or edge tolerance. The surface cache slot now keeps the trimmed cache upstream keeps on the surface, so the query reuses or builds it as upstream does (checked against the C++ with a shim probe and gdb). 230 deferrals: 218 pass, 12 disagree for other reasons and are listed as known mismatches with a findings entry. query.face_p3 green. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RvuXmr7S5RoUXxVohC1giE --- crates/difftest/known_mismatches.toml | 60 +++++++++++++++++ crates/difftest/src/units/topology_face_p1.rs | 3 +- .../solidcore/src/intersect/curve_class/p7.rs | 65 ++---------------- plan/findings.md | 67 +++++++++++++++++++ plan/units/heal.face_props.toml | 2 +- plan/units/heal.heal_data_p1.toml | 2 +- plan/units/intersect.curve_class_p4.toml | 2 +- plan/units/query.face_p3.toml | 4 +- plan/units/query.face_p4.toml | 2 +- plan/units/topology.brep_p1.toml | 2 +- 10 files changed, 142 insertions(+), 67 deletions(-) diff --git a/crates/difftest/known_mismatches.toml b/crates/difftest/known_mismatches.toml index 6accec2276..f0c1c3ab44 100644 --- a/crates/difftest/known_mismatches.toml +++ b/crates/difftest/known_mismatches.toml @@ -310,3 +310,63 @@ why = "merge ray fire on a created operand: primitive surfaces are owned under n unit = "topology.brep_p10" case = "project_t0_Defeature_brep1" why = "ProjectAndTrim removes every face; C++ keeps the former solid region as the infinite region, the port keeps the original one (its SmTopology fields differ: cpp 99999999, rust 0) (plan/findings.md: 'topology.brep_p10 project_t0_Defeature_brep1: the region left after ProjectAndTrim')" + +[[mismatch]] +unit = "heal.face_props" +case = "gen0646" +why = "torus face, edges far off the surface: SetProps_Stage2 seam flags differ after the trimmed cache raised edge tolerances (same tolerances and UV trims both sides; FindVertexSector of the seam disagrees) (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "heal.face_props" +case = "gen0790" +why = "torus face, edges far off the surface: SetProps_Stage2 seam flags differ after the trimmed cache raised edge tolerances (same tolerances and UV trims both sides; FindVertexSector of the seam disagrees) (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "heal.face_props" +case = "gen1312" +why = "torus face, edges far off the surface: V seam intervals 2-5 C++ FACE, port OUTSIDE (FindVertexSector) with identical raised edge tolerances and UV trims (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen1570" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen1622" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen1841" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen1876" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen1968" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "intersect.curve_class_p4" +case = "gen2061" +why = "GetLoopCrossings -> SetProps_Stage2 seam flags differ on a torus face with edges far off the surface after the trimmed cache raised edge tolerances (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "heal.heal_data_p1" +case = "file_Endcap" +why = "Endcap.smb HealTgts: out[215] C++ unset sentinel, port 7.6925e-4; cache creation order matches (gdb), cause not found (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "heal.heal_data_p1" +case = "file_Endcap_rebuild_reverse" +why = "Endcap.smb HealTgts: out[215] C++ unset sentinel, port 7.6925e-4; cache creation order matches (gdb), cause not found (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" + +[[mismatch]] +unit = "topology.brep_p1" +case = "Endcap_heal" +why = "Endcap.smb HealBrep: a healed topology record differs (out[234], out[238]); cache creation order matches (gdb), cause not found (plan/findings.md: 'trimmed surface cache history: tolerance-growth deferral removed')" diff --git a/crates/difftest/src/units/topology_face_p1.rs b/crates/difftest/src/units/topology_face_p1.rs index 048faf4dea..0140420b37 100644 --- a/crates/difftest/src/units/topology_face_p1.rs +++ b/crates/difftest/src/units/topology_face_p1.rs @@ -26,8 +26,7 @@ //! //! Deferred: the cone `TrimWithDomain` callbacks of `ShrinkGeometry` //! (`surface.surface_p2`), and the `MoveSeam` splits past the recomputed seam problem -//! (trimmed surface cache history, `surface.surface_p2`, or merges whose handles the -//! geometry cannot resolve). Rejected: the seam calls the C++ crash trap (below) catches. +//! (merges whose handles the geometry cannot resolve). Rejected: the seam calls the C++ crash trap (below) catches. //! //! **C++ crash trap.** Upstream `SplitAtSeam`'s U-then-V branch recurses on the wrong //! face and crashes (`plan/upstream-issues.md` UB-137). The shim runs every seam call diff --git a/crates/solidcore/src/intersect/curve_class/p7.rs b/crates/solidcore/src/intersect/curve_class/p7.rs index ad6f93f7a4..660ea54260 100644 --- a/crates/solidcore/src/intersect/curve_class/p7.rs +++ b/crates/solidcore/src/intersect/curve_class/p7.rs @@ -77,12 +77,13 @@ //! so their additions are discarded. A handle the adapter cannot resolve keeps the //! [`SolidError::Unported`] dispatch-gap deferral. An edge of another B-rep has no //! use on the face: not inside, as upstream's use walk finds. **Cache history:** a -//! face query on the mutable path that raises a vertex or edge tolerance (a trimmed -//! surface cache built with the face's UV trim curves, `smsurf_GetSurfaceCache`) -//! defers on `surface.surface_p2`: upstream reuses an `SmTrimSrfCache` made earlier -//! without the face (no UV trims, no tolerance growth) when one exists, which the port -//! cannot know (`plan/findings.md`, "classification face queries through -//! `ClassGeometryMut`"). +//! face query on the mutable path may build the face surface's trimmed surface cache +//! with the face (`smsurf_GetSurfaceCache`), whose UV trim curves raise vertex and +//! edge tolerances, or reuse the one the surface already holds (made earlier, with or +//! without the face). The surface's cache slot keeps that history +//! ([`crate::query::face::p2::face_surface_cache`]), so the query does what upstream +//! does and the raised tolerances stay on the B-rep (`plan/findings.md`, "trimmed +//! surface cache history: tolerance-growth deferral removed"). //! - **`SmFace::FindVertexSector`** runs on the mutable path only; the read-only //! path returns [`SolidError::Unported`] (`topology.face_p4`). //! - **`SmFace::PointClassify`** ([`crate::query::face::p2::point_classify`]) runs on @@ -2185,7 +2186,6 @@ impl> FaceQueries for Mutable<'_, G> { uv_space_okay, ); }; - let before = Tolerances::of(b); let r = find_vertex_sector_in( k, b, @@ -2199,7 +2199,6 @@ impl> FaceQueries for Mutable<'_, G> { src_zone_tol_3d, uv_space_okay, )?; - before.check_unchanged(b, "SmFace::FindVertexSector")?; Ok(r) } fn edge_curve_on_classify( @@ -2242,7 +2241,6 @@ impl> FaceQueries for Mutable<'_, G> { face_index, ); }; - let before = Tolerances::of(b); let r = edge_curve_on_classify_in( k, b, @@ -2256,7 +2254,6 @@ impl> FaceQueries for Mutable<'_, G> { uv_space_okay, face_index, )?; - before.check_unchanged(b, "SmFace::EdgeCurveOnClassify")?; Ok(r) } fn point_classify( @@ -2287,7 +2284,6 @@ impl> FaceQueries for Mutable<'_, G> { point_class, ); }; - let before = Tolerances::of(b); let r = qf2::point_classify( k, b, @@ -2296,7 +2292,6 @@ impl> FaceQueries for Mutable<'_, G> { &mut pc, point_classify_options(src_zone_tol_3d, do_xsects, use_uv), ); - before.check_unchanged(b, "SmFace::PointClassify")?; let geometry = &*self.0; let (b, _) = resolve_face(geometry, face).ok_or_else(unresolved_uv)?; *point_class = @@ -2329,7 +2324,6 @@ impl> FaceQueries for Mutable<'_, G> { point_class, ); }; - let before = Tolerances::of(b); let r = qf1::point_3d_classify( k, b, @@ -2340,7 +2334,6 @@ impl> FaceQueries for Mutable<'_, G> { &mut pc, qf1::Point3dClassifyOptions::default(), ); - before.check_unchanged(b, "SmFace::Point3DClassify")?; let geometry = &*self.0; let (b, _) = resolve_face(geometry, face).ok_or_else(unresolved_3d)?; *point_class = @@ -2374,50 +2367,6 @@ impl> FilletAccess for Mutable<'_, G> } } -/// The vertex and edge zone tolerances of a B-rep, taken before a face query of the -/// mutable path. -/// -/// A face query may build the face surface's trimmed surface cache -/// (`smsurf_GetSurfaceCache`), whose `CreateUVTrimCurves` raises the tolerances of -/// edges far from the surface. Upstream does that only when the surface has no -/// trimmed cache yet: an `SmTrimSrfCache` made earlier while the surface had no -/// owner face (no UV trim curves) is reused as is. The port keeps no object caches, -/// so whether upstream grew the tolerances here depends on cache history it cannot -/// know; a query that grew one defers (module Deviations). -struct Tolerances { - vertices: Vec, - edges: Vec, -} - -impl Tolerances { - fn of(b: &Brep) -> Self { - Self { - vertices: b.vertices().iter().map(|(_, v)| v.tolerance).collect(), - edges: b.edges().iter().map(|(_, e)| e.tolerance).collect(), - } - } - - /// `Unported` when face query `query` raised a tolerance of `b`. - fn check_unchanged(&self, b: &Brep, query: &'static str) -> Result<()> { - let after = Self::of(b); - let grew = |before: &[f64], after: &[f64]| { - after - .iter() - .zip(before.iter().chain(core::iter::repeat(&f64::NEG_INFINITY))) - .any(|(a, b)| a > b) - }; - if grew(&self.vertices, &after.vertices) || grew(&self.edges, &after.edges) { - let _ = query; - // UNPORTED(surface.surface_p2): smsurf_GetSurfaceCache object cache history - return Err(SolidError::unported( - "smsurf_GetSurfaceCache (trimmed surface cache history: tolerance growth)", - "surface.surface_p2", - )); - } - Ok(()) - } -} - /// The B-rep and face behind face handle `face` ([`ClassGeometry::topology_entity`]); /// `None` for an adapter that cannot resolve it. fn resolve_face( diff --git a/plan/findings.md b/plan/findings.md index 01dac07906..7e9d7829f0 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18969,3 +18969,70 @@ preserves all four control nets and u knots exactly, and reproduces the last round's complete input and 4125.906 mm net; the other rounds' tiny lower v extension bounds can differ in their last bits (at most 7e-20 in the checked run). The saved JSON inputs fix every bit for differential testing. + +## trimmed surface cache history: tolerance-growth deferral removed (2026-10-24) + +Follows "classification face queries through `ClassGeometryMut`" (#639). The mutable +face queries of `intersect::curve_class::p7` (`FindVertexSector`, +`EdgeCurveOnClassify`, `PointClassify`, `Point3DClassify`) deferred on +`smsurf_GetSurfaceCache (trimmed surface cache history: tolerance growth)` +(`surface.surface_p2`) whenever the query raised a vertex or edge tolerance: 230 cases on +main a5dc2be24 (intersect.curve_class_p4 169, query.face_p3 36, heal.face_props 15, +query.face_p4 5, heal.heal_data_p1 4, topology.brep_p1 1). + +- **The check is obsolete.** It was written when the port kept no object caches. The + surface's cache slot now keeps the trimmed cache upstream keeps on the surface + (`Surface::take_trim_cache` / `store_trim_cache`; a standalone cache made while + `IntersectVertices` / `CreateUVTrimCurve(s)` clear the owner is kept, + `keep_standalone_trim_cache`), so `query::face::p2::face_surface_cache` reuses a + cache made earlier and builds one with the face (UV trim curves, tolerance growth) + only when upstream does. Checked against the C++: + - shim probe (`face->GetSurface()->GetCacheObj()`, edge tolerances after each op) on + heal.face_props gen0010 / gen0646 / gen1312: upstream builds an `SmTrimSrfCache` + with the face (`m_bHaveTSurfaceCache = 1`, `m_bFaceContainmentDone = 1`) in the + first op and raises the edge tolerances (gen1312: 6.01084, 6.02664, 6.83397, + 7.76739); the port's classification snapshot holds exactly those tolerances and + UV trim curves when the next face query runs; + - gdb on the C++ reference (break on `SmTrimSrfCache::SmTrimSrfCache` and + `SmFace::CreateUVTrimCurves`): the first cache of gen0646 / gen1312 is made in + `SmEdgeuse::EvaluateBinormal` <- `SmFace::SectorClassifyCurveOn` <- + `FindVertexSector` <- `ClassifyFaceInterval` <- `CurveOnClassify` <- + `HasSeamProblem`, with one `BuildTree` -> `CreateUVTrimCurves`, the same order as + the port's `face_surface_cache` calls. +- **Removed** the `Tolerances` before/after check in `p7` (`Mutable` face queries). Linux + difftest, before -> after: query.face_p3 1163 pass / 36 deferred -> 1199 pass (green); + intersect.curve_class_p4 3032 / 169 deferred -> 3195 pass, 6 known mismatches; + heal.face_props 1513 / 15 deferred -> 1525 pass, 3 known mismatches; query.face_p4 + 493 / 5 deferred -> 498 pass (2 earlier known mismatches); heal.heal_data_p1 456 / 11 + deferred -> 458 pass, 7 deferred (`primitive.api_primitives`), 2 new known + mismatches; topology.brep_p1 2587 / 15 deferred -> 2587 pass, 14 deferred, 1 new known + mismatch. No other unit changed (Mac `--green` compared with main's Linux reports; + the differences are the documented macOS ones). +- **The 12 remaining disagreements are not cache history** (listed in + `crates/difftest/known_mismatches.toml`): + - heal.face_props gen0646, gen0790, gen1312 and intersect.curve_class_p4 gen1570, + gen1622, gen1841, gen1876, gen1968, gen2061 (all `SmSurfOfRevolution` torus faces + with face tolerance 1000 and line-edge loops far off the surface): the seam + problem flags of `SetProps_Stage2` differ (`m_bBadMissingSeam` / + `m_eBadMissingSeam`, or crossed / near miss). gen1312: both sides classify the V + seam into the same 5 intervals with the same vertex ends, but C++ classifies + intervals 2-5 `SM_PC_FACE` and the port `SM_PC_OUTSIDE`: every + `FindVertexSector` of the V seam returns not inside in the port, run on a B-rep + whose edge tolerances and UV trim curves equal the C++ ones (above). Suspected + cause: `FindVertexSector` / `SectorClassifyCurveOn` (`topology.face_p4`, + `query.face_p3`) for a vertex far (about 6 units) off the surface, within the + raised edge tolerance; not investigated further. + - heal.heal_data_p1 file_Endcap, file_Endcap_rebuild_reverse: `out[215]` C++ + -1.23456e-61 (the runner's unset sentinel), port 7.692503008378702e-4. + topology.brep_p1 Endcap_heal: `out[234]` C++ 1 / port 0, `out[238]` C++ 0 / port 1 + (a healed topology record). gdb: the C++ makes all 8 Endcap trimmed caches in + `SmHealData::HealTgts` -> `Cache_VertexProps` -> `SmVertexuse::ComputeUVPoint` + (global fallback) without `CreateUVTrimCurves`, which the port's + `global_point_solve` -> `face_surface_cache` also does. Cause not found. +- **Not done:** the seam check (`topology::face::p4::has_seam_problem`) reads the + caller's objects (`BrepSeamObjects` over the caller's B-rep) for the V seam and for + its post-classification analysis, while upstream reads the one B-rep the U + classification has changed (raised tolerances, UV trim curves). Routing those reads + through the classification snapshot (tried: a wrapper reading tolerances, trim + curves, gaps and topology from a copy of the snapshot) changed no case in the six + units above, so it is left out. diff --git a/plan/units/heal.face_props.toml b/plan/units/heal.face_props.toml index 92b0b35661..4aaf7d0d05 100644 --- a/plan/units/heal.face_props.toml +++ b/plan/units/heal.face_props.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_trimming_tools", "SM_API_test:TestSmTrimSurfaceWith3 status = "partial" claimed_by = "" claimed_at = "" -notes = "PR #697 (2026-10-24, face surfaces keep their trimmed caches): 1528 cases, 1439 pass, 0 failed, 89 deferred (topology.face_p4 88, intersect.curve_class_p4 1); the 15 cache-history (tolerance growth) deferrals go: IntersectVertices' gap reads now run with the surface owner cleared, so the standalone trimmed cache upstream keeps is made there and reused (8 pass, the rest defer further on at FindVertexSector). PR #639 (2026-10-23): the runner builds the down face use side (SmBrepConstructor), which fixes the seam EDGE-vs-VERTEX snap; difftest 1528 cases, 1431 pass, 0 failed, 97 deferred (topology.face_p4 FindVertexSector 82, surface.surface_p2 trimmed surface cache history 15; was FindVertexSector 69 + EdgeCurveOnClassify 28). PARTIAL: difftest 1528 cases (28 fixture + 1500 generated: SetProps/Gaps/Stage2/Stage3 scripts, SmLoopProps::SetProps of every loop (op 8), seeded state, ReSet, HasProps, HasProblems, IsPoint3dOnPole, loop tree build + FindItem, on one-face B-reps with a plane or SmSurfOfRevolution surface (cylinder, cone with/without apex, sphere, torus; open or closed) owned by the face, a vertex loop or edge loops with holes, face tolerances up to 1e9; 9 fixture cases are FFI-guard regression cases, UB-131 and UB-128), 1314 pass bit-identical (max_rel_err 0), 0 failed, 214 deferred (topology.face_p4 210: SmFace::HasSeamProblem for closed faces in SetProps_Stage2; query.face_p4 4: SmFace::ClassifyLoops in SetProps_Stage3); --perturb 1314/1314 detected. Upstream defects UB-131 (SetProps_Gaps early return makes the Stage2/3 face re-run ineffective) and UB-128 (SetProps_Stage3 on a plane with a degenerate domain and edge loops segfaults in ClassifyLoops; the guard rejects a plane side below 1e-6 when Stage 3 can be reached); the guard rejects the crashing scripts. Snapshots compare the flat corner points too; no generated surface has a nonzero m_lBadFlatCorners (harness limitation, findings.md). Stage 3 is compared only through op 8 (direct Stage 3 cases defer at ClassifyLoops). Added Surface::get_singularities_of_face (face tolerance for the owned surface; other UNPORTED(topology.face_p2) tolerance stubs on surfaces unchanged). Wired: topology.loopuse (PR #102), SmSurface::GetSingularities (surface.surface_p3), SmFace::GetVertices (query.face_p2 co-port). Remaining UNPORTED stubs: topology.face_p4 HasSeamProblem, query.face_p4 ClassifyLoops, intersect.curve_class_p2 CurveClassification::ReSet (dead branch). No UNPORTED(heal.face_props) stubs existed on main. FaceProps is generic over a LoopObject handle; the loop tree is an arena (Looptree). CO-PORTED in heal.face_props (PR #122): SmMixedLinkType, SmTriedLinkType, SmHealerOpType (SmHealProbArray.h) -> heal::face_props::{MixedLinkType, TriedLinkType, HealerOpType} (owner heal.heal_data_p2 extends); SmContainmentType (SmTypes.h) -> heal::face_props::ContainmentType. 25 fns; largest: SmFaceProps::SetProps_Stage3, SmFaceProps::SetProps_Stage2, SmLoopProps::SetProps, SmFaceProps::ReSet; unit-SCC #22 (220 units); 100 upstream test entry points reach this unit PR #137 (intersect.curve_class_p4): the shim's face builder, seed and snapshot moved verbatim into harness/cpp/shim/sc_one_face_brep.h, shared with the intersect.curve_class_p4 shim; the runner's FaceIn, rust_brep, ffi_face, snapshot, seed and gen_face are pub(crate) for that runner, the ffi gained encode_face and pub seed_ok. Re-run: 1528 cases, 1314 pass, 214 deferred (query.face_p4 4, topology.face_p4 210), 0 failed, --perturb SENSITIVE 1314/1314. Merge of query.face_p3 (PR #132): query.face_p4 and query.face_p3 wired, the 4 ClassifyLoops deferrals reach further: 1528 cases, 1317 pass, 211 deferred (topology.face_p4 210, topology.face_p2 1), 0 failed. topology.face_p4: FaceProps::set_props_stage2_with takes the SetProps_Stage2 HasSeamProblem call as a callback (face::p4::has_seam_problem passes itself); set_props_stage2 keeps the stub for callers without classification objects." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: of 15 surface.surface_p2 deferrals 12 pass, 3 are known mismatches (gen0646, gen0790, gen1312: torus seam FindVertexSector). PARTIAL heal.face_props cases=1528 passed=1525 failed=0 deferred=0 max_rel_err=1e0 with_values=1516 no_values=9 vacuous=0 known_mismatch=3. PR #697 (2026-10-24, face surfaces keep their trimmed caches): 1528 cases, 1439 pass, 0 failed, 89 deferred (topology.face_p4 88, intersect.curve_class_p4 1); the 15 cache-history (tolerance growth) deferrals go: IntersectVertices' gap reads now run with the surface owner cleared, so the standalone trimmed cache upstream keeps is made there and reused (8 pass, the rest defer further on at FindVertexSector). PR #639 (2026-10-23): the runner builds the down face use side (SmBrepConstructor), which fixes the seam EDGE-vs-VERTEX snap; difftest 1528 cases, 1431 pass, 0 failed, 97 deferred (topology.face_p4 FindVertexSector 82, surface.surface_p2 trimmed surface cache history 15; was FindVertexSector 69 + EdgeCurveOnClassify 28). PARTIAL: difftest 1528 cases (28 fixture + 1500 generated: SetProps/Gaps/Stage2/Stage3 scripts, SmLoopProps::SetProps of every loop (op 8), seeded state, ReSet, HasProps, HasProblems, IsPoint3dOnPole, loop tree build + FindItem, on one-face B-reps with a plane or SmSurfOfRevolution surface (cylinder, cone with/without apex, sphere, torus; open or closed) owned by the face, a vertex loop or edge loops with holes, face tolerances up to 1e9; 9 fixture cases are FFI-guard regression cases, UB-131 and UB-128), 1314 pass bit-identical (max_rel_err 0), 0 failed, 214 deferred (topology.face_p4 210: SmFace::HasSeamProblem for closed faces in SetProps_Stage2; query.face_p4 4: SmFace::ClassifyLoops in SetProps_Stage3); --perturb 1314/1314 detected. Upstream defects UB-131 (SetProps_Gaps early return makes the Stage2/3 face re-run ineffective) and UB-128 (SetProps_Stage3 on a plane with a degenerate domain and edge loops segfaults in ClassifyLoops; the guard rejects a plane side below 1e-6 when Stage 3 can be reached); the guard rejects the crashing scripts. Snapshots compare the flat corner points too; no generated surface has a nonzero m_lBadFlatCorners (harness limitation, findings.md). Stage 3 is compared only through op 8 (direct Stage 3 cases defer at ClassifyLoops). Added Surface::get_singularities_of_face (face tolerance for the owned surface; other UNPORTED(topology.face_p2) tolerance stubs on surfaces unchanged). Wired: topology.loopuse (PR #102), SmSurface::GetSingularities (surface.surface_p3), SmFace::GetVertices (query.face_p2 co-port). Remaining UNPORTED stubs: topology.face_p4 HasSeamProblem, query.face_p4 ClassifyLoops, intersect.curve_class_p2 CurveClassification::ReSet (dead branch). No UNPORTED(heal.face_props) stubs existed on main. FaceProps is generic over a LoopObject handle; the loop tree is an arena (Looptree). CO-PORTED in heal.face_props (PR #122): SmMixedLinkType, SmTriedLinkType, SmHealerOpType (SmHealProbArray.h) -> heal::face_props::{MixedLinkType, TriedLinkType, HealerOpType} (owner heal.heal_data_p2 extends); SmContainmentType (SmTypes.h) -> heal::face_props::ContainmentType. 25 fns; largest: SmFaceProps::SetProps_Stage3, SmFaceProps::SetProps_Stage2, SmLoopProps::SetProps, SmFaceProps::ReSet; unit-SCC #22 (220 units); 100 upstream test entry points reach this unit PR #137 (intersect.curve_class_p4): the shim's face builder, seed and snapshot moved verbatim into harness/cpp/shim/sc_one_face_brep.h, shared with the intersect.curve_class_p4 shim; the runner's FaceIn, rust_brep, ffi_face, snapshot, seed and gen_face are pub(crate) for that runner, the ffi gained encode_face and pub seed_ok. Re-run: 1528 cases, 1314 pass, 214 deferred (query.face_p4 4, topology.face_p4 210), 0 failed, --perturb SENSITIVE 1314/1314. Merge of query.face_p3 (PR #132): query.face_p4 and query.face_p3 wired, the 4 ClassifyLoops deferrals reach further: 1528 cases, 1317 pass, 211 deferred (topology.face_p4 210, topology.face_p2 1), 0 failed. topology.face_p4: FaceProps::set_props_stage2_with takes the SetProps_Stage2 HasSeamProblem call as a callback (face::p4::has_seam_problem passes itself); set_props_stage2 keeps the stub for callers without classification objects." diff --git a/plan/units/heal.heal_data_p1.toml b/plan/units/heal.heal_data_p1.toml index abf721bfee..6c3214f0e8 100644 --- a/plan/units/heal.heal_data_p1.toml +++ b/plan/units/heal.heal_data_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_closed_sphere_properties", "prog_test:my_test_squeez status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL heal.heal_data_p1 cases=495 passed=367 failed=0 deferred=128 blocked_by=intersect.curve_class_p4(109),primitive.api_primitives(7),surface.surface_p2(4),topology.face_p4(8). PARTIAL (PR #109): difftest 493 cases (173 fixture: 57 in fixtures/heal.heal_data_p1/upstream.json (SM_API_test TestSmHealBrep box 10 through SmApiHealBrep and the HealTgts order, prog_test my_test_squeeze_edge_tracks_removal / my_test_missing_poles_refreshes_all_faces boxes, my_test_closed_sphere_properties spheres, 10 pt_TestFiles .smb B-reps plain and rebuilt+reversed, each with and without the edge/vertex caches), 116 in edge_cases.json (done ops around the Fix_TolSizes / Fix_SplitEdgesAtSeam gates, props without a face, old counts equal/above/tail, not-yet moves, removals, stage re-runs, ReSet with/without new-object lists, HealBrep null/all/target lists/op none, Fix_InfiniteRegion, 1e-4..1e4 boxes, sheet faces, 6 open/closed B-spline files x 4 ops x rebuilt); 320 generated heal scripts on generated B-spline faces, boxes, spheres, files and other generated B-reps, plus SmApiHealBrep null/box), 265 pass (max_rel_err 0, no absolute floors), 0 failed, 228 deferred (topology.face_p2 52: SmFace::GetTolerance stubs in the plane box and surface zone tolerance, reached by stage 2; query.face_p4 62: SmFace::ClassifyLoops; topology.edgeuse_p3 45: p2 gap steps; topology.face_p4 37: SmFace::HasSeamProblem on closed surfaces; topology.face_p3 18: SmFace::CreateUVTrimCurves; topology.brep_p1 12: SmBrep::HealBrep/GetVertices/GetInfiniteRegion; heal.heal_data_p3 2: HealTgts); --perturb 265/265 detected (SmRegion::IsVoid/SetIsVoid wired to topology.shell in PR #154, which moved cases from topology.face_p2 to pass and query.face_p4). Harness: the SmHealData steps run as codes 23..31 of heal.heal_data_p2's heal scripts (shim p1_step, check_heal, p2 runner Step), comparing p2's whole healer dump plus a stage 2/3 face props snapshot; SmApiHealBrep has its own shim. Builds on PR #128 (heal.heal_data_p2, which merges PR #122 heal.face_props): merge those first. No UNPORTED(heal.heal_data_p1) stubs existed. UNPORTED stubs: topology.brep_p1 (SmBrep::GetVertices, GetEdges, GetFaces, GetInfiniteRegion, HealBrep), heal.heal_data_p3 (SmHealData::HealTgts), topology.brep_p6 (SmBrep::TestClosureSplitRegion), topology.brep_p5 (SmBrep::FindAndSetInfiniteRegion), topology.face_p3 (SmFace::CreateUVTrimCurves). 11 fns; largest: SmHealData::Cache_FaceProps_Stage3, SmHealData::Cache_FaceProps_Stage2, SmHealData::Fix_BadSheets, SmHealData::HealBrep; entry points: SmApiHealBrep; cycle back-edges (call into later units; stub or co-port): heal.heal_data_p3(1); unit-SCC #22 (220 units); 59 upstream test entry points reach this unit [topology.edgeuse_p3 merged (PR #117): the p2 gap steps no longer defer: 493 cases, 267 pass, 0 failed, 226 deferred (topology.face_p2 83, query.face_p4 64, topology.face_p4 49, topology.face_p3 18, topology.brep_p1 12)] PR #169 wires TestClosureSplitRegion to topology::brep::p6 with the caller Kernel and persistent Marks; no remaining UNPORTED(topology.brep_p6) sites. Fix_BadSheets uses the split attempt: its bare native call ignores SmStatus but preserves partial outputs, while Rust representation errors and Unported propagate. Re-run in PR #169 after merging main: same 493 cases, 267 pass, 0 failed, 226 deferred UPDATE (PR #145, topology.brep_p1): the five topology.brep_p1 stubs are wired (GetVertices/GetEdges/GetFaces, the region walk from the infinite region via topology::brep::p1::regions, HealBrep); the heal harness lists export carries the infinite-region index (Fix_InfiniteRegion on the boxes compared a B-rep without one before). With #164 (heal.heal_data_p3) merged, HealBrep / SmApiHealBrep run the real heal sequence: 493 cases, 266 pass, 0 failed, 227 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, topology.brep_p2 7 (SmBrep::SetTolerance in Fix_TolSizes), topology.shell 3). After topology.brep_p1 wired its topology.shell calls: 493 cases, 269 pass, 0 failed, 224 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, topology.brep_p2 7). [topology.face_p3 wired (PR #139, CreateUVTrimCurves): the 18 topology.face_p3 deferrals now defer inside CreateUVTrimCurves at SmFace::GetUpwardLoopuses (topology.face_p4); counts: see reports/difftest/heal.heal_data_p1.json] UPDATE after topology.brep_p2 (PR #149) wired SmBrep::SetTolerance: the 7 SmApiHealBrep box cases ran an empty Rust Brep against the C++ SmApiCreateBox box (the Rust side cannot build it), which compared different inputs once SetTolerance stopped deferring; the runner now defers them to primitive.api_primitives (SmApiCreateBox). Re-run 493 cases, 266 pass, 0 failed, 227 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, primitive.api_primitives 7, topology.shell 3), --perturb SENSITIVE 266/266. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 493 cases, 271 pass, 0 fail, 222 deferred (topology.face_p2 84, query.face_p4 64, topology.face_p4 49, topology.face_p3 18, primitive.api_primitives 7). fix_bad_sheets takes the NotifyEnv the region split (topology.brep_p6) notifies through (signature only; PR #149). Owner zone-tol cache (surface/owner-zone-tol): 493 cases, 329 pass, 0 fail, 164 deferred." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: of 4 surface.surface_p2 deferrals 2 pass, 2 are known mismatches (file_Endcap, file_Endcap_rebuild_reverse). PARTIAL heal.heal_data_p1 cases=495 passed=458 failed=0 deferred=7 max_rel_err=1e0 with_values=458 no_values=0 vacuous=0 known_mismatch=30 blocked_by=primitive.api_primitives(7). main CI e33985411 (2026-10-23): PARTIAL heal.heal_data_p1 cases=495 passed=367 failed=0 deferred=128 blocked_by=intersect.curve_class_p4(109),primitive.api_primitives(7),surface.surface_p2(4),topology.face_p4(8). PARTIAL (PR #109): difftest 493 cases (173 fixture: 57 in fixtures/heal.heal_data_p1/upstream.json (SM_API_test TestSmHealBrep box 10 through SmApiHealBrep and the HealTgts order, prog_test my_test_squeeze_edge_tracks_removal / my_test_missing_poles_refreshes_all_faces boxes, my_test_closed_sphere_properties spheres, 10 pt_TestFiles .smb B-reps plain and rebuilt+reversed, each with and without the edge/vertex caches), 116 in edge_cases.json (done ops around the Fix_TolSizes / Fix_SplitEdgesAtSeam gates, props without a face, old counts equal/above/tail, not-yet moves, removals, stage re-runs, ReSet with/without new-object lists, HealBrep null/all/target lists/op none, Fix_InfiniteRegion, 1e-4..1e4 boxes, sheet faces, 6 open/closed B-spline files x 4 ops x rebuilt); 320 generated heal scripts on generated B-spline faces, boxes, spheres, files and other generated B-reps, plus SmApiHealBrep null/box), 265 pass (max_rel_err 0, no absolute floors), 0 failed, 228 deferred (topology.face_p2 52: SmFace::GetTolerance stubs in the plane box and surface zone tolerance, reached by stage 2; query.face_p4 62: SmFace::ClassifyLoops; topology.edgeuse_p3 45: p2 gap steps; topology.face_p4 37: SmFace::HasSeamProblem on closed surfaces; topology.face_p3 18: SmFace::CreateUVTrimCurves; topology.brep_p1 12: SmBrep::HealBrep/GetVertices/GetInfiniteRegion; heal.heal_data_p3 2: HealTgts); --perturb 265/265 detected (SmRegion::IsVoid/SetIsVoid wired to topology.shell in PR #154, which moved cases from topology.face_p2 to pass and query.face_p4). Harness: the SmHealData steps run as codes 23..31 of heal.heal_data_p2's heal scripts (shim p1_step, check_heal, p2 runner Step), comparing p2's whole healer dump plus a stage 2/3 face props snapshot; SmApiHealBrep has its own shim. Builds on PR #128 (heal.heal_data_p2, which merges PR #122 heal.face_props): merge those first. No UNPORTED(heal.heal_data_p1) stubs existed. UNPORTED stubs: topology.brep_p1 (SmBrep::GetVertices, GetEdges, GetFaces, GetInfiniteRegion, HealBrep), heal.heal_data_p3 (SmHealData::HealTgts), topology.brep_p6 (SmBrep::TestClosureSplitRegion), topology.brep_p5 (SmBrep::FindAndSetInfiniteRegion), topology.face_p3 (SmFace::CreateUVTrimCurves). 11 fns; largest: SmHealData::Cache_FaceProps_Stage3, SmHealData::Cache_FaceProps_Stage2, SmHealData::Fix_BadSheets, SmHealData::HealBrep; entry points: SmApiHealBrep; cycle back-edges (call into later units; stub or co-port): heal.heal_data_p3(1); unit-SCC #22 (220 units); 59 upstream test entry points reach this unit [topology.edgeuse_p3 merged (PR #117): the p2 gap steps no longer defer: 493 cases, 267 pass, 0 failed, 226 deferred (topology.face_p2 83, query.face_p4 64, topology.face_p4 49, topology.face_p3 18, topology.brep_p1 12)] PR #169 wires TestClosureSplitRegion to topology::brep::p6 with the caller Kernel and persistent Marks; no remaining UNPORTED(topology.brep_p6) sites. Fix_BadSheets uses the split attempt: its bare native call ignores SmStatus but preserves partial outputs, while Rust representation errors and Unported propagate. Re-run in PR #169 after merging main: same 493 cases, 267 pass, 0 failed, 226 deferred UPDATE (PR #145, topology.brep_p1): the five topology.brep_p1 stubs are wired (GetVertices/GetEdges/GetFaces, the region walk from the infinite region via topology::brep::p1::regions, HealBrep); the heal harness lists export carries the infinite-region index (Fix_InfiniteRegion on the boxes compared a B-rep without one before). With #164 (heal.heal_data_p3) merged, HealBrep / SmApiHealBrep run the real heal sequence: 493 cases, 266 pass, 0 failed, 227 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, topology.brep_p2 7 (SmBrep::SetTolerance in Fix_TolSizes), topology.shell 3). After topology.brep_p1 wired its topology.shell calls: 493 cases, 269 pass, 0 failed, 224 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, topology.brep_p2 7). [topology.face_p3 wired (PR #139, CreateUVTrimCurves): the 18 topology.face_p3 deferrals now defer inside CreateUVTrimCurves at SmFace::GetUpwardLoopuses (topology.face_p4); counts: see reports/difftest/heal.heal_data_p1.json] UPDATE after topology.brep_p2 (PR #149) wired SmBrep::SetTolerance: the 7 SmApiHealBrep box cases ran an empty Rust Brep against the C++ SmApiCreateBox box (the Rust side cannot build it), which compared different inputs once SetTolerance stopped deferring; the runner now defers them to primitive.api_primitives (SmApiCreateBox). Re-run 493 cases, 266 pass, 0 failed, 227 deferred (query.face_p4 62, topology.face_p2 55, topology.edgeuse_p3 45, topology.face_p4 37, topology.face_p3 18, primitive.api_primitives 7, topology.shell 3), --perturb SENSITIVE 266/266. Re-run after merging #145 5b1b28ae (main through e1ae2640) into #149: 493 cases, 271 pass, 0 fail, 222 deferred (topology.face_p2 84, query.face_p4 64, topology.face_p4 49, topology.face_p3 18, primitive.api_primitives 7). fix_bad_sheets takes the NotifyEnv the region split (topology.brep_p6) notifies through (signature only; PR #149). Owner zone-tol cache (surface/owner-zone-tol): 493 cases, 329 pass, 0 fail, 164 deferred." diff --git a/plan/units/intersect.curve_class_p4.toml b/plan/units/intersect.curve_class_p4.toml index e878bb2357..b2e46ee0f2 100644 --- a/plan/units/intersect.curve_class_p4.toml +++ b/plan/units/intersect.curve_class_p4.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_poin status = "partial" claimed_by = "" claimed_at = "" -notes = "main CI e33985411 (2026-10-23): PARTIAL intersect.curve_class_p4 cases=3201 passed=2921 failed=0 deferred=280 blocked_by=topology.face_p4(280). PR #639 (2026-10-23): difftest 3201 cases, 2918 pass, 0 failed, 283 deferred unchanged in count; the 97 EdgeCurveOnClassify deferrals now run and defer on surface.surface_p2 (trimmed surface cache history: tolerance growth, plan/findings.md), FindVertexSector 186. PARTIAL (PR #137): difftest 3201 cases (201 hand-written edge scripts + 3000 generated), 1318 pass bit-identical (max_rel_err 0), 0 failed, 1883 deferred (intersect.curve_class_p2 1014: ReSet in the constructor, ConnectIntervals in every squeeze, GetMinUV/GetMaxUV at an edge end; topology.face_p4 360: SmFace::HasSeamProblem in SetProps_Stage2 of a closed face; topology.face_p2 376: SmFace::GetTolerance in the zone tolerance of the face-owned surface, a surface.surface_p1/_p4 stub reached by the seam sampling; topology.edge 82 and topology.vertex 51: SmEdge::GetCurve / SmEdge::IsConnectedTo / SmVertex::IsConnectedTo in PointClassification::curve_object and is_connected_to, reached by the touch lists); --perturb SENSITIVE 1318/1318. Review round 2 (PR #137): GetPointClassifiesConnectedTo wired to intersect.curve_class_p7 (PR #65 merged: 1076 -> 1318 pass, 2125 -> 1883 deferred). Harness: the shim builds a real one-face B-rep with SmBrepConstructor (shared harness/cpp/shim/sc_one_face_brep.h with heal.face_props: plane, cylinder and sphere faces from SmSurfOfRevolution::CreateCanonical, closed or open, plus the heal.face_props face generator; vertex loops and edge loops of line edges, NURBs made on both sides), rebuilt record by record in Rust (heal_face_props::rust_brep); the topology stand-ins are gone. Compared: 374 passing cases run GetLoopCrossings over real loops up to the touch list (loop -> face, SetProps_Stage2 through the hook or face props preset at stage >= 2a by seeds, the props snapshot, surface, IsClosed/IsDegenerate and the no-crossings return of a degenerate classification curve); 400 passing cases run GetIvlBound_EdgeuseObj on the real face (upward face use, surface, the NULL return of a non-edge end); the squeeze and the constructor compare 0 cases beyond the empty-classification rejection (both defer on intersect.curve_class_p2); every edge end and the touch lists that reach an unported IsConnectedTo defer. Not reachable: a face without surface. Topology members go through the defaulted ClassGeometry::topology_entity / topology_handle hooks (CO-PORTED into intersect.point_class; implemented by topology::face::p2 FaceClassifyObjects and the difftest pool); unresolved handles are InvalidInput. FaceProps::set_props_stage2 InvalidInput (UB-131 null m_pFace) now propagates from GetLoopCrossings, and the FFI guard rejects seeded props that reach it (stage GAPSa/GAPS without field 24). Stubs: UNPORTED(intersect.curve_class_p2) (PR #85, still open); none left for intersect.curve_class_p7 (wired); SmEdgeuse::EvaluateBinormal wired to topology.edgeuse_p3 (PR #117) after merging main: it runs on a copy of the B-rep (bUse3DOnly TRUE creates no UV trim curve); counts unchanged, 1318 pass / 1883 deferred, --perturb SENSITIVE 1318/1318; the status of both EvaluateBinormal call sites is ignored as upstream (a failure leaves the out-vectors as C++ does: point (0,0,0) after the output init, untouched for a non-loop edge use; only Unported and a null-dereference InvalidInput propagate), and the first site keeps its out-vectors across the 10 samples as upstream declares them outside the loop (no difftest case reaches a failing EvaluateBinormal yet: counts unchanged). Wired: topology::face::p2 (constructor x3, CurveOnClassify of query.face_p2; GetPointsInFace site passes dTol, no borrowed data), intersect::topology_solver::p2 (constructor, zone_tol3d of the solver tolerance; its CurveOnClassify stays justified: the solver adapter has no face -> Brep resolution), intersect::curve_class::p6 (sm_SqueezeOutSmallestInterval), topology::proto_topology::p3 (constructor); crv_in_volume / crv_on_surf constructor stubs retagged UNPORTED(intersect.curve_class_p2): SmCurveClassification::ReSet (no Kernel under Curve::knots; plan/findings.md). CO-PORTED in blend.hcr (PR #191): SmCurveClassification::SmCurveClassification(cpCurve, crInterval, NULL, tol, n, pData) with its ReSet (new curve) -> blend::hcr::new_curve_classification (on CurveClassification::with_curve_interval, SM_UNINIT_TOL for a non-positive tolerance) (kept beside the PR #137 constructor: blend.hcr builds it on CurveClassification::with_curve_interval and does not defer on ReSet). Wired to intersect.curve_class_p2 (wire/curve-class-p4): ReSet in the constructor, ConnectIntervals in sm_SqueezeOutSmallestInterval and SmCurveInterval::GetMinUV / GetMaxUV in GetIvlBound_EdgeuseObj (which now takes the Kernel, as does topology::face::p2::review_start_and_stop_on_seam_crv_classifies); no UNPORTED(intersect.curve_class_p2) stub is left here. difftest 3201 cases: 2224 pass (was 1319), 0 failed, 977 deferred (topology.face_p2 566, topology.face_p4 411); max_rel_err 0. New shim guard: GetLoopCrossings over loops on an empty classification (left by squeezing its only interval) is rejected, since GetPointClassifiesConnectedTo reads m_pData[0] (Rust: InvalidInput). Callers that now get past the constructor: heal.heal_data_p2 469 -> 483 pass, heal.heal_data_p3 329 -> 344, heal.stitch_p2 332 -> 337, topology.face_p3 823 -> 888; topology.brep_p2's merge_plane10_line now reaches CurveOnClassify, whose topology its MergeObjects adapter cannot resolve (merge_curves_on_surface holds the B-rep mutably), and defers as topology.brep_p2. The shared .smb import of the difftest runners (topology_edgeuse_p1::import) now sets each face's m_pFU (its SM_OT_SAME face use; the export does not carry it): without it topology.shell's region-order cases on Mouse.smb / box_with_large_fillet.smb reached CurveOnClassify through IsInnerShell with no upward face use and failed; they now defer deeper (crv_on_surf, topology.edge). Full sweep after merging main (7eb93aa4): 238 units, 0 failed. Owner zone-tol cache (surface/owner-zone-tol): 3201 cases, 1740 pass, 0 fail, 1461 deferred. UPDATE (closed revolution UV trim curves in pool build): difftest 3201 cases: 2900 pass (was 1740/2224), 0 failed, 301 deferred (query.face_p1 25, topology.face_p4 276); max_rel_err 0e0; upstream C++ SmTrimSrfCache creates UV trim curves on closed/periodic revolution faces during stage 2 props, expanding vertex tolerances." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: of 169 surface.surface_p2 deferrals 163 pass, 6 are known mismatches (gen1570, gen1622, gen1841, gen1876, gen1968, gen2061: torus seam flags). PARTIAL intersect.curve_class_p4 cases=3201 passed=3195 failed=0 deferred=0 max_rel_err=1e0 with_values=3195 no_values=0 vacuous=0 known_mismatch=6. main CI e33985411 (2026-10-23): PARTIAL intersect.curve_class_p4 cases=3201 passed=2921 failed=0 deferred=280 blocked_by=topology.face_p4(280). PR #639 (2026-10-23): difftest 3201 cases, 2918 pass, 0 failed, 283 deferred unchanged in count; the 97 EdgeCurveOnClassify deferrals now run and defer on surface.surface_p2 (trimmed surface cache history: tolerance growth, plan/findings.md), FindVertexSector 186. PARTIAL (PR #137): difftest 3201 cases (201 hand-written edge scripts + 3000 generated), 1318 pass bit-identical (max_rel_err 0), 0 failed, 1883 deferred (intersect.curve_class_p2 1014: ReSet in the constructor, ConnectIntervals in every squeeze, GetMinUV/GetMaxUV at an edge end; topology.face_p4 360: SmFace::HasSeamProblem in SetProps_Stage2 of a closed face; topology.face_p2 376: SmFace::GetTolerance in the zone tolerance of the face-owned surface, a surface.surface_p1/_p4 stub reached by the seam sampling; topology.edge 82 and topology.vertex 51: SmEdge::GetCurve / SmEdge::IsConnectedTo / SmVertex::IsConnectedTo in PointClassification::curve_object and is_connected_to, reached by the touch lists); --perturb SENSITIVE 1318/1318. Review round 2 (PR #137): GetPointClassifiesConnectedTo wired to intersect.curve_class_p7 (PR #65 merged: 1076 -> 1318 pass, 2125 -> 1883 deferred). Harness: the shim builds a real one-face B-rep with SmBrepConstructor (shared harness/cpp/shim/sc_one_face_brep.h with heal.face_props: plane, cylinder and sphere faces from SmSurfOfRevolution::CreateCanonical, closed or open, plus the heal.face_props face generator; vertex loops and edge loops of line edges, NURBs made on both sides), rebuilt record by record in Rust (heal_face_props::rust_brep); the topology stand-ins are gone. Compared: 374 passing cases run GetLoopCrossings over real loops up to the touch list (loop -> face, SetProps_Stage2 through the hook or face props preset at stage >= 2a by seeds, the props snapshot, surface, IsClosed/IsDegenerate and the no-crossings return of a degenerate classification curve); 400 passing cases run GetIvlBound_EdgeuseObj on the real face (upward face use, surface, the NULL return of a non-edge end); the squeeze and the constructor compare 0 cases beyond the empty-classification rejection (both defer on intersect.curve_class_p2); every edge end and the touch lists that reach an unported IsConnectedTo defer. Not reachable: a face without surface. Topology members go through the defaulted ClassGeometry::topology_entity / topology_handle hooks (CO-PORTED into intersect.point_class; implemented by topology::face::p2 FaceClassifyObjects and the difftest pool); unresolved handles are InvalidInput. FaceProps::set_props_stage2 InvalidInput (UB-131 null m_pFace) now propagates from GetLoopCrossings, and the FFI guard rejects seeded props that reach it (stage GAPSa/GAPS without field 24). Stubs: UNPORTED(intersect.curve_class_p2) (PR #85, still open); none left for intersect.curve_class_p7 (wired); SmEdgeuse::EvaluateBinormal wired to topology.edgeuse_p3 (PR #117) after merging main: it runs on a copy of the B-rep (bUse3DOnly TRUE creates no UV trim curve); counts unchanged, 1318 pass / 1883 deferred, --perturb SENSITIVE 1318/1318; the status of both EvaluateBinormal call sites is ignored as upstream (a failure leaves the out-vectors as C++ does: point (0,0,0) after the output init, untouched for a non-loop edge use; only Unported and a null-dereference InvalidInput propagate), and the first site keeps its out-vectors across the 10 samples as upstream declares them outside the loop (no difftest case reaches a failing EvaluateBinormal yet: counts unchanged). Wired: topology::face::p2 (constructor x3, CurveOnClassify of query.face_p2; GetPointsInFace site passes dTol, no borrowed data), intersect::topology_solver::p2 (constructor, zone_tol3d of the solver tolerance; its CurveOnClassify stays justified: the solver adapter has no face -> Brep resolution), intersect::curve_class::p6 (sm_SqueezeOutSmallestInterval), topology::proto_topology::p3 (constructor); crv_in_volume / crv_on_surf constructor stubs retagged UNPORTED(intersect.curve_class_p2): SmCurveClassification::ReSet (no Kernel under Curve::knots; plan/findings.md). CO-PORTED in blend.hcr (PR #191): SmCurveClassification::SmCurveClassification(cpCurve, crInterval, NULL, tol, n, pData) with its ReSet (new curve) -> blend::hcr::new_curve_classification (on CurveClassification::with_curve_interval, SM_UNINIT_TOL for a non-positive tolerance) (kept beside the PR #137 constructor: blend.hcr builds it on CurveClassification::with_curve_interval and does not defer on ReSet). Wired to intersect.curve_class_p2 (wire/curve-class-p4): ReSet in the constructor, ConnectIntervals in sm_SqueezeOutSmallestInterval and SmCurveInterval::GetMinUV / GetMaxUV in GetIvlBound_EdgeuseObj (which now takes the Kernel, as does topology::face::p2::review_start_and_stop_on_seam_crv_classifies); no UNPORTED(intersect.curve_class_p2) stub is left here. difftest 3201 cases: 2224 pass (was 1319), 0 failed, 977 deferred (topology.face_p2 566, topology.face_p4 411); max_rel_err 0. New shim guard: GetLoopCrossings over loops on an empty classification (left by squeezing its only interval) is rejected, since GetPointClassifiesConnectedTo reads m_pData[0] (Rust: InvalidInput). Callers that now get past the constructor: heal.heal_data_p2 469 -> 483 pass, heal.heal_data_p3 329 -> 344, heal.stitch_p2 332 -> 337, topology.face_p3 823 -> 888; topology.brep_p2's merge_plane10_line now reaches CurveOnClassify, whose topology its MergeObjects adapter cannot resolve (merge_curves_on_surface holds the B-rep mutably), and defers as topology.brep_p2. The shared .smb import of the difftest runners (topology_edgeuse_p1::import) now sets each face's m_pFU (its SM_OT_SAME face use; the export does not carry it): without it topology.shell's region-order cases on Mouse.smb / box_with_large_fillet.smb reached CurveOnClassify through IsInnerShell with no upward face use and failed; they now defer deeper (crv_on_surf, topology.edge). Full sweep after merging main (7eb93aa4): 238 units, 0 failed. Owner zone-tol cache (surface/owner-zone-tol): 3201 cases, 1740 pass, 0 fail, 1461 deferred. UPDATE (closed revolution UV trim curves in pool build): difftest 3201 cases: 2900 pass (was 1740/2224), 0 failed, 301 deferred (query.face_p1 25, topology.face_p4 276); max_rel_err 0e0; upstream C++ SmTrimSrfCache creates UV trim curves on closed/periodic revolution faces during stage 2 props, expanding vertex tolerances." diff --git a/plan/units/query.face_p3.toml b/plan/units/query.face_p3.toml index 7fd3d31090..814c01d1ec 100644 --- a/plan/units/query.face_p3.toml +++ b/plan/units/query.face_p3.toml @@ -6,7 +6,7 @@ soft_depends_on = ["intersect.curve_class_p1", "intersect.curve_class_p2", "inte est_lines = 1620 layer = 44 tests = ["prog_test:my_test_surface_closest_point", "prog_test:my_test_drop_point", "prog_test:my_test_matrix", "prog_test:my_test_reversed_line_join", "SM_API_test:TestSmCreateRectangle", "SM_API_test:TestSmCreateRegularPolygon"] -status = "partial" +status = "green" claimed_by = "" claimed_at = "" -notes = "PR #639 (2026-10-23, WithMadeTrimCurves pass-through + mutable face queries in ClassifyFaceInterval): difftest 1199 cases, 1087 pass, 0 failed, 112 deferred (was 1000 / 199): query.face_p1 EdgeCurveOnClassify 110 and FindVertexSector 10 resolve or relabel; topology.edge 42, query.face_p2 PointClassify 37, surface.surface_p2 22 (trimmed surface cache history: a face query raising a tolerance, plan/findings.md). PARTIAL (PR #132; review fixes 2026-10-19): difftest 1199 cases (201 fixture: CalculateTightBoundingBox / CalculateAnInternalPoint / the loop bisector over every face of the 13 upstream TestFiles .smb files of unit topology.edgeuse_p1 (with and without RebuildUVTrimCurves), SmApiFaceBoundingBox(TRUE) planar faces (TestSmBrep.cpp:3023) and the holed-box top face of TestSmIntersectors.cpp:405 flat and tilted, SectorClassifyCurveOn on plane_square.smb at every edge use (edge interior, vertex with the CW neighbour, radial; 3D lines in, out and reversed, UV lines, singular); 398 review adversarial fixtures (fixtures/query.face_p3/review_adversarial.json: plane and flat B-spline faces, sector sweeps over the .smb files); 600 generated: SmPlane faces at scales 1e-3..1e3, B-spline faces (flat and curved, degrees 1..3) with rectangle / circle trims and holes, CreateFaceFromSurface faces, .smb files, real SmCylinder/SmSphere/SmTorus/SmCone faces; sector edge fractions 0/1/1e-6, tolerances 0..1e-3, uv_space_okay on/off), 523 pass (288 compare values, 235 both inexact_geometry, rejected or the same status), 0 failed, 676 deferred (topology.face_p2 305: SmFace::GetTolerance of a face-owned surface in the global solves of CalculateTightBoundingBox and DropPoint of the bisector; intersect.surface_drop_lib_p3 215 and topology.face_p3 55: SmSurface::DropCurve / SmFace::CreateUVTrimCurves reached through SmEdgeuse::EvaluateBinormal and, for CalculateAnInternalPoint, CurveOnClassify; curve.crv_on_surf 99: SmCrvOnSurf over an SmPlane; query.face_p2 2); max_rel_err 0; --perturb 523/523 detected. Per function, values are compared only for SmFace::CalculateTightBoundingBox (192 cases) and SmFace::SectorClassifyCurveOn (96); sm_FaceClassifyUVLine (through CalculateAnInternalPoint, 97 cases) and sms_FindAndClassifyLoopBisector (154 cases) never compare values: every case defers or both sides return the same status. sm_FaceClassifyUVLine is file-static: compared through SmFace::CalculateAnInternalPoint. Builds on PR #129 (query.face_p4, with #122 heal.face_props and #85 intersect.curve_class_p2) merged into the branch; the shim compiles the query.face_p4 shim's B-rep builders into its translation unit. Wired UNPORTED(query.face_p3): sms_FindAndClassifyLoopBisector in query::face::p4 (classify_loops returns InvalidInput for a handle type that cannot name the bisector's curves; the library's own healer handle topology::brep::p1::HealLoopHandle is now an enum that names them, as are the query.face_p4 and heal.face_props runner handles), sm_FaceClassifyUVLine in topology::face::p2::calculate_an_internal_point, SmFace::CalculateTightBoundingBox in query::api_queries::face_bounding_box. The bisector's seam flags are written through find_and_classify_loop_bisector_into (upstream's reference out-params: initialised after the domain check, kept when it fails later, read by ClassifyLoops). Also wired: SmEdgeuse::EvaluateBinormal (topology.edgeuse_p3); SmCurveClassification::ReSet in query::face::p2 and heal::face_props came from main (PR #85). LoopObject gained from_face/from_face_surface/from_classify_curve (default None). FFI guard: the tight op rejects B-rep values beyond 1e6 (TIGHT_MAX_COORD; upstream's global solves take minutes at 1e10..1e300, not undefined). Not compared: the shim's sector orientations are always eu1 SAME / eu2 OPPOSITE (the OPPOSITE tangent flip and PART 2's eu2/mate vertex use choices are not reached), and the tight op's has_trim_cache output is not asserted. Upstream SmTArray::FindElementInMaskedArray tests `mask & (1 GetVertexFaceGap, B-rep snapshot); the 75 bisector cases that failed without them now defer on the SmCrvOnSurf knots stub (intersect.curve_class_p2 ReSet, 75); plan/findings.md." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: GREEN: the 36 surface.surface_p2 deferrals pass. PASS query.face_p3 cases=1199 passed=1199 failed=0 deferred=0 max_rel_err=7.784285654409864e-11 with_values=964 no_values=235 vacuous=0. PR #639 (2026-10-23, WithMadeTrimCurves pass-through + mutable face queries in ClassifyFaceInterval): difftest 1199 cases, 1087 pass, 0 failed, 112 deferred (was 1000 / 199): query.face_p1 EdgeCurveOnClassify 110 and FindVertexSector 10 resolve or relabel; topology.edge 42, query.face_p2 PointClassify 37, surface.surface_p2 22 (trimmed surface cache history: a face query raising a tolerance, plan/findings.md). PARTIAL (PR #132; review fixes 2026-10-19): difftest 1199 cases (201 fixture: CalculateTightBoundingBox / CalculateAnInternalPoint / the loop bisector over every face of the 13 upstream TestFiles .smb files of unit topology.edgeuse_p1 (with and without RebuildUVTrimCurves), SmApiFaceBoundingBox(TRUE) planar faces (TestSmBrep.cpp:3023) and the holed-box top face of TestSmIntersectors.cpp:405 flat and tilted, SectorClassifyCurveOn on plane_square.smb at every edge use (edge interior, vertex with the CW neighbour, radial; 3D lines in, out and reversed, UV lines, singular); 398 review adversarial fixtures (fixtures/query.face_p3/review_adversarial.json: plane and flat B-spline faces, sector sweeps over the .smb files); 600 generated: SmPlane faces at scales 1e-3..1e3, B-spline faces (flat and curved, degrees 1..3) with rectangle / circle trims and holes, CreateFaceFromSurface faces, .smb files, real SmCylinder/SmSphere/SmTorus/SmCone faces; sector edge fractions 0/1/1e-6, tolerances 0..1e-3, uv_space_okay on/off), 523 pass (288 compare values, 235 both inexact_geometry, rejected or the same status), 0 failed, 676 deferred (topology.face_p2 305: SmFace::GetTolerance of a face-owned surface in the global solves of CalculateTightBoundingBox and DropPoint of the bisector; intersect.surface_drop_lib_p3 215 and topology.face_p3 55: SmSurface::DropCurve / SmFace::CreateUVTrimCurves reached through SmEdgeuse::EvaluateBinormal and, for CalculateAnInternalPoint, CurveOnClassify; curve.crv_on_surf 99: SmCrvOnSurf over an SmPlane; query.face_p2 2); max_rel_err 0; --perturb 523/523 detected. Per function, values are compared only for SmFace::CalculateTightBoundingBox (192 cases) and SmFace::SectorClassifyCurveOn (96); sm_FaceClassifyUVLine (through CalculateAnInternalPoint, 97 cases) and sms_FindAndClassifyLoopBisector (154 cases) never compare values: every case defers or both sides return the same status. sm_FaceClassifyUVLine is file-static: compared through SmFace::CalculateAnInternalPoint. Builds on PR #129 (query.face_p4, with #122 heal.face_props and #85 intersect.curve_class_p2) merged into the branch; the shim compiles the query.face_p4 shim's B-rep builders into its translation unit. Wired UNPORTED(query.face_p3): sms_FindAndClassifyLoopBisector in query::face::p4 (classify_loops returns InvalidInput for a handle type that cannot name the bisector's curves; the library's own healer handle topology::brep::p1::HealLoopHandle is now an enum that names them, as are the query.face_p4 and heal.face_props runner handles), sm_FaceClassifyUVLine in topology::face::p2::calculate_an_internal_point, SmFace::CalculateTightBoundingBox in query::api_queries::face_bounding_box. The bisector's seam flags are written through find_and_classify_loop_bisector_into (upstream's reference out-params: initialised after the domain check, kept when it fails later, read by ClassifyLoops). Also wired: SmEdgeuse::EvaluateBinormal (topology.edgeuse_p3); SmCurveClassification::ReSet in query::face::p2 and heal::face_props came from main (PR #85). LoopObject gained from_face/from_face_surface/from_classify_curve (default None). FFI guard: the tight op rejects B-rep values beyond 1e6 (TIGHT_MAX_COORD; upstream's global solves take minutes at 1e10..1e300, not undefined). Not compared: the shim's sector orientations are always eu1 SAME / eu2 OPPOSITE (the OPPOSITE tangent flip and PART 2's eu2/mate vertex use choices are not reached), and the tight op's has_trim_cache output is not asserted. Upstream SmTArray::FindElementInMaskedArray tests `mask & (1 GetVertexFaceGap, B-rep snapshot); the 75 bisector cases that failed without them now defer on the SmCrvOnSurf knots stub (intersect.curve_class_p2 ReSet, 75); plan/findings.md." diff --git a/plan/units/query.face_p4.toml b/plan/units/query.face_p4.toml index 93d982fbf3..049440ee8c 100644 --- a/plan/units/query.face_p4.toml +++ b/plan/units/query.face_p4.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_closed_sphere_properties", "prog_test:my_test_squeez status = "partial" claimed_by = "" claimed_at = "" -notes = "PARTIAL: difftest 459 cases (59 fixture + 400 generated), 359 pass (332 compare classifications, 27 both rejected or unbuilt), 0 failed, 100 deferred (topology.face_p2 100: SmFace::GetTolerance of the face-owned surface; re-run in PR #132 after merging main with SmEdgeuse::EvaluateBinormal wired; earlier 358 pass / 101 deferred with topology.edgeuse_p3 49, before that query.face_p3 86); max_rel_err 0; --perturb detected all perturbed cases. The shim builds real SmCylinder/SmSphere/SmTorus/SmCone faces with MakeFaceWithCurves (no-area loops of missing-seam faces: cylinder circles, sphere caps and bands with and without pole vertex loops, torus bands and diagonals, seam-straddling area loops nested up to 3 deep; scales 1e-3..1e3, swapped UV, near-pole and near-coincident loops), natural faces (my_test_closed_sphere_properties), B-spline/plane faces and .smb files; the face props are built as SetProps_Stage3 builds them and exported to the Rust side (both run ClassifyLoops on the same props). Builds on PR #122 (heal.face_props: SmFaceProps, SmLooptreeItem, SmClassifyLoopIO, ClassifyLoopsOut) and PR #85 (intersect.curve_class_p2: SmCurveClassification::ReSet), both merged into the branch. Wired UNPORTED(query.face_p4) in heal::face_props::set_props_stage3 (status ignored as upstream). Stubs: UNPORTED(intersect.curve_class_p4) SmCurveClassification::GetLoopCrossings (UNPORTED(query.face_p3) sms_FindAndClassifyLoopBisector wired by PR #132; the runner's handles became an enum H that can name the bisector's curves). LoopObject gained as_loop/as_vertex; LoopuseType gained Zero (SmTArray::SetSize zero-fills new SmClassifyLoopIO records). 1 fns; largest: SmFace::ClassifyLoops; cycle back-edges (call into later units; stub or co-port): heal.face_props(7); unit-SCC #22 (220 units); 59 upstream test entry points reach this unit Review fixes (PR #129, 2026-10-20): the shim starts ClassifyLoops' out-params from SM_HNF_NO_LOOP_TREE_ROOT/SM_CD_LAMINA as the runner does (the SM_ERR no-loops early return failed the gate before); +41 adversarial fixture cases (fixtures/query.face_p4/adversarial.json); classify_loops borrows the done touch list. After merging main: 500 cases, 395 pass, 0 failed, 105 deferred (topology.face_p2 105), max_rel_err 0. GetLoopCrossings wired (PR #717): the stale UNPORTED(intersect.curve_class_p4) stub is replaced by the real call and the runner exports the props the bisector reads (zone tolerance, no-vertex poles, pole points, missing seam direction); 500 cases, 481 pass (454 compare classifications, 27 both rejected or unbuilt), 0 failed, 17 deferred (topology.face_p4 14: SmFace::FindVertexSector; surface.surface_p2 3: trimmed surface cache tolerance growth), 2 known mismatches (gen0157, gen0300: importer curve approximation at scale 1e-3, plan/findings.md)." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: the 5 surface.surface_p2 deferrals pass (2 earlier known mismatches keep it partial). PARTIAL query.face_p4 cases=500 passed=498 failed=0 deferred=0 max_rel_err=0e0 with_values=471 no_values=27 vacuous=0 known_mismatch=2. PARTIAL: difftest 459 cases (59 fixture + 400 generated), 359 pass (332 compare classifications, 27 both rejected or unbuilt), 0 failed, 100 deferred (topology.face_p2 100: SmFace::GetTolerance of the face-owned surface; re-run in PR #132 after merging main with SmEdgeuse::EvaluateBinormal wired; earlier 358 pass / 101 deferred with topology.edgeuse_p3 49, before that query.face_p3 86); max_rel_err 0; --perturb detected all perturbed cases. The shim builds real SmCylinder/SmSphere/SmTorus/SmCone faces with MakeFaceWithCurves (no-area loops of missing-seam faces: cylinder circles, sphere caps and bands with and without pole vertex loops, torus bands and diagonals, seam-straddling area loops nested up to 3 deep; scales 1e-3..1e3, swapped UV, near-pole and near-coincident loops), natural faces (my_test_closed_sphere_properties), B-spline/plane faces and .smb files; the face props are built as SetProps_Stage3 builds them and exported to the Rust side (both run ClassifyLoops on the same props). Builds on PR #122 (heal.face_props: SmFaceProps, SmLooptreeItem, SmClassifyLoopIO, ClassifyLoopsOut) and PR #85 (intersect.curve_class_p2: SmCurveClassification::ReSet), both merged into the branch. Wired UNPORTED(query.face_p4) in heal::face_props::set_props_stage3 (status ignored as upstream). Stubs: UNPORTED(intersect.curve_class_p4) SmCurveClassification::GetLoopCrossings (UNPORTED(query.face_p3) sms_FindAndClassifyLoopBisector wired by PR #132; the runner's handles became an enum H that can name the bisector's curves). LoopObject gained as_loop/as_vertex; LoopuseType gained Zero (SmTArray::SetSize zero-fills new SmClassifyLoopIO records). 1 fns; largest: SmFace::ClassifyLoops; cycle back-edges (call into later units; stub or co-port): heal.face_props(7); unit-SCC #22 (220 units); 59 upstream test entry points reach this unit Review fixes (PR #129, 2026-10-20): the shim starts ClassifyLoops' out-params from SM_HNF_NO_LOOP_TREE_ROOT/SM_CD_LAMINA as the runner does (the SM_ERR no-loops early return failed the gate before); +41 adversarial fixture cases (fixtures/query.face_p4/adversarial.json); classify_loops borrows the done touch list. After merging main: 500 cases, 395 pass, 0 failed, 105 deferred (topology.face_p2 105), max_rel_err 0. GetLoopCrossings wired (PR #717): the stale UNPORTED(intersect.curve_class_p4) stub is replaced by the real call and the runner exports the props the bisector reads (zone tolerance, no-vertex poles, pole points, missing seam direction); 500 cases, 481 pass (454 compare classifications, 27 both rejected or unbuilt), 0 failed, 17 deferred (topology.face_p4 14: SmFace::FindVertexSector; surface.surface_p2 3: trimmed surface cache tolerance growth), 2 known mismatches (gen0157, gen0300: importer curve approximation at scale 1e-3, plan/findings.md)." diff --git a/plan/units/topology.brep_p1.toml b/plan/units/topology.brep_p1.toml index 9f28861746..879ba903c7 100644 --- a/plan/units/topology.brep_p1.toml +++ b/plan/units/topology.brep_p1.toml @@ -9,4 +9,4 @@ tests = ["prog_test:my_test_extent3d", "prog_test:my_test_vector2d", "prog_test: status = "partial" claimed_by = "bluecmd (Claude Fable 5.1; PR #145)" claimed_at = "2026-10-16" -notes = "main CI e33985411 (2026-10-23): PARTIAL topology.brep_p1 cases=2627 passed=2461 failed=0 deferred=150 rejected=16 blocked_by=intersect.curve_class_p4(12),nurbs.b_spline_surface_n_lib_p2(8),surface.surface_p2(1),topology.edge(10),topology.face_p2(117),topology.face_p4(2). EXACT SmSurfOfRevolution EXPORT (PR #304, shared graph exports carry revolution faces exactly; refreshed on main ca3bbd2b, 2026-10-21): difftest 2627 cases, 2207 pass, 0 failed, 404 deferred, 16 rejected (harness:analytic-geometry-export 131, topology.face_p2 94, query.brep 71, geom.tol 64, topology.face_p3 21, intersect.surface_drop_lib_p3 10, curve.crv_on_surf 8, nurbs.b_spline_surface_n_lib_p2 5), max_rel_err 0; 2146 -> 2207 pass; the revolution harness deferrals became passes and deferrals on real stubs; harness label 228 -> 131 (extrusion, sphere, cone/cylinder). Earlier: PARTIAL (PR #145, follow-up after its review): difftest 2627 cases (2335 fixtures on the 15 upstream .smb files, 276 on constructed B-reps, 16 generated), 2142 pass bit-identical, of which 2097 compare values (5 of them, CreateCornerBlend on three edge uses, only the C++ failure status and an empty surface list) and 45 are guard agreements, not coverage (the C++ is not run: the shim stops before an upstream null dereference; 30 are the port's matching refusal: 27 UB-162 redistribute, 3 SWAP without an old infinite region; 15 AddAttributes on a null indexed target are a runner-side mirror with no port call), 16 rejected (15 ops naming an entity kind the wire B-reps lack, 1 HANG-39 corner blend: nothing runs on either side), 0 failed, 469 deferred, max_rel_err 0, --perturb SENSITIVE 2142/2142; --fuzz 2000 (see Round 7); a 1000-seed generated sweep (3611 cases) passes 2881, 0 failed, 714 deferred, 16 rejected (2805 compare values; the other 76 are the same guard agreements); green 206 units (main 89dc103a's units + this unit), 0 failed. All 50 fns have Rust bodies (sm_RedistributeSeamUVTrimCurves and BrepIntersect fully, stubs only at the callee sites); calls into topology.face_p3 (#139) and query.brep are UNPORTED stubs (topology.shell #154 is wired since Round 5, topology.edgeuse_p3 #117 since Round 6) (topology.face_p1's SwapUV is called since #148 merged: sm_SwapFaceUV calls topology.face_p1's SmFace::SwapUV (#148, wired after the merge; status ignored as upstream's release SE, Unported still propagates): on an unmarked face the shim now runs the C++ swap and both sides compare the swapped B-rep digest (the heal digest: surfaces, trim-curve fingerprints, loop-use links), but every such case defers at SmFace::GetBrep (topology.face_p4), the first call of face_p1's swap_uv, so none compares yet (the 16 former topology.face_p1 deferrals are now topology.face_p4 16; no deferral flipped to pass).); HealBrep runs the heal sequence of heal.heal_data_p3 (#164) and compares the healed B-rep (SmBrep::HealBrep always returns SM_SUCCESS): both sides emit the same digest of the healed topology after the heal (Export extras, every record's links, orientations, geometry ids/types, domains, tolerances, gap flags); the 5 Fix_BackPointers fixtures on the B-spline-only files (Endcap, disk, ellips_ts2, plane_circ, plane_square) compare it (2785 values), the other 10 defer on the analytic export, 3 generated heal cases compare in the 1000-seed sweep, no heal case passes on status alone; the digest exposed that the runner's import never set the Rust region list head (import fixed: the exported head is the infinite region); SmSurface::CreateBlendSurface (surface.surface_p8, partial) is called. Coverage caveats: SmLockBrepBox has no passing case (query.brep), and HealBrep passes only up to SM_HO_FIX_BACKPOINTERS: from SM_HO_CACHE_EDGEPROPS on, the sequence reaches SmEdgeuse::NormalizedEvaluate on every B-rep with edges (harness defect fixed in the final round: the HealBrep fixtures passed healer op 39, outside SmHealerOpType whose SM_HO_ALL is 26; the C++ ran every step for it while the Rust runner mapped the unknown code to SM_HO_NONE, so with #164's sequence in place the 15 cases compared an always-equal status; the fixtures now pass SM_HO_ALL, the FFI rejects codes above 26, the generator draws 0..=26 and the runner refuses an unknown code); CreateCornerBlend (<2 edge uses), sm_SwapFaceUV (marked face), sm_RedistributeSeamUVTrimCurves (no trim curve / non-seam), IsDegenerate (vertex check) and most sm_MakeVertexSplitUVTrimCurve passes are early returns; GetFaces/GetEdges/GetVertices compare the imported native list order (no case lists after a mutation or on a Rust-built B-rep); the shim decides the deferral itself (-3, C++ not run) for BrepIntersect with faces (CreateCornerBlend with 2+ edge uses and CreateEdgeEdgeBlend blends run the C++ since Round 6). Adds to topology::Brep the SmBrep-owned members (infinite region, region list head, editing flag, size estimates, deleted surfaces, explicit face/edge/vertex list order; a future record deletion must unlink from the lists). Upstream defects: UB-98 (SmCopyBrepMap::SetAt/GetAt null deref) met again and reproduced with gdb; UB-162 (sm_RedistributeSeamUVTrimCurves reads sNewEdgeuses[3] unchecked) found by source review, the shim refuses it. Back-edges: UNPORTED(topology.brep_p1) wired in topology.edge, topology.brep_cache, heal.heal_data_p2 (GetTolerance, Get/SetThisModelSizeEstimate), geom.tools, mass.integrator, query.api_queries, heal.heal_data_p1 (#109: GetVertices/GetEdges/GetFaces, the region walk from the infinite region, HealBrep) and boolean.merge_p1 (#163: GetEdges, GetShells); the SmTol::GetZoneTol3d(SmObject*) stubs of curve_cache, ellipse_p1, topology_solver_p1, trim_srf_cache, surface_cache_p1/p2, solution_array and the geom::tol header are self-tagged (the tolerance arms are ported but those sites hold an opaque object handle; maintainer's call); geom.object -> query.brep, proto_topology_p1 -> topology.brep_p2 (GetMinEdgeBBoxSize), trim_srf_cache editing flag -> topology.face_p4. 50 fns; largest: SmBrep::HealBrep, SmBrep::CreateCornerBlend, SmBrep::CreateEdgeEdgeBlend, sm_MakeVertexSplitUVTrimCurve; cycle back-edges (call into later units; stub or co-port): query.brep(3), heal.heal_data_p1(2); unit-SCC #22 (220 units); hazards: ptr-hash; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmBrep::m_bValidateOnly, the region/face use/loop use/edge use/vertex use arenas -> topology::Brep (validate_only, regions(), faceuses(), ..., add_*_record, *_mut); CO-PORTED in topology.edgeuse_p2 (PR #87): SmBrep::m_sThisZoneTol3d, GetTolerance -> topology::Brep::tolerance, set_tolerance; CO-PORTED in heal.heal_data_p3 (PR #164): SmBrep::IsLiveTopologyMember -> heal::heal_data::p3::live (private, arena identity checks); UPDATE after topology.brep_p1: that co-port now calls topology::brep::p1::is_live_topology_member. Round 3 (oursland's review, the #145 delta review): the code after every per-call-site stub is ported (GetShells' mark-and-dedup loop, SetRegionIsVoidFlagsForNestedSolids' traversal, ResetInfiniteRegion's ONLY/SWAP arms, IsDegenerate's IsPointSized); statuses upstream ignores (SetRegionIsVoidFlagsForNestedSolids in ResetInfiniteRegion, GetOrCreateUVTrimCurve in CreateCornerBlend, SwapUV in sm_SwapFaceUV) propagate only Unported and the null-dereference InvalidInput; the corner blend's non-B-spline neighbour surface is labelled nurbs.b_spline_surface_n_lib_p2 (its CreateCornerBlend port takes B-spline neighbours); the SplitUv orientation and SwapFaceUv mark codes decode exactly (6 orient-0 fixtures added). Round 4 (delta review of 0e224736): SmMarkType codes pass through unchanged (the fixtures' 1/2/4/8/16 now run as named); the no_entity cases are rejected; RemoveAt(0) of an empty shell list is a no-op as in release; GetMatedShells' status is ignored; the CalculateBoundingBox / GetOrCreateUVTrimCurve stubs carry the header defaults and the latter returns its curve apart from the status; SmShell::GetRegion reads the co-ported Shell::region.; CO-PORTED in topology.shell (PR #154): SmBrep::GetEdges without a filter as topology::shell::brep_get_edges (edge arena in m_pEdgeListHead order); mass::integrator still stubs UNPORTED(topology.brep_p1): SmBrep::GetEdges (properties.rs) and can call it until this unit's port replaces it; Round 5 (main merged through #154 topology.shell, 1d015b6c): the 5 UNPORTED(topology.shell) sites are wired (SmRegion::GetShells with the attribute filter, SetIsVoid / IsVoid on the raw SmBoolean flag so UNSURE (2) negates to FALSE as upstream, SetList as the rotation of the region's shell list, SmShell::GetMatedShells); the 151 topology.shell deferrals all compare: 21 passed at once, 130 SetInf cases first failed only because the runner never emitted the void flags of a successful ResetInfiniteRegion / SetRegionIsVoidFlagsForNestedSolids (it does now; the SmSetRegionVoidType decode is exact), so all 151 pass; #154's SmBrep::GetEdges co-port now calls this port. Round 6 (main merged through 89dc103a: #186 shared FFI reader, #175 offset_executive_p1, #117 topology.edgeuse_p3, #230): the 3 UNPORTED(topology.edgeuse_p3) sites call topology::edgeuse::p3 with upstream's arguments: NormalizedEvaluate (bOKToMakeUVTrimCurve default TRUE; sm_RedistributeSeamUVTrimCurves requests the unused first derivative as upstream), GetOrCreateUVTrimCurve (header defaults NULL, NULL, TRUE; the curve upstream returns beside the status is read back with SmEdgeuse::GetUVTrimCurve), EvaluateBinormal (bUse3DOnly TRUE at both sites; CreateEdgeEdgeBlend takes the B-rep mutably and ignores both statuses as upstream, going on with the outputs upstream sees after a failure: undefined before EvaluateBinormal's 'Init outputs' step, else zero point/tangent/normal with an undefined binormal (review S1; the earlier round propagated the failure); no compared case reaches it yet); offset_executive_p1's UNPORTED(topology.brep_p1) SmBrep::GetEdges calls topology::brep::p1::edges (list order); its CreateEdgeEdgeBlend stub stays labelled topology.brep_p1: upstream pairs an original and an offset B-rep edge use, this port takes both in one B-rep. Harness: the shim ran no C++ for CreateCornerBlend with 2+ edge uses or CreateEdgeEdgeBlend blends (it wrote -3, 'the Rust side defers'); it now runs both. Its guards: the corner blend replays upstream's ordering and UV phase on a copy of the B-rep (surface types kept) and reports the null-dereference guard only where upstream would copy a NULL UV trim curve (no fixture or sweep case reaches it); the edge-edge blend replays CreateEdgeEdgeBlend up to CreateBlendSurface on its own rails and applies surface.surface_p8's UB-157 probe (SM_ERR for a failing sample, 1007 for a rail 2 that knot synchronisation moves), because running the blend corrupted the heap (UB-157, reached here too). The corner blend now emits the blend surfaces after the count. Flips against the previous report: 240 new UNSURE cases pass (the nested-solids !IsVoid() of UNSURE is unreachable upstream: the infinite region and every queued region are set to 0/1 before being read); 11 deferrals pass (5 CreateCornerBlend on three edge uses, which compare the C++ failure status and an empty surface list, and 6 sm_RedistributeSeamUVTrimCurves, which compare the status and the swap flag); 82 deferrals moved to later units (69 topology.face_p2, 10 intersect.surface_drop_lib_p3, 3 topology.face_p3); no pass changed. All 45 blend-flag CreateEdgeEdgeBlend fixtures still defer on the Rust side, so the blend path itself has no compared case. valgrind (--error-exitcode=99 --suppressions=harness/valgrind/ub11.supp) on corner, redistribute, _unsure, _inf, edge_edge_blend: 0 errors; the suppression covers only UB-11 (Memcheck:Cond in N_BasisInsertKnots, reached by the edge-edge blends; its entry records why that read cannot change a result); without it edge_edge_blend reports exactly that one error. Both shim probes' refusals are shim-decided statuses: guard agreements once the Rust side stops deferring. Round 7 (bherbruck's review of 5b1b28ae, follow-up PR after #145 merged with #196): CreateCornerBlend keeps any SmCurve through the point-curve padding; at the SmBSplineSurface::CreateCornerBlend call the first non-B-spline curve gives SM_ERR_NULL_POINTER (1006, sm_CreateNonRationalCurve's NER) after upstream's NULL UV / surface check, and defers to nurbs.b_spline_surface_n_lib_p2 when a rational curve precedes it (was the -2 guard); 30 crv_on_surf_face corner-blend fixtures (25 pass, 5 defer at n_lib_p2). CreateEdgeEdgeBlend takes each edge use's B-rep (b_2 None when shared); offset_executive_p1's UNPORTED(topology.brep_p1) in BuildSideWallOfEdgeuse is wired (it takes the original B-rep and defers at CreateFaceFromSurface, topology.brep_p8); a new edge_edge_blend2 op compares the ruled blend across two B-reps with distinguishable rails and unequal intervals in separate arenas (5 cases). Shared SmCrvOnSurf import uses owner flag 1 as the C++ graph (0 case changes over all 208 green/partial units). GetZoneTol3d(const SmObject*) stubs tagged geom.tol (maintainer's answer): moves 26 curve_p1, 48 topology_solver_p1 and 6 topology_solver_p2 deferral labels. ParallelProj with a non-positive angle tolerance or an approximation tolerance below the smallest normal double (HANG-32) is rejected on both sides. Fuzz on the corner-blend path found UB-195 [PSIRT] (sm_CreateNonRationalCurve reads sUVCurves[0] after an empty rational-curve drop; located with gdb on a -g build; the shim's corner-blend replay now runs to that drop and both sides report the guard), HANG-39 (tangency tolerance below ~1e-24: tens of seconds in the Coons knot merge; the shim rejects a tolerance below 1e-20 only where its replay reaches the cross-tangent approximation, earlier statuses stay compared: one edge use SM_ERR, four non-tangent edge uses 1006) and HANG-29 (#147) while building a face block with a 1e300 control point (every block value bounded at 1e150; the reserved HANG-33 was retired as a duplicate). Open question recorded in findings for curve.line / topology.edge / topology.face_p2: a Line whose parameterisation was edited carries no NURB and the slicing copies hand on that base. Known back-edges left (UNPORTED(topology.brep_p1) elsewhere): offset_executive_p1.rs:827 GetInfiniteRegion ahead of #175's unported MakeFaceWithCurves tail (own follow-up, COORD.md); intersect/curve_class/p2.rs:267 IsLiveTopologyMember on a generic point-class handle (needs a ClassObject -> EntityRef accessor in intersect.point_class); topology/brep/p7.rs GetInfiniteRegion / SetInfiniteRegion (wired by the brep_p7 owner in #246, with the region-list head removal and the p7 runner's native infinite region). UPDATE after blend.fillet_geom_p1 (PR #147, merged with main 05731370): the surface caches now hold an exact copy of their surface that keeps its owner (clone_for_evaluation, as upstream's m_cpSurface pointer), so a face-owned surface's cache reaches SmTol::GetZoneTol3d(const SmObject*) of the face (UNPORTED(surface.surface_cache_p2)) where the old Clone dropped the owner and used the system tolerance: 2589 cases, 2109 pass, 0 failed, 465 deferred (surface.surface_cache_p2 37: 24 formerly topology.face_p2, 9 query.face_p2, 3 surface.surface_cache_p1, and Endcap_parallel_proj_1, which had passed on the system tolerance; topology.face_p2 now 89; the other deferrals unchanged), 15 rejected. After merging main with batch #269 (incl. #147's surface-cache owner change): 2627 cases, 2141 pass, 0 failed, 470 deferred, 16 rejected; the surface-cache GetZoneTol3d deferrals carry this branch's geom.tol label (37 here, Endcap_parallel_proj_1 among them; topology.brep_p10 5, topology.face_p3 46, offset.offset_geometry_creation 78 relabelled, reports regenerated); fuzz 2000 pass, perturb 2141/2141, valgrind 0. Main since: PARTIAL (PR #145, follow-up after its review): difftest 2627 cases (2335 fixtures on the 15 upstream .smb files, 276 on constructed B-reps, 16 generated), 2142 pass bit-identical, of which 2097 compare values (5 of them, CreateCornerBlend on three edge uses, only the C++ failure status and an empty surface list) and 45 are guard agreements, not coverage (the C++ is not run: the shim stops before an upstream null dereference; 30 are the port's matching refusal: 27 UB-162 redistribute, 3 SWAP without an old infinite region; 15 AddAttributes on a null indexed target are a runner-side mirror with no port call), 16 rejected (15 ops naming an entity kind the wire B-reps lack, 1 HANG-39 corner blend: nothing runs on either side), 0 failed, 469 deferred, max_rel_err 0, --perturb SENSITIVE 2142/2142; --fuzz 2000 (see Round 7); a 1000-seed generated sweep (3611 cases) passes 2881, 0 failed, 714 deferred, 16 rejected (2805 compare values; the other 76 are the same guard agreements); green 206 units (main 89dc103a's units + this unit), 0 failed. All 50 fns have Rust bodies (sm_RedistributeSeamUVTrimCurves and BrepIntersect fully, stubs only at the callee sites); calls into topology.face_p3 (#139) and query.brep are UNPORTED stubs (topology.shell #154 is wired since Round 5, topology.edgeuse_p3 #117 since Round 6) (topology.face_p1's SwapUV is called since #148 merged: sm_SwapFaceUV calls topology.face_p1's SmFace::SwapUV (#148, wired after the merge; status ignored as upstream's release SE, Unported still propagates): on an unmarked face the shim now runs the C++ swap and both sides compare the swapped B-rep digest (the heal digest: surfaces, trim-curve fingerprints, loop-use links), but every such case defers at SmFace::GetBrep (topology.face_p4), the first call of face_p1's swap_uv, so none compares yet (the 16 former topology.face_p1 deferrals are now topology.face_p4 16; no deferral flipped to pass).); HealBrep runs the heal sequence of heal.heal_data_p3 (#164) and compares the healed B-rep (SmBrep::HealBrep always returns SM_SUCCESS): both sides emit the same digest of the healed topology after the heal (Export extras, every record's links, orientations, geometry ids/types, domains, tolerances, gap flags); the 5 Fix_BackPointers fixtures on the B-spline-only files (Endcap, disk, ellips_ts2, plane_circ, plane_square) compare it (2785 values), the other 10 defer on the analytic export, 3 generated heal cases compare in the 1000-seed sweep, no heal case passes on status alone; the digest exposed that the runner's import never set the Rust region list head (import fixed: the exported head is the infinite region); SmSurface::CreateBlendSurface (surface.surface_p8, partial) is called. Coverage caveats: SmLockBrepBox has no passing case (query.brep), and HealBrep passes only up to SM_HO_FIX_BACKPOINTERS: from SM_HO_CACHE_EDGEPROPS on, the sequence reaches SmEdgeuse::NormalizedEvaluate on every B-rep with edges (harness defect fixed in the final round: the HealBrep fixtures passed healer op 39, outside SmHealerOpType whose SM_HO_ALL is 26; the C++ ran every step for it while the Rust runner mapped the unknown code to SM_HO_NONE, so with #164's sequence in place the 15 cases compared an always-equal status; the fixtures now pass SM_HO_ALL, the FFI rejects codes above 26, the generator draws 0..=26 and the runner refuses an unknown code); CreateCornerBlend (<2 edge uses), sm_SwapFaceUV (marked face), sm_RedistributeSeamUVTrimCurves (no trim curve / non-seam), IsDegenerate (vertex check) and most sm_MakeVertexSplitUVTrimCurve passes are early returns; GetFaces/GetEdges/GetVertices compare the imported native list order (no case lists after a mutation or on a Rust-built B-rep); the shim decides the deferral itself (-3, C++ not run) for BrepIntersect with faces (CreateCornerBlend with 2+ edge uses and CreateEdgeEdgeBlend blends run the C++ since Round 6). Adds to topology::Brep the SmBrep-owned members (infinite region, region list head, editing flag, size estimates, deleted surfaces, explicit face/edge/vertex list order; a future record deletion must unlink from the lists). Upstream defects: UB-98 (SmCopyBrepMap::SetAt/GetAt null deref) met again and reproduced with gdb; UB-162 (sm_RedistributeSeamUVTrimCurves reads sNewEdgeuses[3] unchecked) found by source review, the shim refuses it. Back-edges: UNPORTED(topology.brep_p1) wired in topology.edge, topology.brep_cache, heal.heal_data_p2 (GetTolerance, Get/SetThisModelSizeEstimate), geom.tools, mass.integrator, query.api_queries, heal.heal_data_p1 (#109: GetVertices/GetEdges/GetFaces, the region walk from the infinite region, HealBrep) and boolean.merge_p1 (#163: GetEdges, GetShells); the SmTol::GetZoneTol3d(SmObject*) stubs of curve_cache, ellipse_p1, topology_solver_p1, trim_srf_cache, surface_cache_p1/p2, solution_array and the geom::tol header are self-tagged (the tolerance arms are ported but those sites hold an opaque object handle; maintainer's call); geom.object -> query.brep, proto_topology_p1 -> topology.brep_p2 (GetMinEdgeBBoxSize), trim_srf_cache editing flag -> topology.face_p4. 50 fns; largest: SmBrep::HealBrep, SmBrep::CreateCornerBlend, SmBrep::CreateEdgeEdgeBlend, sm_MakeVertexSplitUVTrimCurve; cycle back-edges (call into later units; stub or co-port): query.brep(3), heal.heal_data_p1(2); unit-SCC #22 (220 units); hazards: ptr-hash; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmBrep::m_bValidateOnly, the region/face use/loop use/edge use/vertex use arenas -> topology::Brep (validate_only, regions(), faceuses(), ..., add_*_record, *_mut); CO-PORTED in topology.edgeuse_p2 (PR #87): SmBrep::m_sThisZoneTol3d, GetTolerance -> topology::Brep::tolerance, set_tolerance; CO-PORTED in heal.heal_data_p3 (PR #164): SmBrep::IsLiveTopologyMember -> heal::heal_data::p3::live (private, arena identity checks); UPDATE after topology.brep_p1: that co-port now calls topology::brep::p1::is_live_topology_member. Round 3 (oursland's review, the #145 delta review): the code after every per-call-site stub is ported (GetShells' mark-and-dedup loop, SetRegionIsVoidFlagsForNestedSolids' traversal, ResetInfiniteRegion's ONLY/SWAP arms, IsDegenerate's IsPointSized); statuses upstream ignores (SetRegionIsVoidFlagsForNestedSolids in ResetInfiniteRegion, GetOrCreateUVTrimCurve in CreateCornerBlend, SwapUV in sm_SwapFaceUV) propagate only Unported and the null-dereference InvalidInput; the corner blend's non-B-spline neighbour surface is labelled nurbs.b_spline_surface_n_lib_p2 (its CreateCornerBlend port takes B-spline neighbours); the SplitUv orientation and SwapFaceUv mark codes decode exactly (6 orient-0 fixtures added). Round 4 (delta review of 0e224736): SmMarkType codes pass through unchanged (the fixtures' 1/2/4/8/16 now run as named); the no_entity cases are rejected; RemoveAt(0) of an empty shell list is a no-op as in release; GetMatedShells' status is ignored; the CalculateBoundingBox / GetOrCreateUVTrimCurve stubs carry the header defaults and the latter returns its curve apart from the status; SmShell::GetRegion reads the co-ported Shell::region.; CO-PORTED in topology.shell (PR #154): SmBrep::GetEdges without a filter as topology::shell::brep_get_edges (edge arena in m_pEdgeListHead order); mass::integrator still stubs UNPORTED(topology.brep_p1): SmBrep::GetEdges (properties.rs) and can call it until this unit's port replaces it; Round 5 (main merged through #154 topology.shell, 1d015b6c): the 5 UNPORTED(topology.shell) sites are wired (SmRegion::GetShells with the attribute filter, SetIsVoid / IsVoid on the raw SmBoolean flag so UNSURE (2) negates to FALSE as upstream, SetList as the rotation of the region's shell list, SmShell::GetMatedShells); the 151 topology.shell deferrals all compare: 21 passed at once, 130 SetInf cases first failed only because the runner never emitted the void flags of a successful ResetInfiniteRegion / SetRegionIsVoidFlagsForNestedSolids (it does now; the SmSetRegionVoidType decode is exact), so all 151 pass; #154's SmBrep::GetEdges co-port now calls this port. Round 6 (main merged through 89dc103a: #186 shared FFI reader, #175 offset_executive_p1, #117 topology.edgeuse_p3, #230): the 3 UNPORTED(topology.edgeuse_p3) sites call topology::edgeuse::p3 with upstream's arguments: NormalizedEvaluate (bOKToMakeUVTrimCurve default TRUE; sm_RedistributeSeamUVTrimCurves requests the unused first derivative as upstream), GetOrCreateUVTrimCurve (header defaults NULL, NULL, TRUE; the curve upstream returns beside the status is read back with SmEdgeuse::GetUVTrimCurve), EvaluateBinormal (bUse3DOnly TRUE at both sites; CreateEdgeEdgeBlend takes the B-rep mutably and ignores both statuses as upstream, going on with the outputs upstream sees after a failure: undefined before EvaluateBinormal's 'Init outputs' step, else zero point/tangent/normal with an undefined binormal (review S1; the earlier round propagated the failure); no compared case reaches it yet); offset_executive_p1's UNPORTED(topology.brep_p1) SmBrep::GetEdges calls topology::brep::p1::edges (list order); its CreateEdgeEdgeBlend stub stays labelled topology.brep_p1: upstream pairs an original and an offset B-rep edge use, this port takes both in one B-rep. Harness: the shim ran no C++ for CreateCornerBlend with 2+ edge uses or CreateEdgeEdgeBlend blends (it wrote -3, 'the Rust side defers'); it now runs both. Its guards: the corner blend replays upstream's ordering and UV phase on a copy of the B-rep (surface types kept) and reports the null-dereference guard only where upstream would copy a NULL UV trim curve (no fixture or sweep case reaches it); the edge-edge blend replays CreateEdgeEdgeBlend up to CreateBlendSurface on its own rails and applies surface.surface_p8's UB-157 probe (SM_ERR for a failing sample, 1007 for a rail 2 that knot synchronisation moves), because running the blend corrupted the heap (UB-157, reached here too). The corner blend now emits the blend surfaces after the count. Flips against the previous report: 240 new UNSURE cases pass (the nested-solids !IsVoid() of UNSURE is unreachable upstream: the infinite region and every queued region are set to 0/1 before being read); 11 deferrals pass (5 CreateCornerBlend on three edge uses, which compare the C++ failure status and an empty surface list, and 6 sm_RedistributeSeamUVTrimCurves, which compare the status and the swap flag); 82 deferrals moved to later units (69 topology.face_p2, 10 intersect.surface_drop_lib_p3, 3 topology.face_p3); no pass changed. All 45 blend-flag CreateEdgeEdgeBlend fixtures still defer on the Rust side, so the blend path itself has no compared case. valgrind (--error-exitcode=99 --suppressions=harness/valgrind/ub11.supp) on corner, redistribute, _unsure, _inf, edge_edge_blend: 0 errors; the suppression covers only UB-11 (Memcheck:Cond in N_BasisInsertKnots, reached by the edge-edge blends; its entry records why that read cannot change a result); without it edge_edge_blend reports exactly that one error. Both shim probes' refusals are shim-decided statuses: guard agreements once the Rust side stops deferring. Round 7 (bherbruck's review of 5b1b28ae, follow-up PR after #145 merged with #196): CreateCornerBlend keeps any SmCurve through the point-curve padding; at the SmBSplineSurface::CreateCornerBlend call the first non-B-spline curve gives SM_ERR_NULL_POINTER (1006, sm_CreateNonRationalCurve's NER) after upstream's NULL UV / surface check, and defers to nurbs.b_spline_surface_n_lib_p2 when a rational curve precedes it (was the -2 guard); 30 crv_on_surf_face corner-blend fixtures (25 pass, 5 defer at n_lib_p2). CreateEdgeEdgeBlend takes each edge use's B-rep (b_2 None when shared); offset_executive_p1's UNPORTED(topology.brep_p1) in BuildSideWallOfEdgeuse is wired (it takes the original B-rep and defers at CreateFaceFromSurface, topology.brep_p8); a new edge_edge_blend2 op compares the ruled blend across two B-reps with distinguishable rails and unequal intervals in separate arenas (5 cases). Shared SmCrvOnSurf import uses owner flag 1 as the C++ graph (0 case changes over all 208 green/partial units). GetZoneTol3d(const SmObject*) stubs tagged geom.tol (maintainer's answer): moves 26 curve_p1, 48 topology_solver_p1 and 6 topology_solver_p2 deferral labels. ParallelProj with a non-positive angle tolerance or an approximation tolerance below the smallest normal double (HANG-32) is rejected on both sides. Fuzz on the corner-blend path found UB-195 [PSIRT] (sm_CreateNonRationalCurve reads sUVCurves[0] after an empty rational-curve drop; located with gdb on a -g build; the shim's corner-blend replay now runs to that drop and both sides report the guard), HANG-39 (tangency tolerance below ~1e-24: tens of seconds in the Coons knot merge; the shim rejects a tolerance below 1e-20 only where its replay reaches the cross-tangent approximation, earlier statuses stay compared: one edge use SM_ERR, four non-tangent edge uses 1006) and HANG-29 (#147) while building a face block with a 1e300 control point (every block value bounded at 1e150; the reserved HANG-33 was retired as a duplicate). Open question recorded in findings for curve.line / topology.edge / topology.face_p2: a Line whose parameterisation was edited carries no NURB and the slicing copies hand on that base. Known back-edges left (UNPORTED(topology.brep_p1) elsewhere): offset_executive_p1.rs:827 GetInfiniteRegion ahead of #175's unported MakeFaceWithCurves tail (own follow-up, COORD.md); intersect/curve_class/p2.rs:267 IsLiveTopologyMember on a generic point-class handle (needs a ClassObject -> EntityRef accessor in intersect.point_class); topology/brep/p7.rs GetInfiniteRegion / SetInfiniteRegion (wired by the brep_p7 owner in #246, with the region-list head removal and the p7 runner's native infinite region). UPDATE after blend.fillet_geom_p1 (PR #147, merged with main 05731370): the surface caches now hold an exact copy of their surface that keeps its owner (clone_for_evaluation, as upstream's m_cpSurface pointer), so a face-owned surface's cache reaches SmTol::GetZoneTol3d(const SmObject*) of the face (UNPORTED(surface.surface_cache_p2)) where the old Clone dropped the owner and used the system tolerance: 2589 cases, 2109 pass, 0 failed, 465 deferred (surface.surface_cache_p2 37: 24 formerly topology.face_p2, 9 query.face_p2, 3 surface.surface_cache_p1, and Endcap_parallel_proj_1, which had passed on the system tolerance; topology.face_p2 now 89; the other deferrals unchanged), 15 rejected. After merging main with batch #269 (incl. #147's surface-cache owner change): 2627 cases, 2141 pass, 0 failed, 470 deferred, 16 rejected; the surface-cache GetZoneTol3d deferrals carry this branch's geom.tol label (37 here, Endcap_parallel_proj_1 among them; topology.brep_p10 5, topology.face_p3 46, offset.offset_geometry_creation 78 relabelled, reports regenerated); fuzz 2000 pass, perturb 2141/2141, valgrind 0. CO-PORTED in topology.brep_p3 (PR #204): Brep::notify_serial and its crate-private identity slot (the B-rep's kernel-issued notification serial; upstream objects are known by address) -> topology::Brep Owner zone-tol cache (surface/owner-zone-tol): 2627 cases, 2193 pass, 0 fail, 418 deferred. edge_edge_blend on analytic faces deferred as harness:analytic-geometry-export (+27). CO-PORTED in sweep.topology_sweep: curve_owners/surface_owners accept caller owner identities for transforms; curves/surfaces retain the arena-owner convention. UPDATE (owning-face caches for CreateParallelProjectionCurves and sm_FindSectorOfFace): both take the B-rep mutably. The projection's surface cache and intersection come through the B-rep's live surface context, and FindSectorOfFace fetches the face's cache and checks it out with point testing off around its global drop, as upstream. PARTIAL topology.brep_p1 cases=2627 passed=2532 failed=0 deferred=79 blocked_by=intersect.curve_class_p4(12),nurbs.b_spline_surface_n_lib_p2(8),query.face_p2(30),surface.surface_p2(1),topology.edge(10),topology.face_p2(13),topology.face_p4(5) (was 2461 passed, 150 deferred; 71 of the 116 topology.face_p2 cases pass, 30 move on to SmFace::PointClassify, 3 to FindVertexSector)." +notes = "2026-10-24 (wire: trimmed surface cache history; the intersect::curve_class::p7 tolerance-growth deferral removed, plan/findings.md 'trimmed surface cache history: tolerance-growth deferral removed'), Linux: the surface.surface_p2 deferral (Endcap_heal) is a known mismatch. PARTIAL topology.brep_p1 cases=2627 passed=2587 failed=0 deferred=14 max_rel_err=1e0 with_values=2542 no_values=45 vacuous=0 rejected=16 known_mismatch=10 blocked_by=nurbs.b_spline_surface_n_lib_p2(8),topology.face_p2(6). main CI e33985411 (2026-10-23): PARTIAL topology.brep_p1 cases=2627 passed=2461 failed=0 deferred=150 rejected=16 blocked_by=intersect.curve_class_p4(12),nurbs.b_spline_surface_n_lib_p2(8),surface.surface_p2(1),topology.edge(10),topology.face_p2(117),topology.face_p4(2). EXACT SmSurfOfRevolution EXPORT (PR #304, shared graph exports carry revolution faces exactly; refreshed on main ca3bbd2b, 2026-10-21): difftest 2627 cases, 2207 pass, 0 failed, 404 deferred, 16 rejected (harness:analytic-geometry-export 131, topology.face_p2 94, query.brep 71, geom.tol 64, topology.face_p3 21, intersect.surface_drop_lib_p3 10, curve.crv_on_surf 8, nurbs.b_spline_surface_n_lib_p2 5), max_rel_err 0; 2146 -> 2207 pass; the revolution harness deferrals became passes and deferrals on real stubs; harness label 228 -> 131 (extrusion, sphere, cone/cylinder). Earlier: PARTIAL (PR #145, follow-up after its review): difftest 2627 cases (2335 fixtures on the 15 upstream .smb files, 276 on constructed B-reps, 16 generated), 2142 pass bit-identical, of which 2097 compare values (5 of them, CreateCornerBlend on three edge uses, only the C++ failure status and an empty surface list) and 45 are guard agreements, not coverage (the C++ is not run: the shim stops before an upstream null dereference; 30 are the port's matching refusal: 27 UB-162 redistribute, 3 SWAP without an old infinite region; 15 AddAttributes on a null indexed target are a runner-side mirror with no port call), 16 rejected (15 ops naming an entity kind the wire B-reps lack, 1 HANG-39 corner blend: nothing runs on either side), 0 failed, 469 deferred, max_rel_err 0, --perturb SENSITIVE 2142/2142; --fuzz 2000 (see Round 7); a 1000-seed generated sweep (3611 cases) passes 2881, 0 failed, 714 deferred, 16 rejected (2805 compare values; the other 76 are the same guard agreements); green 206 units (main 89dc103a's units + this unit), 0 failed. All 50 fns have Rust bodies (sm_RedistributeSeamUVTrimCurves and BrepIntersect fully, stubs only at the callee sites); calls into topology.face_p3 (#139) and query.brep are UNPORTED stubs (topology.shell #154 is wired since Round 5, topology.edgeuse_p3 #117 since Round 6) (topology.face_p1's SwapUV is called since #148 merged: sm_SwapFaceUV calls topology.face_p1's SmFace::SwapUV (#148, wired after the merge; status ignored as upstream's release SE, Unported still propagates): on an unmarked face the shim now runs the C++ swap and both sides compare the swapped B-rep digest (the heal digest: surfaces, trim-curve fingerprints, loop-use links), but every such case defers at SmFace::GetBrep (topology.face_p4), the first call of face_p1's swap_uv, so none compares yet (the 16 former topology.face_p1 deferrals are now topology.face_p4 16; no deferral flipped to pass).); HealBrep runs the heal sequence of heal.heal_data_p3 (#164) and compares the healed B-rep (SmBrep::HealBrep always returns SM_SUCCESS): both sides emit the same digest of the healed topology after the heal (Export extras, every record's links, orientations, geometry ids/types, domains, tolerances, gap flags); the 5 Fix_BackPointers fixtures on the B-spline-only files (Endcap, disk, ellips_ts2, plane_circ, plane_square) compare it (2785 values), the other 10 defer on the analytic export, 3 generated heal cases compare in the 1000-seed sweep, no heal case passes on status alone; the digest exposed that the runner's import never set the Rust region list head (import fixed: the exported head is the infinite region); SmSurface::CreateBlendSurface (surface.surface_p8, partial) is called. Coverage caveats: SmLockBrepBox has no passing case (query.brep), and HealBrep passes only up to SM_HO_FIX_BACKPOINTERS: from SM_HO_CACHE_EDGEPROPS on, the sequence reaches SmEdgeuse::NormalizedEvaluate on every B-rep with edges (harness defect fixed in the final round: the HealBrep fixtures passed healer op 39, outside SmHealerOpType whose SM_HO_ALL is 26; the C++ ran every step for it while the Rust runner mapped the unknown code to SM_HO_NONE, so with #164's sequence in place the 15 cases compared an always-equal status; the fixtures now pass SM_HO_ALL, the FFI rejects codes above 26, the generator draws 0..=26 and the runner refuses an unknown code); CreateCornerBlend (<2 edge uses), sm_SwapFaceUV (marked face), sm_RedistributeSeamUVTrimCurves (no trim curve / non-seam), IsDegenerate (vertex check) and most sm_MakeVertexSplitUVTrimCurve passes are early returns; GetFaces/GetEdges/GetVertices compare the imported native list order (no case lists after a mutation or on a Rust-built B-rep); the shim decides the deferral itself (-3, C++ not run) for BrepIntersect with faces (CreateCornerBlend with 2+ edge uses and CreateEdgeEdgeBlend blends run the C++ since Round 6). Adds to topology::Brep the SmBrep-owned members (infinite region, region list head, editing flag, size estimates, deleted surfaces, explicit face/edge/vertex list order; a future record deletion must unlink from the lists). Upstream defects: UB-98 (SmCopyBrepMap::SetAt/GetAt null deref) met again and reproduced with gdb; UB-162 (sm_RedistributeSeamUVTrimCurves reads sNewEdgeuses[3] unchecked) found by source review, the shim refuses it. Back-edges: UNPORTED(topology.brep_p1) wired in topology.edge, topology.brep_cache, heal.heal_data_p2 (GetTolerance, Get/SetThisModelSizeEstimate), geom.tools, mass.integrator, query.api_queries, heal.heal_data_p1 (#109: GetVertices/GetEdges/GetFaces, the region walk from the infinite region, HealBrep) and boolean.merge_p1 (#163: GetEdges, GetShells); the SmTol::GetZoneTol3d(SmObject*) stubs of curve_cache, ellipse_p1, topology_solver_p1, trim_srf_cache, surface_cache_p1/p2, solution_array and the geom::tol header are self-tagged (the tolerance arms are ported but those sites hold an opaque object handle; maintainer's call); geom.object -> query.brep, proto_topology_p1 -> topology.brep_p2 (GetMinEdgeBBoxSize), trim_srf_cache editing flag -> topology.face_p4. 50 fns; largest: SmBrep::HealBrep, SmBrep::CreateCornerBlend, SmBrep::CreateEdgeEdgeBlend, sm_MakeVertexSplitUVTrimCurve; cycle back-edges (call into later units; stub or co-port): query.brep(3), heal.heal_data_p1(2); unit-SCC #22 (220 units); hazards: ptr-hash; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmBrep::m_bValidateOnly, the region/face use/loop use/edge use/vertex use arenas -> topology::Brep (validate_only, regions(), faceuses(), ..., add_*_record, *_mut); CO-PORTED in topology.edgeuse_p2 (PR #87): SmBrep::m_sThisZoneTol3d, GetTolerance -> topology::Brep::tolerance, set_tolerance; CO-PORTED in heal.heal_data_p3 (PR #164): SmBrep::IsLiveTopologyMember -> heal::heal_data::p3::live (private, arena identity checks); UPDATE after topology.brep_p1: that co-port now calls topology::brep::p1::is_live_topology_member. Round 3 (oursland's review, the #145 delta review): the code after every per-call-site stub is ported (GetShells' mark-and-dedup loop, SetRegionIsVoidFlagsForNestedSolids' traversal, ResetInfiniteRegion's ONLY/SWAP arms, IsDegenerate's IsPointSized); statuses upstream ignores (SetRegionIsVoidFlagsForNestedSolids in ResetInfiniteRegion, GetOrCreateUVTrimCurve in CreateCornerBlend, SwapUV in sm_SwapFaceUV) propagate only Unported and the null-dereference InvalidInput; the corner blend's non-B-spline neighbour surface is labelled nurbs.b_spline_surface_n_lib_p2 (its CreateCornerBlend port takes B-spline neighbours); the SplitUv orientation and SwapFaceUv mark codes decode exactly (6 orient-0 fixtures added). Round 4 (delta review of 0e224736): SmMarkType codes pass through unchanged (the fixtures' 1/2/4/8/16 now run as named); the no_entity cases are rejected; RemoveAt(0) of an empty shell list is a no-op as in release; GetMatedShells' status is ignored; the CalculateBoundingBox / GetOrCreateUVTrimCurve stubs carry the header defaults and the latter returns its curve apart from the status; SmShell::GetRegion reads the co-ported Shell::region.; CO-PORTED in topology.shell (PR #154): SmBrep::GetEdges without a filter as topology::shell::brep_get_edges (edge arena in m_pEdgeListHead order); mass::integrator still stubs UNPORTED(topology.brep_p1): SmBrep::GetEdges (properties.rs) and can call it until this unit's port replaces it; Round 5 (main merged through #154 topology.shell, 1d015b6c): the 5 UNPORTED(topology.shell) sites are wired (SmRegion::GetShells with the attribute filter, SetIsVoid / IsVoid on the raw SmBoolean flag so UNSURE (2) negates to FALSE as upstream, SetList as the rotation of the region's shell list, SmShell::GetMatedShells); the 151 topology.shell deferrals all compare: 21 passed at once, 130 SetInf cases first failed only because the runner never emitted the void flags of a successful ResetInfiniteRegion / SetRegionIsVoidFlagsForNestedSolids (it does now; the SmSetRegionVoidType decode is exact), so all 151 pass; #154's SmBrep::GetEdges co-port now calls this port. Round 6 (main merged through 89dc103a: #186 shared FFI reader, #175 offset_executive_p1, #117 topology.edgeuse_p3, #230): the 3 UNPORTED(topology.edgeuse_p3) sites call topology::edgeuse::p3 with upstream's arguments: NormalizedEvaluate (bOKToMakeUVTrimCurve default TRUE; sm_RedistributeSeamUVTrimCurves requests the unused first derivative as upstream), GetOrCreateUVTrimCurve (header defaults NULL, NULL, TRUE; the curve upstream returns beside the status is read back with SmEdgeuse::GetUVTrimCurve), EvaluateBinormal (bUse3DOnly TRUE at both sites; CreateEdgeEdgeBlend takes the B-rep mutably and ignores both statuses as upstream, going on with the outputs upstream sees after a failure: undefined before EvaluateBinormal's 'Init outputs' step, else zero point/tangent/normal with an undefined binormal (review S1; the earlier round propagated the failure); no compared case reaches it yet); offset_executive_p1's UNPORTED(topology.brep_p1) SmBrep::GetEdges calls topology::brep::p1::edges (list order); its CreateEdgeEdgeBlend stub stays labelled topology.brep_p1: upstream pairs an original and an offset B-rep edge use, this port takes both in one B-rep. Harness: the shim ran no C++ for CreateCornerBlend with 2+ edge uses or CreateEdgeEdgeBlend blends (it wrote -3, 'the Rust side defers'); it now runs both. Its guards: the corner blend replays upstream's ordering and UV phase on a copy of the B-rep (surface types kept) and reports the null-dereference guard only where upstream would copy a NULL UV trim curve (no fixture or sweep case reaches it); the edge-edge blend replays CreateEdgeEdgeBlend up to CreateBlendSurface on its own rails and applies surface.surface_p8's UB-157 probe (SM_ERR for a failing sample, 1007 for a rail 2 that knot synchronisation moves), because running the blend corrupted the heap (UB-157, reached here too). The corner blend now emits the blend surfaces after the count. Flips against the previous report: 240 new UNSURE cases pass (the nested-solids !IsVoid() of UNSURE is unreachable upstream: the infinite region and every queued region are set to 0/1 before being read); 11 deferrals pass (5 CreateCornerBlend on three edge uses, which compare the C++ failure status and an empty surface list, and 6 sm_RedistributeSeamUVTrimCurves, which compare the status and the swap flag); 82 deferrals moved to later units (69 topology.face_p2, 10 intersect.surface_drop_lib_p3, 3 topology.face_p3); no pass changed. All 45 blend-flag CreateEdgeEdgeBlend fixtures still defer on the Rust side, so the blend path itself has no compared case. valgrind (--error-exitcode=99 --suppressions=harness/valgrind/ub11.supp) on corner, redistribute, _unsure, _inf, edge_edge_blend: 0 errors; the suppression covers only UB-11 (Memcheck:Cond in N_BasisInsertKnots, reached by the edge-edge blends; its entry records why that read cannot change a result); without it edge_edge_blend reports exactly that one error. Both shim probes' refusals are shim-decided statuses: guard agreements once the Rust side stops deferring. Round 7 (bherbruck's review of 5b1b28ae, follow-up PR after #145 merged with #196): CreateCornerBlend keeps any SmCurve through the point-curve padding; at the SmBSplineSurface::CreateCornerBlend call the first non-B-spline curve gives SM_ERR_NULL_POINTER (1006, sm_CreateNonRationalCurve's NER) after upstream's NULL UV / surface check, and defers to nurbs.b_spline_surface_n_lib_p2 when a rational curve precedes it (was the -2 guard); 30 crv_on_surf_face corner-blend fixtures (25 pass, 5 defer at n_lib_p2). CreateEdgeEdgeBlend takes each edge use's B-rep (b_2 None when shared); offset_executive_p1's UNPORTED(topology.brep_p1) in BuildSideWallOfEdgeuse is wired (it takes the original B-rep and defers at CreateFaceFromSurface, topology.brep_p8); a new edge_edge_blend2 op compares the ruled blend across two B-reps with distinguishable rails and unequal intervals in separate arenas (5 cases). Shared SmCrvOnSurf import uses owner flag 1 as the C++ graph (0 case changes over all 208 green/partial units). GetZoneTol3d(const SmObject*) stubs tagged geom.tol (maintainer's answer): moves 26 curve_p1, 48 topology_solver_p1 and 6 topology_solver_p2 deferral labels. ParallelProj with a non-positive angle tolerance or an approximation tolerance below the smallest normal double (HANG-32) is rejected on both sides. Fuzz on the corner-blend path found UB-195 [PSIRT] (sm_CreateNonRationalCurve reads sUVCurves[0] after an empty rational-curve drop; located with gdb on a -g build; the shim's corner-blend replay now runs to that drop and both sides report the guard), HANG-39 (tangency tolerance below ~1e-24: tens of seconds in the Coons knot merge; the shim rejects a tolerance below 1e-20 only where its replay reaches the cross-tangent approximation, earlier statuses stay compared: one edge use SM_ERR, four non-tangent edge uses 1006) and HANG-29 (#147) while building a face block with a 1e300 control point (every block value bounded at 1e150; the reserved HANG-33 was retired as a duplicate). Open question recorded in findings for curve.line / topology.edge / topology.face_p2: a Line whose parameterisation was edited carries no NURB and the slicing copies hand on that base. Known back-edges left (UNPORTED(topology.brep_p1) elsewhere): offset_executive_p1.rs:827 GetInfiniteRegion ahead of #175's unported MakeFaceWithCurves tail (own follow-up, COORD.md); intersect/curve_class/p2.rs:267 IsLiveTopologyMember on a generic point-class handle (needs a ClassObject -> EntityRef accessor in intersect.point_class); topology/brep/p7.rs GetInfiniteRegion / SetInfiniteRegion (wired by the brep_p7 owner in #246, with the region-list head removal and the p7 runner's native infinite region). UPDATE after blend.fillet_geom_p1 (PR #147, merged with main 05731370): the surface caches now hold an exact copy of their surface that keeps its owner (clone_for_evaluation, as upstream's m_cpSurface pointer), so a face-owned surface's cache reaches SmTol::GetZoneTol3d(const SmObject*) of the face (UNPORTED(surface.surface_cache_p2)) where the old Clone dropped the owner and used the system tolerance: 2589 cases, 2109 pass, 0 failed, 465 deferred (surface.surface_cache_p2 37: 24 formerly topology.face_p2, 9 query.face_p2, 3 surface.surface_cache_p1, and Endcap_parallel_proj_1, which had passed on the system tolerance; topology.face_p2 now 89; the other deferrals unchanged), 15 rejected. After merging main with batch #269 (incl. #147's surface-cache owner change): 2627 cases, 2141 pass, 0 failed, 470 deferred, 16 rejected; the surface-cache GetZoneTol3d deferrals carry this branch's geom.tol label (37 here, Endcap_parallel_proj_1 among them; topology.brep_p10 5, topology.face_p3 46, offset.offset_geometry_creation 78 relabelled, reports regenerated); fuzz 2000 pass, perturb 2141/2141, valgrind 0. Main since: PARTIAL (PR #145, follow-up after its review): difftest 2627 cases (2335 fixtures on the 15 upstream .smb files, 276 on constructed B-reps, 16 generated), 2142 pass bit-identical, of which 2097 compare values (5 of them, CreateCornerBlend on three edge uses, only the C++ failure status and an empty surface list) and 45 are guard agreements, not coverage (the C++ is not run: the shim stops before an upstream null dereference; 30 are the port's matching refusal: 27 UB-162 redistribute, 3 SWAP without an old infinite region; 15 AddAttributes on a null indexed target are a runner-side mirror with no port call), 16 rejected (15 ops naming an entity kind the wire B-reps lack, 1 HANG-39 corner blend: nothing runs on either side), 0 failed, 469 deferred, max_rel_err 0, --perturb SENSITIVE 2142/2142; --fuzz 2000 (see Round 7); a 1000-seed generated sweep (3611 cases) passes 2881, 0 failed, 714 deferred, 16 rejected (2805 compare values; the other 76 are the same guard agreements); green 206 units (main 89dc103a's units + this unit), 0 failed. All 50 fns have Rust bodies (sm_RedistributeSeamUVTrimCurves and BrepIntersect fully, stubs only at the callee sites); calls into topology.face_p3 (#139) and query.brep are UNPORTED stubs (topology.shell #154 is wired since Round 5, topology.edgeuse_p3 #117 since Round 6) (topology.face_p1's SwapUV is called since #148 merged: sm_SwapFaceUV calls topology.face_p1's SmFace::SwapUV (#148, wired after the merge; status ignored as upstream's release SE, Unported still propagates): on an unmarked face the shim now runs the C++ swap and both sides compare the swapped B-rep digest (the heal digest: surfaces, trim-curve fingerprints, loop-use links), but every such case defers at SmFace::GetBrep (topology.face_p4), the first call of face_p1's swap_uv, so none compares yet (the 16 former topology.face_p1 deferrals are now topology.face_p4 16; no deferral flipped to pass).); HealBrep runs the heal sequence of heal.heal_data_p3 (#164) and compares the healed B-rep (SmBrep::HealBrep always returns SM_SUCCESS): both sides emit the same digest of the healed topology after the heal (Export extras, every record's links, orientations, geometry ids/types, domains, tolerances, gap flags); the 5 Fix_BackPointers fixtures on the B-spline-only files (Endcap, disk, ellips_ts2, plane_circ, plane_square) compare it (2785 values), the other 10 defer on the analytic export, 3 generated heal cases compare in the 1000-seed sweep, no heal case passes on status alone; the digest exposed that the runner's import never set the Rust region list head (import fixed: the exported head is the infinite region); SmSurface::CreateBlendSurface (surface.surface_p8, partial) is called. Coverage caveats: SmLockBrepBox has no passing case (query.brep), and HealBrep passes only up to SM_HO_FIX_BACKPOINTERS: from SM_HO_CACHE_EDGEPROPS on, the sequence reaches SmEdgeuse::NormalizedEvaluate on every B-rep with edges (harness defect fixed in the final round: the HealBrep fixtures passed healer op 39, outside SmHealerOpType whose SM_HO_ALL is 26; the C++ ran every step for it while the Rust runner mapped the unknown code to SM_HO_NONE, so with #164's sequence in place the 15 cases compared an always-equal status; the fixtures now pass SM_HO_ALL, the FFI rejects codes above 26, the generator draws 0..=26 and the runner refuses an unknown code); CreateCornerBlend (<2 edge uses), sm_SwapFaceUV (marked face), sm_RedistributeSeamUVTrimCurves (no trim curve / non-seam), IsDegenerate (vertex check) and most sm_MakeVertexSplitUVTrimCurve passes are early returns; GetFaces/GetEdges/GetVertices compare the imported native list order (no case lists after a mutation or on a Rust-built B-rep); the shim decides the deferral itself (-3, C++ not run) for BrepIntersect with faces (CreateCornerBlend with 2+ edge uses and CreateEdgeEdgeBlend blends run the C++ since Round 6). Adds to topology::Brep the SmBrep-owned members (infinite region, region list head, editing flag, size estimates, deleted surfaces, explicit face/edge/vertex list order; a future record deletion must unlink from the lists). Upstream defects: UB-98 (SmCopyBrepMap::SetAt/GetAt null deref) met again and reproduced with gdb; UB-162 (sm_RedistributeSeamUVTrimCurves reads sNewEdgeuses[3] unchecked) found by source review, the shim refuses it. Back-edges: UNPORTED(topology.brep_p1) wired in topology.edge, topology.brep_cache, heal.heal_data_p2 (GetTolerance, Get/SetThisModelSizeEstimate), geom.tools, mass.integrator, query.api_queries, heal.heal_data_p1 (#109: GetVertices/GetEdges/GetFaces, the region walk from the infinite region, HealBrep) and boolean.merge_p1 (#163: GetEdges, GetShells); the SmTol::GetZoneTol3d(SmObject*) stubs of curve_cache, ellipse_p1, topology_solver_p1, trim_srf_cache, surface_cache_p1/p2, solution_array and the geom::tol header are self-tagged (the tolerance arms are ported but those sites hold an opaque object handle; maintainer's call); geom.object -> query.brep, proto_topology_p1 -> topology.brep_p2 (GetMinEdgeBBoxSize), trim_srf_cache editing flag -> topology.face_p4. 50 fns; largest: SmBrep::HealBrep, SmBrep::CreateCornerBlend, SmBrep::CreateEdgeEdgeBlend, sm_MakeVertexSplitUVTrimCurve; cycle back-edges (call into later units; stub or co-port): query.brep(3), heal.heal_data_p1(2); unit-SCC #22 (220 units); hazards: ptr-hash; 319 upstream test entry points reach this unit; CO-PORTED in topology.edgeuse_p1 (PR #55): SmBrep::m_bValidateOnly, the region/face use/loop use/edge use/vertex use arenas -> topology::Brep (validate_only, regions(), faceuses(), ..., add_*_record, *_mut); CO-PORTED in topology.edgeuse_p2 (PR #87): SmBrep::m_sThisZoneTol3d, GetTolerance -> topology::Brep::tolerance, set_tolerance; CO-PORTED in heal.heal_data_p3 (PR #164): SmBrep::IsLiveTopologyMember -> heal::heal_data::p3::live (private, arena identity checks); UPDATE after topology.brep_p1: that co-port now calls topology::brep::p1::is_live_topology_member. Round 3 (oursland's review, the #145 delta review): the code after every per-call-site stub is ported (GetShells' mark-and-dedup loop, SetRegionIsVoidFlagsForNestedSolids' traversal, ResetInfiniteRegion's ONLY/SWAP arms, IsDegenerate's IsPointSized); statuses upstream ignores (SetRegionIsVoidFlagsForNestedSolids in ResetInfiniteRegion, GetOrCreateUVTrimCurve in CreateCornerBlend, SwapUV in sm_SwapFaceUV) propagate only Unported and the null-dereference InvalidInput; the corner blend's non-B-spline neighbour surface is labelled nurbs.b_spline_surface_n_lib_p2 (its CreateCornerBlend port takes B-spline neighbours); the SplitUv orientation and SwapFaceUv mark codes decode exactly (6 orient-0 fixtures added). Round 4 (delta review of 0e224736): SmMarkType codes pass through unchanged (the fixtures' 1/2/4/8/16 now run as named); the no_entity cases are rejected; RemoveAt(0) of an empty shell list is a no-op as in release; GetMatedShells' status is ignored; the CalculateBoundingBox / GetOrCreateUVTrimCurve stubs carry the header defaults and the latter returns its curve apart from the status; SmShell::GetRegion reads the co-ported Shell::region.; CO-PORTED in topology.shell (PR #154): SmBrep::GetEdges without a filter as topology::shell::brep_get_edges (edge arena in m_pEdgeListHead order); mass::integrator still stubs UNPORTED(topology.brep_p1): SmBrep::GetEdges (properties.rs) and can call it until this unit's port replaces it; Round 5 (main merged through #154 topology.shell, 1d015b6c): the 5 UNPORTED(topology.shell) sites are wired (SmRegion::GetShells with the attribute filter, SetIsVoid / IsVoid on the raw SmBoolean flag so UNSURE (2) negates to FALSE as upstream, SetList as the rotation of the region's shell list, SmShell::GetMatedShells); the 151 topology.shell deferrals all compare: 21 passed at once, 130 SetInf cases first failed only because the runner never emitted the void flags of a successful ResetInfiniteRegion / SetRegionIsVoidFlagsForNestedSolids (it does now; the SmSetRegionVoidType decode is exact), so all 151 pass; #154's SmBrep::GetEdges co-port now calls this port. Round 6 (main merged through 89dc103a: #186 shared FFI reader, #175 offset_executive_p1, #117 topology.edgeuse_p3, #230): the 3 UNPORTED(topology.edgeuse_p3) sites call topology::edgeuse::p3 with upstream's arguments: NormalizedEvaluate (bOKToMakeUVTrimCurve default TRUE; sm_RedistributeSeamUVTrimCurves requests the unused first derivative as upstream), GetOrCreateUVTrimCurve (header defaults NULL, NULL, TRUE; the curve upstream returns beside the status is read back with SmEdgeuse::GetUVTrimCurve), EvaluateBinormal (bUse3DOnly TRUE at both sites; CreateEdgeEdgeBlend takes the B-rep mutably and ignores both statuses as upstream, going on with the outputs upstream sees after a failure: undefined before EvaluateBinormal's 'Init outputs' step, else zero point/tangent/normal with an undefined binormal (review S1; the earlier round propagated the failure); no compared case reaches it yet); offset_executive_p1's UNPORTED(topology.brep_p1) SmBrep::GetEdges calls topology::brep::p1::edges (list order); its CreateEdgeEdgeBlend stub stays labelled topology.brep_p1: upstream pairs an original and an offset B-rep edge use, this port takes both in one B-rep. Harness: the shim ran no C++ for CreateCornerBlend with 2+ edge uses or CreateEdgeEdgeBlend blends (it wrote -3, 'the Rust side defers'); it now runs both. Its guards: the corner blend replays upstream's ordering and UV phase on a copy of the B-rep (surface types kept) and reports the null-dereference guard only where upstream would copy a NULL UV trim curve (no fixture or sweep case reaches it); the edge-edge blend replays CreateEdgeEdgeBlend up to CreateBlendSurface on its own rails and applies surface.surface_p8's UB-157 probe (SM_ERR for a failing sample, 1007 for a rail 2 that knot synchronisation moves), because running the blend corrupted the heap (UB-157, reached here too). The corner blend now emits the blend surfaces after the count. Flips against the previous report: 240 new UNSURE cases pass (the nested-solids !IsVoid() of UNSURE is unreachable upstream: the infinite region and every queued region are set to 0/1 before being read); 11 deferrals pass (5 CreateCornerBlend on three edge uses, which compare the C++ failure status and an empty surface list, and 6 sm_RedistributeSeamUVTrimCurves, which compare the status and the swap flag); 82 deferrals moved to later units (69 topology.face_p2, 10 intersect.surface_drop_lib_p3, 3 topology.face_p3); no pass changed. All 45 blend-flag CreateEdgeEdgeBlend fixtures still defer on the Rust side, so the blend path itself has no compared case. valgrind (--error-exitcode=99 --suppressions=harness/valgrind/ub11.supp) on corner, redistribute, _unsure, _inf, edge_edge_blend: 0 errors; the suppression covers only UB-11 (Memcheck:Cond in N_BasisInsertKnots, reached by the edge-edge blends; its entry records why that read cannot change a result); without it edge_edge_blend reports exactly that one error. Both shim probes' refusals are shim-decided statuses: guard agreements once the Rust side stops deferring. Round 7 (bherbruck's review of 5b1b28ae, follow-up PR after #145 merged with #196): CreateCornerBlend keeps any SmCurve through the point-curve padding; at the SmBSplineSurface::CreateCornerBlend call the first non-B-spline curve gives SM_ERR_NULL_POINTER (1006, sm_CreateNonRationalCurve's NER) after upstream's NULL UV / surface check, and defers to nurbs.b_spline_surface_n_lib_p2 when a rational curve precedes it (was the -2 guard); 30 crv_on_surf_face corner-blend fixtures (25 pass, 5 defer at n_lib_p2). CreateEdgeEdgeBlend takes each edge use's B-rep (b_2 None when shared); offset_executive_p1's UNPORTED(topology.brep_p1) in BuildSideWallOfEdgeuse is wired (it takes the original B-rep and defers at CreateFaceFromSurface, topology.brep_p8); a new edge_edge_blend2 op compares the ruled blend across two B-reps with distinguishable rails and unequal intervals in separate arenas (5 cases). Shared SmCrvOnSurf import uses owner flag 1 as the C++ graph (0 case changes over all 208 green/partial units). GetZoneTol3d(const SmObject*) stubs tagged geom.tol (maintainer's answer): moves 26 curve_p1, 48 topology_solver_p1 and 6 topology_solver_p2 deferral labels. ParallelProj with a non-positive angle tolerance or an approximation tolerance below the smallest normal double (HANG-32) is rejected on both sides. Fuzz on the corner-blend path found UB-195 [PSIRT] (sm_CreateNonRationalCurve reads sUVCurves[0] after an empty rational-curve drop; located with gdb on a -g build; the shim's corner-blend replay now runs to that drop and both sides report the guard), HANG-39 (tangency tolerance below ~1e-24: tens of seconds in the Coons knot merge; the shim rejects a tolerance below 1e-20 only where its replay reaches the cross-tangent approximation, earlier statuses stay compared: one edge use SM_ERR, four non-tangent edge uses 1006) and HANG-29 (#147) while building a face block with a 1e300 control point (every block value bounded at 1e150; the reserved HANG-33 was retired as a duplicate). Open question recorded in findings for curve.line / topology.edge / topology.face_p2: a Line whose parameterisation was edited carries no NURB and the slicing copies hand on that base. Known back-edges left (UNPORTED(topology.brep_p1) elsewhere): offset_executive_p1.rs:827 GetInfiniteRegion ahead of #175's unported MakeFaceWithCurves tail (own follow-up, COORD.md); intersect/curve_class/p2.rs:267 IsLiveTopologyMember on a generic point-class handle (needs a ClassObject -> EntityRef accessor in intersect.point_class); topology/brep/p7.rs GetInfiniteRegion / SetInfiniteRegion (wired by the brep_p7 owner in #246, with the region-list head removal and the p7 runner's native infinite region). UPDATE after blend.fillet_geom_p1 (PR #147, merged with main 05731370): the surface caches now hold an exact copy of their surface that keeps its owner (clone_for_evaluation, as upstream's m_cpSurface pointer), so a face-owned surface's cache reaches SmTol::GetZoneTol3d(const SmObject*) of the face (UNPORTED(surface.surface_cache_p2)) where the old Clone dropped the owner and used the system tolerance: 2589 cases, 2109 pass, 0 failed, 465 deferred (surface.surface_cache_p2 37: 24 formerly topology.face_p2, 9 query.face_p2, 3 surface.surface_cache_p1, and Endcap_parallel_proj_1, which had passed on the system tolerance; topology.face_p2 now 89; the other deferrals unchanged), 15 rejected. After merging main with batch #269 (incl. #147's surface-cache owner change): 2627 cases, 2141 pass, 0 failed, 470 deferred, 16 rejected; the surface-cache GetZoneTol3d deferrals carry this branch's geom.tol label (37 here, Endcap_parallel_proj_1 among them; topology.brep_p10 5, topology.face_p3 46, offset.offset_geometry_creation 78 relabelled, reports regenerated); fuzz 2000 pass, perturb 2141/2141, valgrind 0. CO-PORTED in topology.brep_p3 (PR #204): Brep::notify_serial and its crate-private identity slot (the B-rep's kernel-issued notification serial; upstream objects are known by address) -> topology::Brep Owner zone-tol cache (surface/owner-zone-tol): 2627 cases, 2193 pass, 0 fail, 418 deferred. edge_edge_blend on analytic faces deferred as harness:analytic-geometry-export (+27). CO-PORTED in sweep.topology_sweep: curve_owners/surface_owners accept caller owner identities for transforms; curves/surfaces retain the arena-owner convention. UPDATE (owning-face caches for CreateParallelProjectionCurves and sm_FindSectorOfFace): both take the B-rep mutably. The projection's surface cache and intersection come through the B-rep's live surface context, and FindSectorOfFace fetches the face's cache and checks it out with point testing off around its global drop, as upstream. PARTIAL topology.brep_p1 cases=2627 passed=2532 failed=0 deferred=79 blocked_by=intersect.curve_class_p4(12),nurbs.b_spline_surface_n_lib_p2(8),query.face_p2(30),surface.surface_p2(1),topology.edge(10),topology.face_p2(13),topology.face_p4(5) (was 2461 passed, 150 deferred; 71 of the 116 topology.face_p2 cases pass, 30 move on to SmFace::PointClassify, 3 to FindVertexSector)." From 81a2ddaaa7afb436c2c6e212c5eaaeb47b206fbc Mon Sep 17 00:00:00 2001 From: Federico Dal Maso Date: Sat, 24 Oct 2026 17:44:28 +0200 Subject: [PATCH 026/491] fix: classify architecture-dependent C++ references as platform skips Co-Authored-By: OpenAI Codex --- crates/difftest/README.md | 21 ++ crates/difftest/src/baseline.rs | 1 + crates/difftest/src/fuzz.rs | 19 +- crates/difftest/src/jobs.rs | 85 +++++--- crates/difftest/src/lib.rs | 1 + crates/difftest/src/main.rs | 80 ++++++-- crates/difftest/src/platform.rs | 185 ++++++++++++++++++ crates/difftest/src/report.rs | 30 ++- crates/difftest/src/runner.rs | 38 +++- crates/difftest/src/units/geom_hash_table.rs | 4 + .../difftest/src/units/heal_heal_data_p1.rs | 4 + .../difftest/src/units/heal_heal_data_p2.rs | 4 + .../difftest/src/units/heal_heal_data_p3.rs | 4 + .../src/units/surface_twist_volume.rs | 4 + plan/findings.md | 24 +++ 15 files changed, 447 insertions(+), 57 deletions(-) create mode 100644 crates/difftest/src/platform.rs diff --git a/crates/difftest/README.md b/crates/difftest/README.md index dad247a8c1..6f3b02025c 100644 --- a/crates/difftest/README.md +++ b/crates/difftest/README.md @@ -53,6 +53,27 @@ PASS|PARTIAL|FAIL cases=… passed=… failed=… deferred=… max_rel_er ``` then the first few failing cases and `reports/difftest/.json`. + +**Native reference restrictions ([#448](https://github.com/tangency-labs/solidcore/issues/448)).** +`geom.hash_table`, `heal.heal_data_p1`, `heal.heal_data_p2`, `heal.heal_data_p3` +and `surface.twist_volume` require an x86-64 C++ reference. Their out-of-range +floating-point-to-integer conversions are undefined upstream; ARM64 saturates +where the pinned x86-64 reference wraps. Rust deliberately reproduces the latter +on every target. On other architectures these units print `SKIP ... +skipped(platform)` (or `FUZZ-SKIP`), before building fixtures or calling C++. +The twist unit is included because its period reduction has the same dependency, +even though the current committed cases do not expose it. + +Skipped units are counted separately from passes, partials and failures. Their +JSON reports have `status: "skipped(platform)"`, `pass: false`, a reason in +`skipped_platform`, and no compared cases. `--status-of` prints `skipped(platform)`; +ordinary comparison and fuzz runs exit successfully when they contain only passes, +partials and platform skips. `--baseline` fails if any selected reference is +unsupported, and skipped reports are invalid baselines: this cannot waive the CI +coverage ratchet. Plan statuses and tolerances are unchanged. Rust-only +`--digest-dir` runs remain enabled on every target. On Apple Silicon, an x86-64 +harness and difftest binary under Rosetta can compare these units; this does not +address independent libc++ reader or libm differences. `with_values` / `no_values` split the passing cases into those that compared at least one float, integer or topology entry and those that compared nothing (both sides returned the same error, or empty outputs). `vacuous` counts the diff --git a/crates/difftest/src/baseline.rs b/crates/difftest/src/baseline.rs index a8993a3099..5b38893d17 100644 --- a/crates/difftest/src/baseline.rs +++ b/crates/difftest/src/baseline.rs @@ -609,6 +609,7 @@ mod tests { unit: "a.b".into(), cases, harness_error: None, + skipped_platform: None, abs_floors: Vec::new(), seeds: 16, timing: crate::runner::UnitTiming::default(), diff --git a/crates/difftest/src/fuzz.rs b/crates/difftest/src/fuzz.rs index cfc01428c4..ea012414f7 100644 --- a/crates/difftest/src/fuzz.rs +++ b/crates/difftest/src/fuzz.rs @@ -635,6 +635,8 @@ pub struct FuzzResult { pub notes: Vec, /// Failure to build or run the cases. pub harness_error: Option, + /// Reason the native reference is unsupported; no mutations or workers ran. + pub skipped_platform: Option, } impl FuzzResult { @@ -643,15 +645,21 @@ impl FuzzResult { self.verdicts.iter().filter(|&&x| x == v).count() } - /// No failure and no harness error. + /// At least one verdict, no failure, no harness error and no platform skip. pub fn pass(&self) -> bool { - self.harness_error.is_none() && self.failures.is_empty() && !self.verdicts.is_empty() + self.skipped_platform.is_none() + && self.harness_error.is_none() + && self.failures.is_empty() + && !self.verdicts.is_empty() } } /// One-line summary, e.g. `FUZZ-PASS harness.selftest cases=300 rejected=81 malformed=40 /// ran=179 differs=2 crashes=0 panics=0 timeouts=0` (`ran` includes `differs`). pub fn summary_line(r: &FuzzResult) -> String { + if let Some(reason) = &r.skipped_platform { + return format!("FUZZ-SKIP {} skipped(platform): {reason}", r.unit); + } let mut s = format!( "{} {} cases={} rejected={} malformed={} ran={} differs={} crashes={} panics={} timeouts={}", if r.pass() { "FUZZ-PASS" } else { "FUZZ-FAIL" }, @@ -732,8 +740,15 @@ fn clear_old(dir: &Path) { pub fn run_fuzz(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &FuzzOptions) -> FuzzResult { let mut res = FuzzResult { unit: unit.id().to_string(), + skipped_platform: unit + .reference_platform() + .and_then(|p| p.skip_reason(std::env::consts::ARCH)) + .map(str::to_string), ..FuzzResult::default() }; + if res.skipped_platform.is_some() { + return res; + } if let Err(e) = fuzz_unit(unit, ctx, opts, &mut res) { res.harness_error = Some(e.0); } diff --git a/crates/difftest/src/jobs.rs b/crates/difftest/src/jobs.rs index 492d084ec8..d82db88bb6 100644 --- a/crates/difftest/src/jobs.rs +++ b/crates/difftest/src/jobs.rs @@ -161,10 +161,8 @@ pub struct UnitRun { pub stdout: String, /// The child's stderr, forwarded as is. pub stderr: String, - /// The unit counts as failed. - pub failed: bool, - /// The unit counts as partial. - pub partial: bool, + /// Comparison outcome, including unsupported native references. + pub status: crate::UnitStatus, /// The child hit a setup error (exit status 2), e.g. an unwritable report. pub setup_error: bool, } @@ -181,14 +179,20 @@ fn exit_detail(status: ExitStatus) -> String { format!("exit code {}", status.code().unwrap_or(-1)) } -/// Parses `difftest: 1 unit(s), P passed, Q partial, F failed[...]` into `(Q, F)`. -fn parse_summary(line: &str) -> Option<(usize, usize)> { +/// Parses the aggregate counts `(partial, failed, skipped)` of one child. +fn parse_summary(line: &str) -> Option<(usize, usize, usize)> { let rest = line.strip_prefix("difftest: 1 unit(s), ")?; let mut parts = rest.split(", "); let _passed = parts.next()?.strip_suffix(" passed")?; let partial = parts.next()?.strip_suffix(" partial")?.parse().ok()?; let failed = parts.next()?.split(' ').next()?.parse().ok()?; - Some((partial, failed)) + let skipped = parts + .find_map(|p| p.strip_suffix(" skipped(platform)")) + .map(str::parse) + .transpose() + .ok()? + .unwrap_or(0); + Some((partial, failed, skipped)) } /// Turns a finished child's output into what the parent prints and counts. A child that @@ -197,7 +201,7 @@ pub fn interpret(id: &str, status: Option, stdout: &str, stderr: &st let code = status.and_then(|s| s.code()); let body = stdout.trim_end_matches('\n'); let (block, last) = body.rsplit_once('\n').unwrap_or(("", body)); - if let (Some(c @ 0..=2), Some((partial, failed))) = (code, parse_summary(last)) { + if let (Some(c @ 0..=2), Some((partial, failed, skipped))) = (code, parse_summary(last)) { let mut out = block.to_string(); if !out.is_empty() { out.push('\n'); @@ -205,8 +209,15 @@ pub fn interpret(id: &str, status: Option, stdout: &str, stderr: &st return UnitRun { stdout: out, stderr: stderr.to_string(), - failed: failed > 0, - partial: partial > 0, + status: if failed > 0 { + crate::UnitStatus::Fail + } else if partial > 0 { + crate::UnitStatus::Partial + } else if skipped > 0 { + crate::UnitStatus::SkippedPlatform + } else { + crate::UnitStatus::Green + }, setup_error: c == 2, }; } @@ -226,8 +237,7 @@ pub fn interpret(id: &str, status: Option, stdout: &str, stderr: &st UnitRun { stdout: out, stderr: String::new(), - failed: true, - partial: false, + status: crate::UnitStatus::Fail, setup_error: false, } } @@ -317,6 +327,7 @@ pub fn run_parallel( let mut printed = 0usize; let mut pool = Pool(Vec::new()); let (mut failed, mut partial, mut setup_error) = (0usize, 0usize, false); + let mut skipped = 0usize; let stdout = std::io::stdout(); while printed < units.len() { while pool.0.len() < jobs.max(1) { @@ -361,8 +372,9 @@ pub fn run_parallel( let _ = so.flush(); drop(so); let _ = std::io::stderr().write_all(r.stderr.as_bytes()); - failed += usize::from(r.failed); - partial += usize::from(r.partial && !r.failed); + failed += usize::from(r.status == crate::UnitStatus::Fail); + partial += usize::from(r.status == crate::UnitStatus::Partial); + skipped += usize::from(r.status == crate::UnitStatus::SkippedPlatform); setup_error |= r.setup_error; printed += 1; } @@ -371,9 +383,8 @@ pub fn run_parallel( } } println!( - "difftest: {} unit(s), {} passed, {partial} partial, {failed} failed", - units.len(), - units.len() - failed - partial + "{}", + crate::report::run_summary(units.len(), partial, failed, skipped) ); // Timing goes to stderr so stdout stays identical to a serial run. let sum: Duration = times.iter().sum(); @@ -467,17 +478,42 @@ mod tests { #[test] fn summary_lines() { + assert_eq!( + parse_summary( + "difftest: 1 unit(s), 0 passed, 0 partial, 0 failed, 1 skipped(platform)" + ), + Some((0, 0, 1)) + ); assert_eq!( parse_summary("difftest: 1 unit(s), 0 passed, 1 partial, 0 failed"), - Some((1, 0)) + Some((1, 0, 0)) ); assert_eq!( parse_summary("difftest: 1 unit(s), 0 passed, 0 partial, 1 failed, 1 dropped"), - Some((0, 1)) + Some((0, 1, 0)) ); assert_eq!(parse_summary("PASS a cases=1"), None); } + #[test] + #[cfg(unix)] + fn skipped_child_is_neither_a_pass_nor_a_failure() { + use std::os::unix::process::ExitStatusExt; + let summary = crate::report::run_summary(1, 0, 0, 1); + let stdout = format!("SKIP u skipped(platform): unsupported reference\n{summary}\n"); + let r = interpret("u", Some(ExitStatus::from_raw(0)), &stdout, ""); + assert_eq!(r.status, crate::UnitStatus::SkippedPlatform); + assert!(!r.setup_error); + assert_eq!( + r.stdout, + "SKIP u skipped(platform): unsupported reference\n" + ); + assert_eq!( + crate::report::run_summary(3, 1, 0, 1), + "difftest: 3 unit(s), 1 passed, 1 partial, 0 failed, 1 skipped(platform)" + ); + } + #[cfg(unix)] #[test] fn interprets_children() { @@ -490,31 +526,32 @@ mod tests { "", ); assert_eq!(r.stdout, "PASS u cases=1\n report: r/u.json\n"); - assert!(!r.failed && !r.partial && !r.setup_error); + assert_eq!(r.status, crate::UnitStatus::Green); + assert!(!r.setup_error); let partial = interpret( "u", Some(ok), "PARTIAL u\ndifftest: 1 unit(s), 0 passed, 1 partial, 0 failed\n", "", ); - assert!(partial.partial && !partial.failed); + assert_eq!(partial.status, crate::UnitStatus::Partial); let fail = interpret( "u", Some(ExitStatus::from_raw(1 << 8)), "FAIL u\ndifftest: 1 unit(s), 0 passed, 0 partial, 1 failed\n", "", ); - assert!(fail.failed); + assert_eq!(fail.status, crate::UnitStatus::Fail); // SIGSEGV let crash = interpret("u", Some(ExitStatus::from_raw(11)), "", "a\nboom\n"); - assert!(crash.failed); + assert_eq!(crash.status, crate::UnitStatus::Fail); assert_eq!( crash.stdout, "FAIL u (worker process killed by signal 11)\n stderr: a\n stderr: boom\n" ); // A clean exit without the summary line is a crash too. let cut = interpret("u", Some(ok), "PASS u\n", ""); - assert!(cut.failed); + assert_eq!(cut.status, crate::UnitStatus::Fail); assert!(cut .stdout .starts_with("PASS u\nFAIL u (worker process exit code 0)")); diff --git a/crates/difftest/src/lib.rs b/crates/difftest/src/lib.rs index d9f5d70247..28ab51754f 100644 --- a/crates/difftest/src/lib.rs +++ b/crates/difftest/src/lib.rs @@ -19,6 +19,7 @@ pub mod json; pub mod known; pub mod output; pub mod plan; +pub mod platform; pub mod report; pub mod rng; pub mod runner; diff --git a/crates/difftest/src/main.rs b/crates/difftest/src/main.rs index a1bce73abf..2e85e052e3 100644 --- a/crates/difftest/src/main.rs +++ b/crates/difftest/src/main.rs @@ -250,9 +250,7 @@ fn main() -> ExitCode { let res = run_unit(unit.as_ref(), &ctx, &opts); // The value-share floor is part of the gate (`--green` enforces it too). match baseline::value_share(&res) { - Some(v) - if res.status() != UnitStatus::Fail && waivers.covering(&v).is_none() => - { + Some(v) if res.pass() && waivers.covering(&v).is_none() => { eprintln!("difftest: {id}: {}", v.detail); UnitStatus::Fail } @@ -377,6 +375,7 @@ fn main() -> ExitCode { let mut failed = 0usize; let mut partial = 0usize; + let mut skipped = 0usize; for id in &wanted { let Some(unit) = reg.iter().find(|u| u.id() == id.as_str()) else { println!("FAIL {id} (no difftest runner registered for this unit)"); @@ -384,7 +383,7 @@ fn main() -> ExitCode { continue; }; let res = run_unit(unit.as_ref(), &ctx, &opts); - if args.perturb { + if args.perturb && res.status() != UnitStatus::SkippedPlatform { let (n, caught) = res.perturb_stats(); let ok = n > 0 && n == caught; let unperturbed = res @@ -452,18 +451,12 @@ fn main() -> ExitCode { .iter() .find(|p| p.id == id.as_str()) .map(|p| p.status.as_str()); - let mut unit_failed = match res.status() { - UnitStatus::Fail => true, - UnitStatus::Partial if plan_status == Some("green") => { - // Green units cannot contain deferred or known mismatches. - println!( - " FAIL {id}: plan status is green but cases are deferred or known mismatches" - ); - true - } - UnitStatus::Partial | UnitStatus::Green => false, - }; - if !unit_failed { + let gate_error = selection_gate(res.status(), plan_status, args.baseline.is_some()); + if let Some(reason) = gate_error { + println!(" FAIL {id}: {reason}"); + } + let mut unit_failed = res.status() == UnitStatus::Fail || gate_error.is_some(); + if !unit_failed && res.status() != UnitStatus::SkippedPlatform { let mut violations: Vec = baseline::value_share(&res).into_iter().collect(); if let (Some(dir), None) = (&args.baseline, &args.filter) { match baseline::load_baseline(dir, id) { @@ -481,6 +474,8 @@ fn main() -> ExitCode { failed += 1; } else if res.status() == UnitStatus::Partial { partial += 1; + } else if res.status() == UnitStatus::SkippedPlatform { + skipped += 1; } if !args.no_report { match report::write_unit_report(&report_dir, &res) { @@ -501,9 +496,8 @@ fn main() -> ExitCode { String::new() }; println!( - "difftest: {} unit(s), {} passed, {partial} partial, {failed} failed{dropped_note}", - wanted.len(), - wanted.len() - failed - partial + "{}{dropped_note}", + report::run_summary(wanted.len(), partial, failed, skipped) ); if setup_error { ExitCode::from(2) @@ -514,6 +508,24 @@ fn main() -> ExitCode { } } +/// Selection constraints are not case-level waivers. An unsupported oracle cannot +/// satisfy an exact-base run, including when that baseline has no earlier unit report. +fn selection_gate( + status: UnitStatus, + plan_status: Option<&str>, + has_baseline: bool, +) -> Option<&'static str> { + match status { + UnitStatus::Partial if plan_status == Some("green") => { + Some("plan status is green but cases are deferred or known mismatches") + } + UnitStatus::SkippedPlatform if has_baseline => { + Some("skipped reference cannot validate an exact-base comparison") + } + _ => None, + } +} + /// Prints gate violations (waived ones with their reason); true if any is not waived. fn print_violations(violations: &[Violation], waivers: &Waivers) -> bool { let (waived, open): (Vec<&Violation>, Vec<&Violation>) = violations @@ -631,6 +643,7 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode { let mut seen = std::collections::BTreeSet::new(); wanted.retain(|w| seen.insert(w.clone())); let mut failed = 0usize; + let mut skipped = 0usize; for id in &wanted { let Some(unit) = reg.iter().find(|u| u.id() == id.as_str()) else { println!("FUZZ-FAIL {id} (no difftest runner registered for this unit)"); @@ -639,6 +652,10 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode { }; let res = fuzz::run_fuzz(unit.as_ref(), ctx, &opts); println!("{}", fuzz::summary_line(&res)); + if res.skipped_platform.is_some() { + skipped += 1; + continue; + } for f in res.failures.iter().take(MAX_FUZZ_LINES) { let file = f.file.as_deref().map_or_else(String::new, |p| { format!(" -> {}", p.strip_prefix(root).unwrap_or(p).display()) @@ -664,8 +681,13 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode { failed += 1; } } + let skipped_note = if skipped > 0 { + format!(", {skipped} skipped(platform)") + } else { + String::new() + }; println!( - "difftest --fuzz: {} unit(s), {failed} failed, {:.1} s", + "difftest --fuzz: {} unit(s), {failed} failed, {:.1} s{skipped_note}", wanted.len(), start.elapsed().as_secs_f64() ); @@ -675,3 +697,21 @@ fn fuzz_main(args: &Args, ctx: &CaseContext, root: &Path) -> ExitCode { ExitCode::SUCCESS } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn platform_skips_are_not_exact_base_comparisons() { + for plan in [None, Some("green"), Some("partial")] { + assert!(selection_gate(UnitStatus::SkippedPlatform, plan, false).is_none()); + assert!(selection_gate(UnitStatus::SkippedPlatform, plan, true).is_some()); + assert!(selection_gate(UnitStatus::Green, plan, true).is_none()); + } + // A real deferral in a green unit must still fail, on every platform. + assert!(selection_gate(UnitStatus::Partial, Some("green"), false).is_some()); + assert!(selection_gate(UnitStatus::Partial, Some("green"), true).is_some()); + assert!(selection_gate(UnitStatus::Partial, Some("partial"), true).is_none()); + } +} diff --git a/crates/difftest/src/platform.rs b/crates/difftest/src/platform.rs new file mode 100644 index 0000000000..0b081b9d3b --- /dev/null +++ b/crates/difftest/src/platform.rs @@ -0,0 +1,185 @@ +//! Platform requirements of the native reference, independent of Rust portability. +//! +//! Some pinned C++ operations convert out-of-range floats to integers (undefined +//! behavior). The Rust port deliberately reproduces the x86-64 reference on every +//! target. A different native instruction cannot act as its parity oracle (#448). + +/// A documented architecture requirement of a unit's C++ reference. +#[derive(Debug, Clone, Copy)] +pub struct ReferencePlatform { + /// Architecture name, using Rust's `std::env::consts::ARCH` spelling. + pub arch: &'static str, + /// Why other architectures cannot supply this unit's reference results. + pub reason: &'static str, +} + +impl ReferencePlatform { + /// Reason to skip comparison on `arch`; `None` when it is supported. + pub fn skip_reason(self, arch: &str) -> Option<&'static str> { + (arch != self.arch).then_some(self.reason) + } +} + +/// Native voxel mapping and edge-length histograms depend on x86-64 float casts. +pub const X86_64_HASH_CASTS: ReferencePlatform = ReferencePlatform { + arch: "x86_64", + reason: "C++ float-to-ULONG conversions require x86-64 (UB-35/UB-194, #448)", +}; + +/// Native twist-period reduction uses an out-of-range float-to-LONG conversion. +pub const X86_64_TWIST_CASTS: ReferencePlatform = ReferencePlatform { + arch: "x86_64", + reason: "C++ float-to-LONG conversion requires x86-64 (TwistVolume::EvaluateBoundingBoxSimple, #448)", +}; + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::Cell; + + use crate::{Case, CaseContext, CaseSource, HarnessError, Output, RunOptions, UnitDiff}; + + #[test] + fn reference_requirements_are_architecture_specific() { + for platform in [X86_64_HASH_CASTS, X86_64_TWIST_CASTS] { + assert_eq!(platform.skip_reason("x86_64"), None); + for arch in ["aarch64", "arm", "x86", "wasm32"] { + assert_eq!(platform.skip_reason(arch), Some(platform.reason)); + } + } + } + + #[derive(Default)] + struct Restricted { + cases: Cell, + rust: Cell, + } + + impl UnitDiff for Restricted { + fn id(&self) -> &'static str { + "test.platform" + } + + fn reference_platform(&self) -> Option { + Some(ReferencePlatform { + arch: "unsupported-test-architecture", + reason: "test reference is unavailable", + }) + } + + fn cases(&self, _: &CaseContext) -> Result, HarnessError> { + self.cases.set(self.cases.get() + 1); + Ok(vec![Case { + unit: self.id().into(), + name: "value".into(), + source: CaseSource::Inline, + inputs: serde_json::Value::Null, + }]) + } + + fn run_cpp(&self, _: &Case) -> Result { + panic!("unsupported C++ must never run") + } + + fn run_rust(&self, _: &Case) -> Result { + self.rust.set(self.rust.get() + 1); + Ok(Output::new().scalar("value", 42.0)) + } + } + + #[test] + fn unsupported_comparisons_skip_before_fixture_construction_and_never_pass() { + let unit = Restricted::default(); + let ctx = CaseContext { + root: crate::case::repo_root(), + seeds: 16, + }; + for perturb in [false, true] { + let r = crate::run_unit( + &unit, + &ctx, + &RunOptions { + filter: None, + perturb, + }, + ); + assert_eq!(r.status(), crate::UnitStatus::SkippedPlatform); + assert!(!r.pass()); + assert!(r.cases.is_empty()); + assert_eq!((unit.cases.get(), unit.rust.get()), (0, 0)); + let report = crate::report::unit_report(&r); + assert_eq!(report["status"], "skipped(platform)"); + assert_eq!(report["pass"], false); + assert_eq!(report["cases_passed"], 0); + assert_eq!(report["skipped_platform"], "test reference is unavailable"); + assert!(crate::report::summary_line(&r) + .starts_with("SKIP test.platform skipped(platform):")); + // A platform-only report must never replace a trusted CI baseline. + assert!(crate::baseline::parse_baseline(&report.to_string()).is_err()); + let baseline = crate::baseline::Baseline { + unit: unit.id().into(), + status: Some("green".into()), + seeds: Some(16), + cases: vec![crate::baseline::BaselineCase { + name: "value".into(), + outcome: "pass".into(), + generated: false, + compared_values: true, + }], + }; + assert!(!crate::baseline::ratchet(&baseline, &r).is_empty()); + } + } + + #[test] + fn fuzz_skips_without_workers_but_rust_digests_still_run() { + let unit = Restricted::default(); + let ctx = CaseContext { + root: crate::case::repo_root(), + seeds: 16, + }; + let r = crate::fuzz::run_fuzz( + &unit, + &ctx, + &crate::fuzz::FuzzOptions { + cases: 10, + out_dir: ctx.root.join("bin/platform-test-unused"), + filter: None, + }, + ); + assert!(r.skipped_platform.is_some()); + assert!(!r.pass()); + assert!(r.verdicts.is_empty()); + assert_eq!((unit.cases.get(), unit.rust.get()), (0, 0)); + assert!(crate::fuzz::summary_line(&r).starts_with("FUZZ-SKIP test.platform")); + + let digest = crate::runner::digest_unit(&unit, &ctx, &RunOptions::default(), false); + assert!(digest.harness_error.is_none()); + assert_eq!(digest.lines.len(), 1); + assert_eq!((unit.cases.get(), unit.rust.get()), (1, 1)); + } + + #[test] + #[cfg(feature = "cpp")] + fn registry_marks_only_the_documented_units() { + let marked: Vec<_> = crate::registry() + .iter() + .filter_map(|u| { + u.reference_platform().map(|p| { + assert_eq!(p.arch, "x86_64"); + u.id() + }) + }) + .collect(); + assert_eq!( + marked, + [ + "geom.hash_table", + "heal.heal_data_p1", + "heal.heal_data_p2", + "heal.heal_data_p3", + "surface.twist_volume" + ] + ); + } +} diff --git a/crates/difftest/src/report.rs b/crates/difftest/src/report.rs index 3b35417dde..903991860a 100644 --- a/crates/difftest/src/report.rs +++ b/crates/difftest/src/report.rs @@ -87,6 +87,9 @@ pub fn unit_report(r: &UnitResult) -> Value { m.insert("unit".into(), json!(r.unit)); m.insert("pass".into(), json!(r.pass())); m.insert("status".into(), json!(r.status().as_str())); + if let Some(reason) = &r.skipped_platform { + m.insert("skipped_platform".into(), json!(reason)); + } m.insert("rel_tol".into(), num(REL_TOL)); m.insert("abs_floors".into(), Value::Array(floors)); m.insert("seeds".into(), json!(r.seeds)); @@ -131,7 +134,7 @@ pub fn write_unit_report(dir: &Path, r: &UnitResult) -> Result String { + let mut s = format!( + "difftest: {total} unit(s), {} passed, {partial} partial, {failed} failed", + total.saturating_sub(partial + failed + skipped) + ); + if skipped > 0 { + let _ = write!(s, ", {skipped} skipped(platform)"); + } + s +} + /// One-line summary, e.g. /// `PASS harness.selftest cases=40 passed=40 failed=0 deferred=0 max_rel_err=1.1e-16 with_values=36 no_values=4 vacuous=1`. -/// The status word is `PASS` (green), `PARTIAL` or `FAIL`; `with_values` / `no_values` +/// The status word is `PASS` (green), `PARTIAL`, `FAIL` or `SKIP`; a skip prints +/// only the unit and its platform reason. Otherwise, `with_values` / `no_values` /// split the passing cases into those that compared at least one value and those that /// compared nothing (the same error on both sides, or empty outputs); `vacuous` counts /// the `no_values` cases where both sides succeeded, so only the status agreed; with deferred /// cases the line ends with ` blocked_by=(),...`. pub fn summary_line(r: &UnitResult) -> String { + if r.status() == UnitStatus::SkippedPlatform { + return format!( + "SKIP {} skipped(platform): {}", + r.unit, + r.skipped_platform.as_deref().unwrap_or_default() + ); + } let with_values = r.passed_with_values(); let mut s = format!( "{} {} cases={} passed={} failed={} deferred={} max_rel_err={:e} with_values={with_values} no_values={} vacuous={}", @@ -212,6 +236,7 @@ pub fn summary_line(r: &UnitResult) -> String { UnitStatus::Green => "PASS", UnitStatus::Partial => "PARTIAL", UnitStatus::Fail => "FAIL", + UnitStatus::SkippedPlatform => "SKIP", }, r.unit, r.cases.len(), @@ -256,6 +281,7 @@ mod tests { unit: "a.b".into(), cases: Vec::new(), harness_error: None, + skipped_platform: None, abs_floors: Vec::new(), seeds: 16, timing: crate::runner::UnitTiming::default(), diff --git a/crates/difftest/src/runner.rs b/crates/difftest/src/runner.rs index e3f055478f..7f114bd203 100644 --- a/crates/difftest/src/runner.rs +++ b/crates/difftest/src/runner.rs @@ -18,6 +18,13 @@ pub trait UnitDiff { /// Unit id as in `plan/units/.toml` (e.g. `nlib.knots`). fn id(&self) -> &'static str; + /// Architecture restriction of the C++ oracle, if any. This does not restrict + /// Rust-only digest runs. Unsupported comparisons are skipped before fixtures + /// are constructed and never count as parity passes. + fn reference_platform(&self) -> Option { + None + } + /// Runs the upstream C++ (via `solidcore_ffi::units::*`). /// Return `Ok(Output::err(..))` when the *operation* fails and `Err` only /// for harness problems (bad inputs); `Err` always fails the case. Native fixture @@ -197,15 +204,18 @@ pub enum UnitStatus { Partial, /// Any failing case, a harness error, no cases, or only rejected fixtures. Fail, + /// The C++ reference is unsupported on this architecture; no cases ran. + SkippedPlatform, } impl UnitStatus { - /// `green`, `partial` or `fail`. + /// `green`, `partial`, `fail` or `skipped(platform)`. pub fn as_str(self) -> &'static str { match self { Self::Green => "green", Self::Partial => "partial", Self::Fail => "fail", + Self::SkippedPlatform => "skipped(platform)", } } } @@ -219,6 +229,8 @@ pub struct UnitResult { pub cases: Vec, /// Failure to build the case list. pub harness_error: Option, + /// Reason the C++ reference cannot run on this architecture (no cases ran). + pub skipped_platform: Option, /// Documented absolute floors used. pub abs_floors: Vec, /// `--seeds` value used. @@ -280,13 +292,13 @@ impl UnitTiming { } impl UnitResult { - /// Green, partial or fail (see [`UnitStatus`]). + /// Comparison status, including an unsupported reference (see [`UnitStatus`]). pub fn status(&self) -> UnitStatus { - if self.harness_error.is_some() - || self.cases.is_empty() - || self.failed() > 0 - || self.cases.len() == self.rejected() - { + if self.harness_error.is_some() || self.failed() > 0 { + UnitStatus::Fail + } else if self.skipped_platform.is_some() { + UnitStatus::SkippedPlatform + } else if self.cases.is_empty() || self.cases.len() == self.rejected() { UnitStatus::Fail } else if self.deferred() > 0 || self.known_mismatches() > 0 { UnitStatus::Partial @@ -295,9 +307,9 @@ impl UnitResult { } } - /// No failure: the unit is green or partial (the exit status is 0). + /// Parity was checked: the unit is green or partial. Skipped units do not pass. pub fn pass(&self) -> bool { - self.status() != UnitStatus::Fail + matches!(self.status(), UnitStatus::Green | UnitStatus::Partial) } /// Number of passing cases. @@ -452,10 +464,17 @@ pub fn run_unit(unit: &dyn UnitDiff, ctx: &CaseContext, opts: &RunOptions) -> Un unit: unit.id().to_string(), cases: Vec::new(), harness_error: None, + skipped_platform: unit + .reference_platform() + .and_then(|p| p.skip_reason(std::env::consts::ARCH)) + .map(str::to_string), abs_floors: unit.abs_floors().to_vec(), seeds: ctx.seeds, timing: UnitTiming::default(), }; + if res.skipped_platform.is_some() { + return res; + } let digest = std::env::var_os("DIFFTEST_RUST_DIGEST").is_some(); let t0 = Instant::now(); let cases = unit.cases(ctx); @@ -735,6 +754,7 @@ mod tests { unit: "u".into(), cases: vec![case(None, true), case(d(), true)], harness_error: None, + skipped_platform: None, abs_floors: Vec::new(), seeds: 0, timing: UnitTiming::default(), diff --git a/crates/difftest/src/units/geom_hash_table.rs b/crates/difftest/src/units/geom_hash_table.rs index 27eca03f94..0cddf6001e 100644 --- a/crates/difftest/src/units/geom_hash_table.rs +++ b/crates/difftest/src/units/geom_hash_table.rs @@ -637,6 +637,10 @@ fn apply(m: &mut Model, upd: &mut Vec, op: &Op) -> solidcore::Result<()> } impl UnitDiff for Runner { + fn reference_platform(&self) -> Option { + Some(crate::platform::X86_64_HASH_CASTS) + } + fn id(&self) -> &'static str { "geom.hash_table" } diff --git a/crates/difftest/src/units/heal_heal_data_p1.rs b/crates/difftest/src/units/heal_heal_data_p1.rs index c301cb9a68..d7a97f626b 100644 --- a/crates/difftest/src/units/heal_heal_data_p1.rs +++ b/crates/difftest/src/units/heal_heal_data_p1.rs @@ -336,6 +336,10 @@ fn gen_api(rng: &mut Rng) -> Inputs { } impl UnitDiff for Runner { + fn reference_platform(&self) -> Option { + Some(crate::platform::X86_64_HASH_CASTS) + } + fn id(&self) -> &'static str { "heal.heal_data_p1" } diff --git a/crates/difftest/src/units/heal_heal_data_p2.rs b/crates/difftest/src/units/heal_heal_data_p2.rs index 97aa8ca9f8..5c0c16891c 100644 --- a/crates/difftest/src/units/heal_heal_data_p2.rs +++ b/crates/difftest/src/units/heal_heal_data_p2.rs @@ -2021,6 +2021,10 @@ fn gen_prob(rng: &mut Rng) -> Inputs { } impl UnitDiff for Runner { + fn reference_platform(&self) -> Option { + Some(crate::platform::X86_64_HASH_CASTS) + } + fn id(&self) -> &'static str { "heal.heal_data_p2" } diff --git a/crates/difftest/src/units/heal_heal_data_p3.rs b/crates/difftest/src/units/heal_heal_data_p3.rs index f62de33e39..8c6997c5e7 100644 --- a/crates/difftest/src/units/heal_heal_data_p3.rs +++ b/crates/difftest/src/units/heal_heal_data_p3.rs @@ -104,6 +104,10 @@ fn script(rng: &mut Rng) -> Inputs { } impl UnitDiff for Runner { + fn reference_platform(&self) -> Option { + Some(crate::platform::X86_64_HASH_CASTS) + } + fn id(&self) -> &'static str { "heal.heal_data_p3" } diff --git a/crates/difftest/src/units/surface_twist_volume.rs b/crates/difftest/src/units/surface_twist_volume.rs index 86441e6efa..d6cb0d09c6 100644 --- a/crates/difftest/src/units/surface_twist_volume.rs +++ b/crates/difftest/src/units/surface_twist_volume.rs @@ -767,6 +767,10 @@ fn gen_case(rng: &mut Rng) -> Inputs { } impl UnitDiff for Runner { + fn reference_platform(&self) -> Option { + Some(crate::platform::X86_64_TWIST_CASTS) + } + fn id(&self) -> &'static str { "surface.twist_volume" } diff --git a/plan/findings.md b/plan/findings.md index a6afa8c9da..7ecc5fc7be 100644 --- a/plan/findings.md +++ b/plan/findings.md @@ -18986,3 +18986,27 @@ other operand in the merge editing stages (`boolean.merge_p4` / `boolean.merge_p2` classification): the contained shell's faces are dropped instead of being kept reversed as a void. Listed in `known_mismatches.toml`; the offset port itself is not involved after the shell is built. + +## Issue #448: architecture requirements of the differential reference (2026-10-24) + +The pinned C++ converts out-of-range floating-point values to integers in +`SmHashTable.h:1957-1959` (UB-35), `SmTArray.h:2395-2397` (UB-194), and +`SmTwistVolume.cpp:1202`. The Rust ports intentionally emulate the x86-64 +conversions on every target. The ARM64 differences and macOS reproduction are +recorded in #448; this change does not alter either kernel or the reference pin. + +The five runners named in #448 now declare an x86-64 reference requirement. +On other architectures, comparison and fuzz runs stop before fixture construction +with `skipped(platform)`, separate from passes, deferrals and native rejections. +Serial and parallel summaries exclude these units from their passed counts; JSON +reports have `pass: false`. Rust-only digests remain enabled. Skipped reports are +not accepted as baselines, and an exact-base comparison with an unsupported +reference fails even for a newly selected unit. No plan status, case, tolerance or +coverage waiver changed. + +Validation of the skip paths uses an unsupported-reference test double, so it runs +on x86-64 as well. It checks that neither fixture construction nor C++ execution +happens, fuzz workers are not launched, Rust digests still run, skipped reports +cannot replace baselines, and previously passing cases remain ratcheted. The +architecture predicates also cover ARM64 explicitly. No fresh ARM64 native run is +claimed; this machine is Linux x86-64. From e0528587c3e37bf387cd6922818e1cdfdf07c46c Mon Sep 17 00:00:00 2001 From: Federico Dal Maso Date: Sat, 24 Oct 2026 18:04:51 +0200 Subject: [PATCH 027/491] fix: keep perturbation runs from writing skipped reports Co-Authored-By: OpenAI Codex --- crates/difftest/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/difftest/src/main.rs b/crates/difftest/src/main.rs index 2e85e052e3..4e81cdd62f 100644 --- a/crates/difftest/src/main.rs +++ b/crates/difftest/src/main.rs @@ -477,7 +477,7 @@ fn main() -> ExitCode { } else if res.status() == UnitStatus::SkippedPlatform { skipped += 1; } - if !args.no_report { + if !args.no_report && !args.perturb { match report::write_unit_report(&report_dir, &res) { Ok(p) => println!( " report: {}", From 54d7fd4cef977b4470967bce1faa6592b98b582e Mon Sep 17 00:00:00 2001 From: zorak1103 Date: Sat, 24 Oct 2026 19:06:35 +0200 Subject: [PATCH 028/491] wire: intersect.curve_class_p1 face point class (unit to green) The difftest runner's op 50 (sm_ComputeFacePointClass) called the generic Unported stub and ended in a harness error, leaving one deferred case (topology.face_p2). It now runs compute_face_point_class_in on the pool's B-rep record, and the shim emits the whole point classification (object, face, deviation, parameters) instead of the class code only. The generator reaches the op on the one-face B-rep (corners, edges, interior, lifts off the surface, strays) and on bare faces. The shim rejects a face whose UV domain has zero size on either axis (the C++ reference segfaults on it). difftest intersect.curve_class_p1: 2125 cases, 2125 passed, 0 deferred (was 2124 + 1); --seeds 6000: 6125/6125. Co-Authored-By: Claude Sonnet 5.5 --- .../src/units/intersect_curve_class_p1.rs | 78 +++++++++++++++++-- .../src/units/intersect_curve_class_p1.rs | 2 +- harness/cpp/shim/intersect_curve_class_p1.cpp | 10 ++- plan/findings.md | 16 ++++ plan/units/intersect.curve_class_p1.toml | 4 +- 5 files changed, 100 insertions(+), 10 deletions(-) diff --git a/crates/difftest/src/units/intersect_curve_class_p1.rs b/crates/difftest/src/units/intersect_curve_class_p1.rs index e7614b66a8..abe30ae875 100644 --- a/crates/difftest/src/units/intersect_curve_class_p1.rs +++ b/crates/difftest/src/units/intersect_curve_class_p1.rs @@ -28,7 +28,16 @@ //! `sm_SetXSectEdgeThroughPoint` and `sm_GetCCIGeometry` run on the edge's B-rep record //! (an edited edge curve is written back to the pool curve the shim's edge shares). //! -//! Deferred cases: ops that reach other topology members (`sm_ComputeFacePointClass`), +//! `sm_ComputeFacePointClass` runs on the face's B-rep record; both sides emit the status and the whole +//! point classification (object, face, deviation, parameters). On the one-face B-rep the points sit +//! on the corners, edges and interior, lifted off the surface, and strays, so the classes are +//! vertex, edge, face and outside. On a bare face (a surface, no loops, an empty UV domain) the +//! point solve finds nothing and both sides end in `SM_ERR` with the default record, so those +//! cases check the status, not a classification; so do points too far from the one-face +//! B-rep's surface. The shim rejects a face whose UV domain has zero size on either axis (the +//! C++ crashes on the single-point domain; cause not located). +//! +//! Inputs the shim rejects, for which the Rust side would defer or that are undefined upstream: //! surface point solves (`ComputePointParameters` on surfaces, `sm_GetCCISurfaceGaps` with //! surfaces), `SmTol::MapTo2d` for a curve of dimension other than 2 or 3 on a surface //! (`SmSurface::DropPoint`) and an `SmCrvOnSurf` over a plane. Generated scripts are filtered by the shim's @@ -1765,9 +1774,35 @@ fn rust_run(s: &Script) -> Result, Fail> { } OpIn::FacePointClass { f, p, tol, .. } => { let h = pool.handle(*f)?.ok_or_else(|| harness("no face"))?; - let mut pc = PointClassification::new(zone_tol3d(&k, USE_DEFAULT)); - must(cc_mod::compute_face_point_class(v3(*p), zone_tol3d(&k, *tol), h, &mut pc))?; - return Err(harness("face point class ported").into()); + let Some((_, EntityRef::Face(fid))) = pool.topology_entity(h) else { + return Err(harness("face handle without a face").into()); + }; + let mut pc = PointClassification::::new(zone_tol3d(&k, USE_DEFAULT)); + let brep = match pool.topo.as_mut() { + Some(t) => &mut t.brep, + None => &mut pool.bare.brep, + }; + let r = cc_mod::p1::compute_face_point_class_in( + &k, + brep, + fid, + v3(*p), + zone_tol3d(&k, *tol), + &mut pc, + ); + // the status, then the whole classification (also on failure, as the shim) + o.push(match st(r)? { + Ok(()) => 0.0, + Err(s) => s, + }); + let pc = pc + .map_objects(&mut |e| { + pool.topology_handle(e).ok_or_else(|| { + SolidError::invalid_input("difftest", "entity without a pool handle") + }) + }) + .map_err(|e| harness(e.to_string()))?; + put_pc(&mut o, &pc); } _ => {} } @@ -2387,7 +2422,14 @@ fn gen_script(rng: &mut Rng) -> Script { let all: Vec = (0..objects.len()).collect(); for _ in 0..=rng.below(6) { let t = rng.below(2) as usize; - let op = match rng.below(5) { + let op = match rng.below(6) { + // sm_ComputeFacePointClass on a bare face (a surface, no loops) + 5 => OpIn::FacePointClass { + t, + f: pick(rng, &of_kind(&objects, kind::FACE)), + p: rp(rng, 10.0), + tol: *rng.choose(&[1e-5, 1e-4, 1e-3, 1e-6, 0.05]).unwrap_or(&1e-5), + }, 0 | 1 => { let c1 = maybe(rng, &curves, 0.05); let c2 = if rng.chance(0.2) { c1 } else { maybe(rng, &curves, 0.05) }; @@ -2590,6 +2632,30 @@ fn gen_face_script(rng: &mut Rng) -> Option