fix(html): trace hyperedge perimeter in convex-hull order, not member order - #2450
fix(html): trace hyperedge perimeter in convex-hull order, not member order#2450ysys143 wants to merge 1 commit into
Conversation
… order (Graphify-Labs#2449) _hyperedge_script mapped h.nodes straight to positions and traced that array in order, so the ring self-intersected whenever the layout did not happen to place members in angular order — fill() then painted crossed wedges instead of one region. Over 20k random 4-7 member sets, 75.3% of arbitrary orderings self-intersect, so this was the common case rather than a corner. The 1.15x expansion could not mask it: a positive homothety preserves segment intersections. Compute Andrew's monotone chain over the member positions and expand the hull instead of the raw array. This matches the existing "expanded hull" comment and Graphify-Labs#334's "convex-hull polygons" wording, and degrades cleanly where an angular sort would not: collinear and duplicate points collapse to the extremes rather than producing a zero-area crossed path, with no atan2(0, 0) tie. Tests execute the emitted JS in node (skipped when absent) and assert the ring is simple, convex, counter-clockwise, and encloses every member.
There was a problem hiding this comment.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Graphify review — findings
This PR modifies the HTML exporter's hyperedge rendering to compute a convex hull (via Andrew's monotone chain) over member node positions before tracing the surrounding polygon, replacing the prior approach that expanded points in raw member order. It adds two tests in tests/test_export.py: one asserting the emitted script contains the convexHull function and uses hull-ordered points, and another that extracts the emitted JavaScript and runs it under node (skipping if unavailable) to check geometric properties like non-self-intersection, convexity, and point enclosure across random and degenerate inputs. The large list of "changed symbols" appears to reflect the test module being touched; the substantive surface area is the _hyperedge_script function in graphify/exporters/html.py and the two new test functions.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 320 functions depend on the 114 functions this change touches.
Health — grade A; 5 existing hotspot(s) in the area this change touches (pre-existing, not introduced here):
_rebuild_code()— 68 callers, 49 callees (high)dispatch_command()— 2 callers, 110 callees (high)to_html()— 16 callers, 7 callees (high)run_pipeline()— 8 callers, 12 callees (high)watch()— 4 callers, 6 callees (medium)
Verification — 320 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 190 function(s) in the blast radius were not formally verified this run
Fixes #2449.
What
_hyperedge_scripttraced the hyperedge ring by walkingh.nodesin arrayorder. That self-intersects whenever the layout does not happen to place members
in angular order around the centroid, so
fill()paints crossed wedges insteadof one region. The 1.15x expansion cannot mask it — a positive homothety
preserves segment intersections.
This computes Andrew's monotone chain over the member positions and expands the
hull instead of the raw array.
Why a hull rather than an angular sort
A one-line
atan2sort also removes the crossings, and I tried that first. Iwent with the hull because it matches the existing
// Centroid and expanded hullcomment and #334's "convex-hull polygons" wording, and because it degradesbetter on degenerate member sets: collinear and duplicate points collapse to the
extremes rather than producing a zero-area crossed path, and there is no
atan2(0, 0)tie when the centroid lands on a vertex.Happy to swap it for the one-liner if you prefer the smaller diff.
Scale of the defect
Over 20,000 random 4-7 member point sets (the sizes real hyperedges carry — mine
were
{4: 1, 5: 8, 6: 3, 7: 3}), 75.3% of arbitrary orderings self-intersectafter expansion. This was the common case, not a corner.
Tests
Two tests in
tests/test_export.py:test_hyperedge_perimeter_uses_convex_hull_not_member_order— guards againstreverting to raw member order.
test_hyperedge_convex_hull_js_is_geometrically_sound— executes the emittedJS in node and asserts the ring is simple, convex, counter-clockwise, and
encloses every member, across 2,000 random sets plus the degenerate cases and
the bow-tie ordering this exists for. Skipped when node is unavailable.
Both fail on the unpatched exporter and pass with the change.
tests/is otherwise unaffected: 3894 passed. The 15 failures intest_ollama_retry_cap.pyandtest_skillgen.pyreproduce identically on aclean
v8checkout without this change.Open question
#2449 raises it too: if
h.nodesorder is meant to be load-bearing — severalemitted arrays do follow their flow labels — then tracing in array order is the
intent and this should be closed. My reading is that the schema treats
hyperedges as sets (
participate_in | implement | form, no sequence relation)and that a translucent fill with no direction cues cannot convey a sequence
anyway, but that call is yours.