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
+50 -20
View File
@@ -1859,6 +1859,12 @@ void CadDocument::checkpoint()
m_undo.erase(m_undo.begin());
}
void CadDocument::abandon_checkpoint()
{
if (!m_undo.empty())
m_undo.pop_back();
}
bool CadDocument::undo()
{
if (m_undo.empty())
@@ -1915,6 +1921,33 @@ static bool commit_or_rollback(CadDocument& doc, std::vector<CadFeature>& snapsh
return false;
}
// Visit every field of `f` that holds an index into features[].
//
// Deliberately NOT dispatched on f.type. A type switch is what this replaced, and it was
// wrong in the way type switches go wrong: it listed Extrude and Mate, and every feature
// type added afterwards — Revolve, Sweep, Loft, Rib, Pattern-on-curve, the Surface* family —
// silently inherited a remap that skipped its references. Visiting the FIELDS instead means
// a new type is covered the moment it reuses one of them, and reaching a field its type
// never reads costs nothing: unset refs are -1 and every visitor here ignores those.
//
// The list is exactly the fields documented as "index into features[]" / "feature index" in
// CadDocument.hpp. Not included, because they are a different basis and need their own pass:
// plane_base / axis_plane_a / axis_plane_b encode `3 + N` = the Nth DATUM PLANE, an ordinal
// into resolve_datum_planes(), not into features[]. Everything named *_body / *_face / *_edge
// is a body index or a global topology id and must never be remapped here.
template<class Visit>
static void for_each_feature_ref(CadFeature& f, Visit&& visit)
{
visit(f.sketch_ref);
visit(f.sweep_path_ref);
visit(f.pattern_curve_sketch);
visit(f.rib_sketch_ref);
visit(f.mate_cs_a);
visit(f.mate_cs_b);
for (int& r : f.loft_profile_refs)
visit(r);
}
bool CadDocument::remove_feature(int index)
{
if (index < 0 || index >= int(features.size()))
@@ -1922,13 +1955,22 @@ bool CadDocument::remove_feature(int index)
std::vector<CadFeature> snapshot = features;
// Deleting a Sketch cascades to every Extrude that consumes it (a dangling
// Extrude would have no wire). A lone Sketch, by contrast, is harmless.
// Deleting a Sketch cascades to every feature that consumes it AS ITS PROFILE — the
// reason is the original one ("a dangling Extrude would have no wire"), and it applies
// unchanged to Revolve, Sweep, Rib and the Surface* family, which all read the profile
// through sketch_ref, plus the sweep spine and the rib line. Testing the FIELD rather
// than the type is what makes that true without a list to keep up to date.
//
// pattern_curve_sketch and loft_profile_refs are deliberately NOT cascaded: a Pattern or
// a Loft that loses one of several inputs is degraded, not meaningless, so those refs go
// to -1 in the remap below and the feature survives. A lone Sketch is harmless either way.
std::vector<int> remove{index};
if (features[index].type == CadFeatureType::Sketch) {
for (int j = 0; j < int(features.size()); ++j)
if (features[j].type == CadFeatureType::Extrude && features[j].sketch_ref == index)
for (int j = 0; j < int(features.size()); ++j) {
const CadFeature& c = features[j];
if (c.sketch_ref == index || c.sweep_path_ref == index || c.rib_sketch_ref == index)
remove.push_back(j);
}
}
std::sort(remove.begin(), remove.end());
remove.erase(std::unique(remove.begin(), remove.end()), remove.end());
@@ -1956,14 +1998,8 @@ bool CadDocument::remove_feature(int index)
ref -= shift;
}
};
for (auto& f : features) {
if (f.type == CadFeatureType::Extrude) {
remap(f.sketch_ref);
} else if (f.type == CadFeatureType::Mate) {
remap(f.mate_cs_a);
remap(f.mate_cs_b);
}
}
for (auto& f : features)
for_each_feature_ref(f, remap);
return commit_or_rollback(*this, snapshot);
}
@@ -1985,14 +2021,8 @@ bool CadDocument::move_feature(int index, int delta)
if (ref == index) ref = target;
else if (ref == target) ref = index;
};
for (auto& f : features) {
if (f.type == CadFeatureType::Extrude) {
swap_ref(f.sketch_ref);
} else if (f.type == CadFeatureType::Mate) {
swap_ref(f.mate_cs_a);
swap_ref(f.mate_cs_b);
}
}
for (auto& f : features)
for_each_feature_ref(f, swap_ref);
return commit_or_rollback(*this, snapshot);
}