Skip to content

fix(review): make review_cad's fitness and mechanism verdicts agree on joint-primitive assemblies - #690

Merged
w1ne merged 1 commit into
developfrom
fix/validator-joint-primitive-agreement
Aug 25, 2026
Merged

fix(review): make review_cad's fitness and mechanism verdicts agree on joint-primitive assemblies#690
w1ne merged 1 commit into
developfrom
fix/validator-joint-primitive-agreement

Conversation

@w1ne

@w1ne w1ne commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The defect (KC-04)

review_cad on a sound joint-primitive assembly returned this, in a single response:

"ok": false,  "mechanism": "broken",
"fitness": { "functional": true, "repairMode": "none",
             "repairDirective": "No repair needed. Preserve the current topology…" }
"mechanismFailures": [ mechanism.orphan-part —
   "Part 'arm' is not reachable from the mate graph (no mate edge connects it to 'base'…)" ]

An agent trusting fitness ships a broken mechanism; one trusting mechanism abandons a sound one. Both cannot be right.

Root cause — three places knew about only one of the two conventions

  1. checkOrphanParts (src/modeling/runtime/mechanismTruth.ts) built adjacency from arm.__mates() alone. A joint-primitive assembly has zero mates by construction, so every part after parts[0] was reported orphaned and the verdict was broken.
  2. summarizeMechanismFitness never saw mechanism-truth failures at all — it reads the validator/envelope streams only, and independently concluded functional: true. reviewPipeline folded the verdict into ok but left fitness untouched. That is why one response disagreed with itself.
  3. validateAssemblyWithMates early-returned at arm.__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) and connect: { 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, while warn carriers (mechanism.sweep-budget-exceededunverified) 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 loops arm.__mates() and is inert on an empty set, so the no-mates cost is unchanged, while validateWorkspaceReachability now 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 untouchedorigin remains the parent→child frame offset and forwardKinematics still composes T(o)·M. git diff against forwardKinematics.ts and solver.ts is empty. The robotics tests that caught an earlier bad attempt at this area stay green: tests/unit/assemblies/forwardKinematics.test.ts and tests/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/agent1183 passed, 0 failed.
  • tests/3370 passed, 0 test failures (one file fails to collect: the known lightningcss.darwin-arm64.node darwin-only issue).
  • Negative control: reverting mechanismTruth.ts alone → 6 fail, including the orphan diagnostic reappearing verbatim and expected '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

  • No joint-primitive analogue of assembly.mate.limit-missing — a .revolute() with no limitsDeg is 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.ts has a third connectivity walk (assembly.mechanical.part-disconnected, reading arm.__joints()). Not implicated in the reported output, but worth consolidating so three walks don't drift apart again.
  • The 'fixed' joint kind is handled generically but untested — arm.fixed() was removed in G0, so there is no public API to create one.

…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
w1ne enabled auto-merge August 25, 2026 20:45
@w1ne
w1ne merged commit 3c06850 into develop Aug 25, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant