fix(review): make review_cad's fitness and mechanism verdicts agree on joint-primitive assemblies - #690
Merged
Conversation
…mechanism contradiction (KC-04)
kernelCAD has two assembly conventions, both correct:
- `.mate()` + connectors -> `arm.__mates()`
- `.revolute()/.prismatic()/.ball()` (joint primitives, URDF semantics)
-> `arm.__joints()`
Two linked defects fell out of code that only knew about the first.
1. `checkMechanismTruth`'s reachability walk (criterion 4) built its
adjacency from the mate edge list alone. A joint-primitive assembly has
ZERO mates by construction, so every part but the first was reported
`mechanism.orphan-part` and the verdict was 'broken' — on a sound
mechanism. The walk now also reads joint-primitive edges (by FeatureId)
and `connect: { to }` placement edges, which the v0.5 validator has
always treated as structural.
2. `review_cad` could therefore return, IN ONE RESPONSE,
`fitness.functional: true` + `repairMode: 'none'` + "No repair needed.
Preserve the current topology" ALONGSIDE `mechanism: 'broken'`. An agent
trusting `fitness` ships a broken mechanism; one trusting `mechanism`
abandons a sound one. `summarizeMechanismFitness` now folds
error-severity mechanism-truth failures in as blocking reasons, so
`functional` / `repairMode` / `repairDirective` move with the verdict and
the contradiction is structurally impossible, not merely fixed for this
case. 'warn' carriers (sweep-budget-exceeded -> 'unverified') are NOT
folded: "couldn't verify" must not read as "broken".
`mechanism.orphan-part` maps to `repairMode: 'topology-redesign'`.
3. `validateAssemblyWithMates` early-RETURNED at `arm.__mates().length === 0`,
silently disabling every gate below it for joint-primitive assemblies.
It is now a skip of the SOLVER ONLY. Every mate-driven gate already loops
`arm.__mates()` and is inert on an empty mate set, so the no-mates cost is
unchanged — but a gate that does not depend on mates (today
`validateWorkspaceReachability`, and any future one) now actually fires
instead of being dropped. This generalises the `validateJointConventionMix`
precedent that was hoisted above the return for the same reason.
Joint-primitive FK semantics are UNTOUCHED: `origin` stays the parent->child
frame offset and `forwardKinematics` still composes T(o).M.
Tests: 3 new in mechanismTruth.test.ts (sound hinge not orphaned; multi-hop
prismatic+ball chain; a genuinely unconnected part is STILL flagged) and a
new tests/unit/review/jointPrimitiveReviewAgreement.test.ts asserting
fitness/mechanism agreement on both a sound and a broken joint-primitive
assembly.
w1ne
enabled auto-merge
August 25, 2026 20:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect (KC-04)
review_cadon a sound joint-primitive assembly returned this, in a single response:An agent trusting
fitnessships a broken mechanism; one trustingmechanismabandons a sound one. Both cannot be right.Root cause — three places knew about only one of the two conventions
checkOrphanParts(src/modeling/runtime/mechanismTruth.ts) built adjacency fromarm.__mates()alone. A joint-primitive assembly has zero mates by construction, so every part afterparts[0]was reported orphaned and the verdict wasbroken.summarizeMechanismFitnessnever saw mechanism-truth failures at all — it reads the validator/envelope streams only, and independently concludedfunctional: true.reviewPipelinefolded the verdict intookbut leftfitnessuntouched. That is why one response disagreed with itself.validateAssemblyWithMatesearly-returned atarm.__mates().length === 0, reading "no mates" as "nothing left to validate" and disabling every gate below for joint-primitive assemblies.After:
"ok": true, "mechanism": "real", "mechanismFailures": [], fitness unchanged and now consistent.The change
The connectivity walk adds joint-primitive edges (joints address parts by
FeatureId, so a name map is built) andconnect: { to }placement edges, which the v0.5 validator has always treated as structural. Fitness now receives mechanism-truth failures, so the contradiction is structurally impossible rather than fixed for one case —error-severity truth failures become blocking reasons, whilewarncarriers (mechanism.sweep-budget-exceeded→unverified) are deliberately not folded, so "couldn't verify" never reads as "broken". The early return became a solver-only skip; every mate-driven gate already loopsarm.__mates()and is inert on an empty set, so the no-mates cost is unchanged, whilevalidateWorkspaceReachabilitynow actually fires instead of being dropped.Registry hints no longer tell an agent to "add a mate" when a joint primitive is the right fix. No new diagnostic code, so the 248 catalogue counts are untouched.
Safety
Joint-primitive FK semantics are untouched —
originremains the parent→child frame offset andforwardKinematicsstill composesT(o)·M.git diffagainstforwardKinematics.tsandsolver.tsis empty. The robotics tests that caught an earlier bad attempt at this area stay green:tests/unit/assemblies/forwardKinematics.test.tsandtests/unit/kinematic/checkReachableMultiDof.test.ts→ 5 passed.Verification (re-run independently in a checkout with generated assets)
tsc --noEmit→ clean.src/modeling src/kinematic src/agent→ 1183 passed, 0 failed.tests/→ 3370 passed, 0 test failures (one file fails to collect: the knownlightningcss.darwin-arm64.nodedarwin-only issue).mechanismTruth.tsalone → 6 fail, including the orphan diagnostic reappearing verbatim andexpected 'broken' to be 'real'; restored → 16 pass. The "genuinely broken" test fails differently under revert (expected 2 orphans, wanted 1), confirming it discriminates rather than merely passing.Deliberately left undone
assembly.mate.limit-missing— a.revolute()with nolimitsDegis still invisible to envelope review. Needs a new diagnostic code and the 248→249 bumps; larger than the contradiction fix. The structural blocker is removed, so future gates apply automatically.mechanicalPlausibility.tshas a third connectivity walk (assembly.mechanical.part-disconnected, readingarm.__joints()). Not implicated in the reported output, but worth consolidating so three walks don't drift apart again.'fixed'joint kind is handled generically but untested —arm.fixed()was removed in G0, so there is no public API to create one.