A reference is a reference whatever feature holds it

Port of snaporca 1bb9825db0. Four defects from an independent 20-agent audit,
each verified in the code first; two further findings from the same report were
verified OUT and are not in this commit.

remove_feature()/move_feature() remapped Extrude::sketch_ref and a Mate's two
connectors and nothing else, leaving seven of the nine index-bearing fields —
sweep_path_ref, loft_profile_refs[], pattern_curve_sketch, rib_sketch_ref, and
sketch_ref on Revolve, Sweep, Rib and the Surface* family — pointing at whatever
slid into the slot. Quiet by construction: the shifted index still names a real
feature, recompute() succeeds, the solid is built from the wrong profile. The
comment above the loop already required "EVERY field holding a feature index"; the
code under it handled two, because a type switch is only correct on the day it is
written. for_each_feature_ref() visits the FIELDS instead, so a feature type added
later is covered the moment it reuses one. plane_base and axis_plane_a/b are
excluded on purpose and documented at the helper — they encode an ordinal into the
datum-plane list, not an index into features[], and are filed separately. The
delete cascade got the same field-based treatment.

The regression test was run against the pre-fix code to prove it bites: all three
sections fail there, and move_feature returns TRUE while leaving sketch_ref == 1
where it must be 0 — success with the wrong answer, which is what makes this class
expensive.

apply_constraint, commit_entity_constraints and delete_constraint mutated the
recipe with no checkpoint() and no sync_recipe_to_model(), alone among seventeen
mutation sites in that file: Ctrl+Z reached past the constraint edit and discarded
unrelated work, and saving persisted the pre-constraint blob. A rejected constraint
now calls abandon_checkpoint() rather than leaving an undo step that does nothing.

MCP: params["generation"].get<uint64_t>() sat outside the try inside a bare
CallAfter lambda, so one malformed string terminated the process through the wx
event loop; it is type-checked now and the lambda lets nothing escape. The socket
bound with no mode of its own in a world-writable directory — umask around bind()
plus chmod, and it refuses to listen rather than listen wide. The reply write is no
longer a bare write(), which could SIGPIPE the app when a client hung up.

Kernel suite on this fork: 190 cases / 2562 assertions, green. GUI target compiles.
The full ladder gate ran on snaporca (ALL LADDERS HELD — gestures 98/98, offer
108/108, corpus and corpus-scale green) and fork-check parity holds at 17 identical
/ 8 diverging as expected, which is what makes that gate transferable here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
This commit is contained in:
Tommaso Bianchi
2026-08-24 19:00:45 +02:00
co-authored by Claude Opus 5
parent 6e7f6429fa
commit 3fd5c3353a
5 changed files with 188 additions and 23 deletions
+16
View File
@@ -7850,16 +7850,25 @@ void DesignPanel::commit_entity_constraints(const std::vector<SketchEntityConstr
// (Symmetric on two lines) must solve together, so push all then resize back.
const std::vector<SketchEntity> saved = feat.entities;
const size_t before = feat.entity_constraints.size();
// Undo boundary: adding a constraint. Without it Ctrl+Z reached PAST this edit to the
// previous boundary and threw away whatever happened in between — a constraint was the
// one document mutation the user could not take back on its own.
m_doc.checkpoint();
for (const auto& d : defs) feat.entity_constraints.push_back(d);
if (!m_doc.solve_sketch_feature(m_constrain_feat)) {
feat.entity_constraints.resize(before);
feat.entities = saved;
m_doc.abandon_checkpoint(); // fully restored above: nothing happened, so nothing to undo
m_status->SetForegroundColour(wxColour(235, 110, 110));
set_status(_L("Constraint rejected (over-constrained)"));
m_status->Refresh();
return;
}
m_doc.recompute();
// The constraint lives in the recipe, so the save path has to be told the recipe moved;
// otherwise saving after a constraint edit wrote the blob from before it.
sync_recipe_to_model();
update_undo_redo_buttons();
m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities);
if (!m_doc.display_mesh.its.indices.empty())
feed_bodies();
@@ -8029,11 +8038,14 @@ void DesignPanel::delete_constraint(int idx)
CadFeature& feat = m_doc.features[m_constrain_feat];
if (idx < 0 || idx >= int(feat.entity_constraints.size()))
return;
m_doc.checkpoint(); // undo boundary: deleting a constraint
feat.entity_constraints.erase(feat.entity_constraints.begin() + idx);
// Re-solve the remaining system (deleting a constraint can only free DoF, so it
// cannot fail for over-constraint; ignore the bool and refresh either way).
m_doc.solve_sketch_feature(m_constrain_feat);
m_doc.recompute();
sync_recipe_to_model(); // the removal is part of the recipe
update_undo_redo_buttons();
m_viewport->set_constraint_highlight({});
m_viewport->update_constrain_entities(m_doc.features[m_constrain_feat].entities);
if (!m_doc.display_mesh.its.indices.empty())
@@ -8867,16 +8879,20 @@ void DesignPanel::apply_constraint(SketchConstraintType type)
// solve_sketch_feature rewrites profile.points even on failure, so snapshot
// the geometry to roll back a rejected constraint cleanly.
const std::vector<Vec2d> saved_pts = feat.profile.points;
m_doc.checkpoint(); // undo boundary: adding a constraint (profile sketches)
feat.constraints.push_back(SketchConstraintDef{type, a, b, -1, -1, 0.0});
if (!m_doc.solve_sketch_feature(m_constrain_feat)) {
feat.constraints.pop_back(); // reject the non-converging addition
feat.profile.points = saved_pts; // and restore the pre-solve geometry
m_doc.abandon_checkpoint(); // restored: no state change, so no undo step
m_status->SetForegroundColour(wxColour(235, 110, 110));
set_status(_L("Constraint rejected (over-constrained)"));
m_status->Refresh();
return;
}
m_doc.recompute();
sync_recipe_to_model();
update_undo_redo_buttons();
m_viewport->update_constrain_profile(m_doc.features[m_constrain_feat].profile.points);
if (!m_doc.display_mesh.its.indices.empty())
feed_bodies();