fix(#702): analyze()'s free-edge detection was dead code; isValidSolid already answers the demotion question - #717
Conversation
…d already answers the demotion question Shape.healed()/Shape.fixSolid() can reach isValid == true by demoting a solid to a shell (ShapeFix_Solid hands back the shell unpromoted whenever it cannot close it, already documented for fixSolid() since #442; a shell has no closure requirement of its own). The issue's own ThruSectionsBuilder bevel-gear reproducer does not reproduce on this branch (OCCT 8.0.1 + patches vs the issue's v1.17.0): three independent reconstructions, including a faithful port of OCCTSwiftScripts/recipes/04-spur-gear's involute math matching the issue's own 1152-points-per-wire figure exactly, all produced a BRepCheck_Analyzer-valid raw loft across 400+ parameter combinations, and source-reading ShapeFix_Solid found no path that demotes a genuinely closed shell. No fix was made for that specific scenario. What is real: an open shell wrapped as a TopoDS_Solid demotes correctly, but OCCTShapeAnalyze called ShapeAnalysis_Shell::LoadShells(), which only does bookkeeping and runs no edge analysis, instead of CheckOrientedShells(), which populates the free-edge set (the sibling OCCTShapeAnalyzeShell already calls it correctly). freeEdgeCount was hardcoded to 0 for every shape, and freeFaceCount's counter was never incremented at all. Fixed. Shape.isValidSolid already answered the issue's question, unaffected by either bug (added for false on a demoted shell where plain isValid reads true. Had no test for that branch; added. healed() gained the demotion warning fixSolid() already carried, and both cross-reference isValidSolid. New tests: Tests/OCCTShapeHealingTests/Issue702SolidDemotionTests.swift, 10 cases on a tiny open-shell fixture (box missing one face). Proven to catch the fix: reverting OCCTShapeAnalyze to LoadShells() fails exactly the 3 tests exercising freeEdgeCount/ freeFaceCount and leaves the other 7 green. Bug fix to values analyze() returns, not a new field or signature; PATCH per SEMVER.md's own rule for a fabricated-zero repair (like #605/#609/#583/#595), not a recorded exception. Closes #702 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
be775f0 to
13b78c0
Compare
secondmouseAU-bot
left a comment
There was a problem hiding this comment.
Automated code review (xhigh effort)
11 findings verified, collapsing to 7 distinct defects. Most severe: a real correctness bug at the core of the #702 fix itself — OCCTShapeAnalyze's new CheckOrientedShells call omits checkinternaledges=true (unlike its sibling OCCTShapeAnalyzeShell), so analyze()/analyzeShell() can silently disagree, contradicting this PR's own "must agree" test invariant. Verified directly against OCCT's ShapeAnalysis_Shell.cxx source.
1. Sources/OCCTBridge/src/OCCTBridge_Healing.mm:181 — correctness (CONFIRMED)
analyze() omits checkinternaledges that analyzeShell() passes for the same check.
The #702 fix calls CheckOrientedShells(shell, /*alsofree*/ true), leaving checkinternaledges at its default false, while the sibling OCCTShapeAnalyzeShell passes CheckOrientedShells(shape->shape, true, true). For a shell containing a TopAbs_INTERNAL-oriented edge, analyze().freeEdgeCount/freeFaceCount can silently disagree with analyzeShell()'s numbers — directly contradicting the new test's own assertion that the two "must agree." The test fixture (a box missing one face) has no internal-orientation edges, so it can't catch this.
2. Sources/OCCTSwift/ShapeAnalysisResult.swift:40 — correctness (CONFIRMED)
totalProblems now double-counts a single open-shell defect (once per free edge via freeEdgeCount, again as +1 via freeFaceCount) now that #702 makes both fields live simultaneously for the first time. For the PR's own fixture, analyze() returns freeEdgeCount=4/freeFaceCount=1 for one defect, so totalProblems=5. The new tests never assert totalProblems's magnitude, so this newly-live double-counting went unverified.
3. docs/guides/cookbook/healing-and-validity.md:83 — correctness (CONFIRMED)
The #702 solid-to-shell demotion warning was added only to healed()'s bullet, not to fixed(tolerance:...)'s or upgraded(tolerance:)'s, even though both are confirmed to run the identical ShapeFix_Shape mechanism that causes the demotion. A caller reading only those docs gets no warning that a solid input can come back as a shell reading isValid == true — the exact bug #702 was filed over.
4. Sources/OCCTBridge/src/OCCTBridge_Healing.mm:182 — correctness (PLAUSIBLE)
Replacing the near-no-op LoadShells(shell) with computational CheckOrientedShells adds a new exception path with no per-shell try/catch, so one OCCT exception now discards the whole analysis (all counts reset to 0, isValid=false) instead of just the free-edge count, for any shell in a multi-shell shape that raises Standard_Failure.
5. Sources/OCCTBridge/src/OCCTBridge_Healing.mm:182 — cleanup (CONFIRMED)
The free-edge/free-face scan is hand-duplicated between OCCTShapeAnalyzeShell and this loop in OCCTShapeAnalyze instead of factored into one shared helper — which is exactly how finding #1's parameter divergence happened (one copy was updated, the other wasn't).
6. Sources/OCCTBridge/include/OCCTBridge_Healing.h:26 — cleanup (CONFIRMED)
New field comments use -- as a sentence dash three times (lines 26, 32, 35; also OCCTBridge_Healing.mm:172 and Issue702SolidDemotionTests.swift:111), violating the repo's no-em-dash writing-style policy — contradicting the PR's own claim of "zero em-dashes in the diff" (true only for the literal — character, not the banned -- variant).
7. Tests/OCCTShapeHealingTests/Issue702SolidDemotionTests.swift:37 — cleanup (CONFIRMED)
The new @Suite display name reads "solid demotion is honestly reported", using the banned word "honestly" per the writing-style policy's hedge-language ban — appears verbatim in every CI test-report run.
🤖 Generated by an automated xhigh-effort multi-agent code review (Claude Code)
secondmouseAU-bot
left a comment
There was a problem hiding this comment.
Corrected automated code review (xhigh effort)
checkinternaledges omission and the totalProblems double-count); the rest (a docs gap on fixed/upgraded, a missing per-shell try/catch, duplicated scan logic, and two writing-style nits) were not part of the verified output and should not be treated as this run's findings. The 7 findings below are the actual verified output.
Most severe: a real correctness bug at the core of the #702 fix itself.
1. Sources/OCCTBridge/src/OCCTBridge_Healing.mm:182 — correctness (CONFIRMED)
New CheckOrientedShells call omits checkinternaledges=true.
The #702 fix's new call omits the third checkinternaledges argument that the sibling OCCTShapeAnalyzeShell passes explicitly as true, so for a shape whose shell contains a TopAbs_INTERNAL/EXTERNAL-oriented edge, Shape.analyze(tolerance:) can silently disagree with Shape.analyzeShell() on free-edge counts — directly contradicting the invariant the PR's own new regression test's name asserts ("analyze() must agree with analyzeShell()"). That test only passes because its fixture happens to contain no internal-oriented edges.
2. OCCTBridge_Healing.mm:182 — correctness (CONFIRMED)
CheckOrientedShells' bad-orientation return value is discarded.
The new call's boolean return value, which reports whether the shell has badly-oriented edges, is discarded entirely, so a real per-shell orientation defect the scan now computes is silently thrown away instead of surfaced in OCCTShapeAnalysisResult. A shape with a non-manifold shell but zero free edges could report isHealthy == true despite ShapeAnalysis_Shell itself having just determined it has bad-oriented edges.
3. Sources/OCCTSwift/ShapeAnalysisResult.swift:40 — correctness (CONFIRMED)
totalProblems double-counts free edges and free faces.
totalProblems sums freeEdgeCount and freeFaceCount together, so a single open shell's defect is now counted twice, now that #702 makes these fields non-zero for the first time. For the PR's own test fixture (4 free edges, 1 free face from one open shell), totalProblems == 5 for what is really one defect.
4. Sources/OCCTSwift/Shape.swift:2035 — correctness (CONFIRMED)
isValidSolid doc comment missing the required fenced Swift snippet.
isValidSolid's doc, including the new #702 demotion paragraph appended in this PR, has no fenced swift snippet anywhere — the same Documentation Standards violation already present on isValid and healed() two properties earlier.
5. Tests/OCCTShapeHealingTests/Issue702SolidDemotionTests.swift:42 — cleanup (CONFIRMED)
New open-shell fixture duplicates an existing one.
openShellMissingOneFace() re-implements the "drop one face, sew" open-shell fixture that Issue442FixSolidMultiBodyTests.swift already builds inline in the same test target, instead of reusing/extracting it.
6. Shape.swift:754 — cleanup (CONFIRMED)
isValid doc expanded but missing a fenced Swift snippet.
isValid's doc comment is rewritten from a one-liner into a multi-sentence explanation of the #702 demotion hazard with no fenced swift snippet, against the repo's Documentation Standards rule.
7. Shape.swift:770 — cleanup (CONFIRMED)
healed() doc's new Warning block missing a Swift snippet.
healed() gains a new multi-sentence Warning block about solid-to-shell demotion but still carries no fenced swift snippet, against the same rule — despite being exactly the high-traffic API the rule calls out for an in-source snippet.
🤖 Generated by an automated xhigh-effort multi-agent code review (Claude Code) — corrected pass
7 findings from an automated review of #702's fix, all addressed: 1. OCCTShapeAnalyze's CheckOrientedShells call omitted checkinternaledges (defaulted false), while the sibling OCCTShapeAnalyzeShell already passed true. Per ShapeAnalysis_Shell.cxx, an edge with no FORWARD/REVERSED partner is unconditionally free when false, but read as connected when true and the same shape also occurs INTERNAL elsewhere in the shell. The two entry points could silently disagree, contradicting the fix's own "must agree" test. New fixture: a single face wrapped as a shell with an .internal duplicate of one of its own boundary edges embedded back into the face before it joins the shell. 2. totalProblems double-counted an open shell's defect: once via freeEdgeCount, again as +1 via freeFaceCount. freeFaceCount is a derived summary of the same scan, not an independent category, so it is excluded from the sum; it stays a public field for the per-shell breakdown. 3. (docs) fixed(tolerance:) and upgraded(tolerance:) run the same ShapeFix_Shape mechanism as healed() and can demote a solid to a shell the same way; the cookbook now warns on all three, not just healed(). 4. The per-shell scan gained a scoped try/catch: CheckOrientedShells is a real computation, unlike the LoadShells() it replaced, so one bad shell must not discard every other shell's contribution. 5. OCCTShapeAnalyze and OCCTShapeAnalyzeShell now share one helper (occtAnalyzeShellOrientation) instead of hand-duplicating the CheckOrientedShells/FreeEdges scan, which is how finding 1 happened. 6/7. Swept the diff for the banned '--' sentence dash and "honestly" (writing-style policy), including two the review didn't name (OCCTBridge_Healing.mm:177 and docs/reference/Shape-Features.md). Injection matrix (prove-the-test-fails): - Finding 1's new test: reverted OCCTShapeAnalyze to the pre-fix checkinternaledges-omitted call -> failed (freeEdgeCount 4 vs 3). Restored -> passed. - Finding 2's totalProblems assertions (3 tests): reinstated + freeFaceCount in the formula -> all 3 failed. Restored -> passed. Full suite: 5374 tests pass. All 4 static gate scripts and their --self-test variants pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed all 7 findings from the automated review (verified each independently against 1. New fixture, since the original box-missing-one-face fixture has no INTERNAL edges and can't catch this: a single square face wrapped directly as a shell (
2. 3. Docs gap (confirmed). Read 4. Missing per-shell try/catch. Added: 5. Shared helper. 6/7. Writing style. Swept the whole diff programmatically (not just the lines the review named) for the banned sentence dash and the hedge word the policy bans. Fixed all instances, including two the review did not name: one in Injection matrix (prove-the-test-fails):
Full Not touched: no |
Verified independently. All 7 addressed, and I reproduced the decisive injection myself.I re-ran finding 1's injection rather than taking the matrix on trust. Reverting Restored, 11/11 green. So the fixture genuinely distinguishes the two argument values, with exactly The two empirical constraints found while building it, that Also confirmed:
An independent finding in the diff worth calling outThe new documentation on CI
|
…dings PR #717 got two automated reviews; the second retracted 5 of the first's 7 points as not part of its own verified output. Findings 1-3 (checkinternaledges, discarded bool return, totalProblems double-count) were already fixed. This addresses the remaining 4: 4/6/7. isValidSolid, isValid and healed() doc comments had no fenced Swift code block. isValid and healed() gained new demotion-hazard prose in the prior commit and neither picked up an example; isValidSolid had none even before that commit touched its doc (verified with git diff against the pre-#702 base: the review was right on all three, contrary to my own read before checking). All three now carry a runnable snippet that builds the box-missing-one-face fixture and shows isValid == true and isValidSolid == false on the demoted shell, the exact hazard each paragraph describes. Verified each snippet's literal code against the real build via a throwaway test (3 cases, all passed, not committed). 5. Issue702SolidDemotionTests.openShellMissingOneFace() duplicated a fixture Issue442FixSolidMultiBodyTests.swift already built inline in the same target: both drop one face from a box, compound the remaining five, and sew them. Extracted to Tests/OCCTShapeHealingTests/ShapeHealingTestFixtures.swift (sewnBoxMissingOneFace(_:tolerance:), parameterized on the box so each suite keeps its own box size/origin), following the precedent Tests/OCCTStressTests/StressTestFixtures.swift already set for sharing fixtures within one test target. Both suites re-verified unchanged. No new tests added (doc snippets plus a fixture refactor), so no new prove-the-test-fails injection applies; both affected suites (Issue442, Issue702) and the full suite were re-run after the refactor. Full suite: 5374 tests pass (unchanged from before this commit). All 5 static gate scripts and their --self-test variants pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed the second (corrected) review's 4 remaining findings, in Finding 4/6/7 (missing fenced Swift snippets on Finding 5 ( No new tests were added (doc snippets + a fixture refactor), so prove-the-test-fails injection doesn't apply here; instead both affected suites and the full suite were re-run to confirm the refactor is behavior-preserving. Verification:
CHANGELOG updated under the existing #702 entry with a note on this second, corrected review pass. No |
PR #717 drew two automated review passes and the second retracted the first, renumbering as it went. The try/catch guard was the first pass's finding 4; the second pass's finding 4 is an unrelated missing doc snippet. Six code comments carried bare ordinals from the retracted pass, so each had silently come to point at a different finding than the one it described. A bare ordinal is not a stable citation when the thing it indexes can be reissued. Every site now names what the finding said. Comment-only; the one note that does mention numbers is the one explaining why the others do not. Also records, on the extracted fixture, that it drops the caller-side `#expect(faces.count == 6)` Issue442FixSolidMultiBodyTests asserted inline: the test still fails on a wrong face count, one step later and with a less specific message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rdinals The README framed itself as ground truth for a specific automated review and cited its findings by number. Both are unstable framing. Review ordinals are not a citation: PR #717 drew two passes whose numbering disagreed and the second retracted five of the first's seven findings, so an ordinal can come to mean a different finding without anything visibly changing. The measurements themselves are worth keeping and are the reason Scripts/repro is committed at all. Retitled around the questions they answer, sliver-face centroid stability and the cost of the centroid formula, with the review noted as the prompt rather than the subject. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What & why
Shape.healed()andShape.fixSolid()can reachisValid == trueby demoting a solid to ashell (
ShapeFix_Solidhands back the shell unpromoted whenever it cannot close it, alreadydocumented for
fixSolid()since #442; a shell has no closure requirement of its own, so thedemoted result is genuinely valid). #702 reported this as invisible to every diagnostic:
isValidtrue,
volumecorrect,analyze()clean.The issue's own ThruSections/bevel-gear reproducer does not reproduce on this branch (OCCT
8.0.1 + patches vs the issue's v1.17.0). Three independent, faithful reconstructions, including a
port of
OCCTSwiftScripts/recipes/04-spur-gear's involute math matching the issue's own1152-points-per-wire figure exactly, across 400+ parameter combinations, plus source-reading
ShapeFix_Solid, all confirm nothing is broken there today. No manufactured fix for a defect thatis not reproducing.
What is real and fixed:
Shape.analyze(tolerance:)'s free-edge/free-face detection was deadcode.
OCCTShapeAnalyzecalledShapeAnalysis_Shell::LoadShells(), which only does bookkeepingand runs no edge analysis, instead of
CheckOrientedShells(), which actually populates thefree-edge set (the sibling entry point
OCCTShapeAnalyzeShellalready calls it correctly).freeEdgeCountwas hardcoded to 0 for every shape, andfreeFaceCount's counter variable wasnever incremented at all. Fixed; both now report accurately.
Shape.isValidSolidalready answered the issue's actual question, unaffected by either bug(added for #206/#208, unrelated): it checks
shapeType == .solidbeforeBRepCheck_Analyzer, soit reads
falseon a demoted shell where plainisValidreadstrue. It had no test for thatbranch; added.
healed()andfixSolid()'s docs now cross-reference it.Closes #702
Checklist
Tests/OCCTShapeHealingTests/Issue702SolidDemotionTests.swift, 10 cases.Notes for the reviewer
Reproduction status, in detail. The issue's reproducer was measured on OCCT v1.17.0. Tried on
this branch's kernel (8.0.1 + eleven carried patches):
OCCTSwiftScripts/recipes/04-spur-gear's involute tooth math, scaled persection for a bevel taper (with and without a spiral-twist variant), matching the issue's own
1152-points-per-wire figure exactly at
flankSamples = 14.Over 400 parameter combinations across these four families, every raw
ThruSectionsBuilderresult was
BRepCheck_Analyzer-valid, so nothing ever reachedhealed()/fixSolid()in a statethat could demote. Separately:
BRepCheck_Analyzerdoes not flag 3D self-intersection betweennon-adjacent faces at all (the deliberately self-intersecting twisted prisms above all report
isValid == true), which matchesanalyze()'s ownselfIntersectionCountfield being apermanent, undocumented-until-now 0. Source-reading
ShapeFix_Solid::Perform(),CreateSolids(), andCollectSolids()(all three demotion code paths, single-shell andmulti-shell) found every one gated on a genuine closure check (
BRep_Tool::IsClosed/ShapeAnalysis_FreeBounds), matching #442's already-documented and already-tested contract. Icould not construct any case where a topologically closed shell gets wrongly demoted.
Whose bug is the free-edge/free-face defect? Ours, in the bridge, not OCCT.
OCCTShapeAnalyzecalled the wrong
ShapeAnalysis_Shellmethod (LoadShells()instead ofCheckOrientedShells());OCCT's own API works correctly when called right, which the sibling
OCCTShapeAnalyzeShellalready demonstrates. No kernel patch, no upstream filing.
The contract chosen for
healed()/fixSolid()demotion itself: preserve, not report orreject, because it already was, correctly, since #442; this PR doesn't revisit that decision.
Refusing (returning
nil/the original invalid solid) would reverse a deliberate #442 decision("no body is ever dropped to make [isValid] true") for no new reason. Reporting (a new
WithReport-style API, matching #639/#709's precedent) would be redundant:isValidSolidalreadyexists, already answers the question correctly (verified directly, unaffected by the
analyze()bug), and just needed cross-referencing and a test.Injection matrix (
Tests/OCCTShapeHealingTests/Issue702SolidDemotionTests.swift, all 10cases, bug reintroduced by reverting
OCCTShapeAnalyzetoshellAnalysis.LoadShells(shell)):The three tests targeting the actual bug fail precisely when it's present; the other seven
(testing the already-correct demotion mechanism and the already-correct
isValidSolid) staygreen throughout, confirming the new tests are isolated to the bug they exist to catch.
docs/SEMVER.md: no entry added. This is a bug fix to the valuesanalyze(tolerance:)returns, not a new field, removed field, or signature change; per the file's own quick-reference
table this is the PATCH case ("a wrong value repaired"), matching the precedent of the
#605/#609/#583/#595 fabricated-zero fixes, none of which are recorded exceptions either. Rechecked
the "thirteen recorded exceptions" counters at the top of the file: still accurate (3 compile-break
What the issue got wrong (its own numbers are from v1.17.0, expected to have moved): the
specific bevel-gear reproducer no longer produces an invalid raw solid on this branch, so the
premise "the builder does produce a solid [that's invalid]" no longer holds here. The issue's
"analyze() ... no self-intersections" evidence was inadvertently reporting a permanently-stubbed
field, not a measured absence, both before and after this fix (now documented rather than silent).
Verified: clean
swift build, 0 errors, no new warnings, noOCCTSWIFT_LOCAL. Fullswift test: 5356 baseline + 10 new = 5366, 0 failures.Censuses cluster-a45 rows,cluster-b16rows (both unaffected by this change, confirmed unchanged). Four gate scripts and their three
--self-tests all green. Zero em-dashes in the diff.