Skip to content

fix(studio): Validity panel reports the loaded model, not a vacuous green - #689

Merged
w1ne merged 1 commit into
developfrom
fix/studio-validity-truthful
Aug 25, 2026
Merged

fix(studio): Validity panel reports the loaded model, not a vacuous green#689
w1ne merged 1 commit into
developfrom
fix/studio-validity-truthful

Conversation

@w1ne

@w1ne w1ne commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The defect (KC-06)

Open a saved project with 2 parts and 1 revolute joint on app.kernelcad.com. The Validity tab renders:

solved · 0 parts · 0 joints · 0 diagnostics

…while the Scene tab lists both parts and the status bar reads "2 bodies". A green verdict computed over an empty set — worse than no panel, because it tells the user their mechanism validated when nothing was checked.

Root cause — two independent defects, both in src/studio/**

My initial hypothesis (the joint-primitive vs mate split) was wrong for the counts. It was right for the Joints tab.

1. The counts were literally hardcoded. src/studio/adapters/reviewToValidity.ts:61-62 on develop:

partCount: 0,
jointCount: 0,

Every model reported 0/0 — a .mate()-based model too. The file described itself as a "conservative Phase 1.1 adapter"; the deeper payload it was waiting for had already landed. src/agent/review/reviewPipeline.ts:262-267 returns validator: { status, partCount, jointCount } on both branches, but Studio's ScriptReviewSummary never declared the field, so the adapter never read it.

2. The green was fabricated. GeometryContext.tsx:988 (hosted path) and vite.config.ts:630:

const hostedReview = payload.review ?? { ok: true, diagnostics: [] };

deriveStatus reads if (review.ok) return 'solved'. So a placeholder no validator ever touched rendered as a pass.

3. Joints tab — this one is the joint/mate split. extractJointSnapshots read only metadata.mates, and __buildMateMetadata() returns undefined when mates.length === 0. Joint primitives capture as their own assemblyJoint records, with poses on the solvedAssembly record — reachable all along.

The change

Counts prefer the server's validator block and fall back to counting the loaded model's feature records. reviewWasValidated() derives provenance instead of trusting a flag a server could forget to set. When a review did not actually run, the chip reads not run (grey) with an explanatory notice — and only the passing verdict is suppressed; a real error/warning still reports as itself, covered by its own test. Joint primitives now produce joint snapshots, so the Joints tab populates.

Deliberately scoped to src/studio/**. Adding a 'not run' member to ValidatorStatus would have rippled through exhaustive switches across modeling and collided with concurrent work, so StudioValidity carries it Studio-side.

Verification

  • tsc --noEmit clean; eslint clean on all 9 changed files.
  • npx vitest run src/studio99 files, 654 tests, all passing (re-run independently in a checkout with generated assets).
  • Negative controls — three separate sabotages, each restored:
Reverted Result
counts back to 0, 0 5 fail — expected +0 to be 2, expected +0 to be 5
reviewWasValidatedreturn true 3 fail — incl. expected 'solved' to be 'not run'
joint-primitive block removed 1 fail — expected [] to have length 1

I independently re-ran the second one — the one that guards the actual reported symptom — and reproduced expected 'solved' to be 'not run' exactly.

tests/ failures in a fresh worktree are all environmental (missing gitignored assets/parts/, unbuilt dist/cli, kernelcad not on PATH, the known lightningcss.darwin-arm64.node); none import src/studio.

Two things for reviewer judgement

  1. One existing test was updated deliberately: ValidityTab.test.tsx's makeValidity helper now sets validated: true, since a hand-built validator result stands for a validation that ran. Without it, every existing green-chip assertion would read not run.
  2. The upstream half is not fixed here. The hosted mesh path ships responses with no review block, and the live=1 short-circuit is used for session-backed initial load. So on hosted load the panel will honestly read "not run" until the user presses Validate. If a saved project is meant to arrive already validated, that fix belongs on the server/review side — this PR stops the lie rather than papering over it.

…reen

The Validity tab rendered 'solved - 0 parts - 0 joints - 0 diagnostics'
for a saved project whose model has 2 parts and 1 revolute joint, while
the Scene tab listed both parts on the same page. Two defects stacked.

1. reviewToValidity() hardcoded partCount: 0 and jointCount: 0. Its own
   header called this a conservative Phase 1.1 adapter pending a deeper
   server payload; the deeper payload had in fact already landed
   (reviewPipeline returns a validator block with real counts on both
   its ok and not-ok branches) but the adapter never read it, so EVERY
   model reported zero regardless of how it was declared.

2. When a mesh response carries no review block - which is every
   session-backed load, plus the dev endpoint's live=1 short-circuit -
   GeometryContext substitutes { ok: true, diagnostics: [] } and
   deriveStatus turned that ok into 'solved'. A green verdict computed
   over nothing.

Counts now come from the server's validator block when present and from
the loaded model's own featureRecords otherwise, so the panel reports
what the model declares. Provenance is derived (not flagged) from the
payload: a real reviewCadTool result always carries validator, fitness
and a mechanism verdict, a review that found something carries
diagnostics; a payload with none of those is marked unvalidated and the
chip reads 'not run' with an explanatory notice. Only the PASSING
verdict is suppressed - a real failure still reports as itself.

Joints tab: extractJointSnapshots() only read metadata.mates, so a model
built with joint primitives (asm.revolute) produced an empty list, which
emptied the tab and greyed it out via getVisibleTabs. Joint primitives
capture as their own assemblyJoint records with their pose on the
solvedAssembly record's pose map; both vocabularies are now read.
@w1ne
w1ne merged commit 115a262 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