From 39ad867fe6b3e010eb490b5441b4258dbd73fcef Mon Sep 17 00:00:00 2001 From: Andrii Shylenko <14119286+w1ne@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:14:38 +0200 Subject: [PATCH 1/2] feat(cookbook): index mechanism/assembly snippets for lookup_cookbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lookup_cookbook` had no reachable mechanism content. Queries for "assembly with parts connectors and mates", "hinge", "revolute joint" and "how do I declare a connector" all returned single-body geometry snippets — the top hit for the first was a snippet about mirroring a symmetric part. That gap has a cost beyond discoverability. A connector origin MUST use the tagged `{ kind: 'vec3', value: [x, y, z] }` form; a bare `[x, y, z]` array crashes the evaluator with an unhandled `Cannot read properties of undefined (reading 'kind')` rather than a validation diagnostic. That tagged form appears in no tool documentation, so the only way to discover it was to read cookbook source the discovery tool could not reach. Add two snippets to the indexed corpus — a two-link arm (connector + revolute mate) and a clamshell hinge — plus the assembly/connector/mate/revolute/hinge/ joint tags, and a test asserting all four queries return a mechanism snippet and that the returned body shows the tagged origin form. Verified: - Both snippets EXECUTE clean against the live engine: ok: true, 2 parts, zero diagnostics. A cookbook snippet that does not run is worse than none. - Negative control: with the two snippets removed, all 5 tests fail; restored, all 5 pass. - src/agent in a checkout with generated assets: 362 passed, 0 failed. - tsc --noEmit clean. Known limitation: this indexes the root `cookbook/` corpus. The worked examples under `src/agent/skills/kernelcad-kinematic/cookbook/*.kcad.ts` (scissor jack, over-centre latch, swept-collision cases) remain unreachable from `lookup_cookbook` and are a separate follow-up. --- .../assembly-connector-and-revolute-mate.md | 36 +++++++++++++++ .../clamshell-hinge-two-part-assembly.md | 34 ++++++++++++++ cookbook/tags.json | 8 +++- src/agent/mcp/tools/mechanismCookbook.test.ts | 44 +++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 cookbook/snippets/assembly-connector-and-revolute-mate.md create mode 100644 cookbook/snippets/clamshell-hinge-two-part-assembly.md create mode 100644 src/agent/mcp/tools/mechanismCookbook.test.ts diff --git a/cookbook/snippets/assembly-connector-and-revolute-mate.md b/cookbook/snippets/assembly-connector-and-revolute-mate.md new file mode 100644 index 000000000..0825719c6 --- /dev/null +++ b/cookbook/snippets/assembly-connector-and-revolute-mate.md @@ -0,0 +1,36 @@ +--- +id: assembly-connector-and-revolute-mate +title: Declare a connector and join parts with a revolute mate +tags: [assembly, connector, mate, revolute, hinge, joint] +keywords: + - assembly with parts connectors and mates + - how do I declare a connector + - revolute joint + - hinge + - axis connector origin + - pivot joint between parts + - declare connector and mate +when_to_use: >- + The model has multiple mechanical parts (not a single fused body) that + need a named pivot between them. Declare an axis connector on each part + with partRef.connector(name, { type: 'axis', origin: { kind: 'vec3', value: + [...] }, axis: [...] }), then join the connectors with arm.mate(name, + 'partA.conn', 'partB.conn', 'revolute', { limitsDeg: [min, max] }). This is + the canonical assembly-topology vocabulary for hinges, elbows, and any + revolute joint — connector origins must use the tagged + { kind: 'vec3', value: [...] } form, not a bare [x, y, z] array. +--- + +```typescript +const arm = assembly('two-link-arm'); + +const base = arm.part('base', box(40, 40, 16)); +const link = arm.part('arm', box(10, 40, 6).translate(0, 20, 0)); + +base.connector('pivot', { type: 'axis', origin: { kind: 'vec3', value: [5, 20, 16] }, axis: [0, 1, 0] }); +link.connector('pivot', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [0, 1, 0] }); + +arm.mate('elbow', 'base.pivot', 'arm.pivot', 'revolute', { limitsDeg: [0, 90] }); + +return arm.solvedModel({ elbow: 45 }); +``` diff --git a/cookbook/snippets/clamshell-hinge-two-part-assembly.md b/cookbook/snippets/clamshell-hinge-two-part-assembly.md new file mode 100644 index 000000000..12ea9dffc --- /dev/null +++ b/cookbook/snippets/clamshell-hinge-two-part-assembly.md @@ -0,0 +1,34 @@ +--- +id: clamshell-hinge-two-part-assembly +title: Clamshell hinge between a base and a lid, joined by a revolute mate +tags: [assembly, connector, mate, revolute, hinge, joint] +keywords: + - hinge + - revolute joint + - clamshell hinge + - lid pivots on base + - assembly with connectors and mates + - declare a connector + - hingeAxis connector +when_to_use: >- + You are modeling a laptop-lid-style clamshell hinge, or any assembly + where one rigid body swings about a fixed pivot line on another rigid + body. Declare a matching axis connector on both bodies along the + physical hinge line, then join them with a revolute mate and limitsDeg + to bound the swing angle. Relevant for hinge, revolute joint, or + assembly with connectors and mates. +--- + +```typescript +const arm = assembly('clamshell-hinge'); + +const base = arm.part('base', box(300, 200, 12).translate(0, 0, 6)); +const lid = arm.part('lid', box(300, 200, 6).translate(0, 100, 3)); + +base.connector('hingeAxis', { type: 'axis', origin: { kind: 'vec3', value: [0, -100, 12] }, axis: [1, 0, 0] }); +lid.connector('hingeAxis', { type: 'axis', origin: { kind: 'vec3', value: [0, 0, 0] }, axis: [1, 0, 0] }); + +arm.mate('hinge', 'base.hingeAxis', 'lid.hingeAxis', 'revolute', { limitsDeg: [-15, 135] }); + +return arm.solvedModel({ hinge: 90 }); +``` diff --git a/cookbook/tags.json b/cookbook/tags.json index 9dd24a90b..d6833f0c4 100644 --- a/cookbook/tags.json +++ b/cookbook/tags.json @@ -23,5 +23,11 @@ "joinery", "bracket", "stacking", - "symmetry" + "symmetry", + "assembly", + "connector", + "mate", + "revolute", + "hinge", + "joint" ] diff --git a/src/agent/mcp/tools/mechanismCookbook.test.ts b/src/agent/mcp/tools/mechanismCookbook.test.ts new file mode 100644 index 000000000..88d533e6c --- /dev/null +++ b/src/agent/mcp/tools/mechanismCookbook.test.ts @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 Andrii Shylenko and kernelCAD contributors +// +// KC-10: lookup_cookbook could not see any mechanism/assembly content — every +// query about connectors, mates, or hinges returned single-body geometry +// snippets (top hit for "assembly with parts, connectors and mates" was +// mirror-half-part). This asserts the cookbook corpus now surfaces the +// mechanism snippets for the queries that used to fall through to +// single-body geometry. +import { describe, it, expect } from 'vitest'; +import { lookupCookbookTool } from './lookupCookbook'; + +const MECHANISM_SNIPPET_IDS = new Set([ + 'assembly-connector-and-revolute-mate', + 'clamshell-hinge-two-part-assembly', +]); + +describe('lookupCookbookTool — mechanism/assembly coverage (KC-10)', () => { + const queries = [ + 'assembly with parts connectors and mates', + 'hinge', + 'revolute joint', + 'how do I declare a connector', + ]; + + for (const query of queries) { + it(`returns a mechanism snippet for "${query}"`, async () => { + const r = await lookupCookbookTool({ query }); + expect(r.ok).toBe(true); + expect(r.hits!.length).toBeGreaterThan(0); + const ids = r.hits!.map((h) => h.id); + expect(ids.some((id) => MECHANISM_SNIPPET_IDS.has(id))).toBe(true); + }); + } + + it('the returned mechanism snippet shows the tagged connector-origin form, not a bare array', async () => { + const r = await lookupCookbookTool({ query: 'how do I declare a connector' }); + const hit = r.hits!.find((h) => MECHANISM_SNIPPET_IDS.has(h.id)); + expect(hit).toBeDefined(); + expect(hit!.body).toContain("origin: { kind: 'vec3', value:"); + expect(hit!.body).toMatch(/type: 'axis'/); + expect(hit!.body).toMatch(/'revolute'/); + }); +}); From 1bc5dba0215e5918c56f5932a56cf412ff512b5b Mon Sep 17 00:00:00 2001 From: Andrii Shylenko <14119286+w1ne@users.noreply.github.com> Date: Tue, 25 Aug 2026 22:46:38 +0200 Subject: [PATCH 2/2] docs(skill): regenerate the cookbook index for the new mechanism snippets `cookbook:build` generates the snippet table in kernelcad-authoring/SKILL.md and CI drift-gates it, so adding snippets without regenerating fails build-and-checks. Regenerated, not hand-edited. --- src/agent/skills/kernelcad-authoring/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/agent/skills/kernelcad-authoring/SKILL.md b/src/agent/skills/kernelcad-authoring/SKILL.md index 94be2ea4c..420c1d7a0 100644 --- a/src/agent/skills/kernelcad-authoring/SKILL.md +++ b/src/agent/skills/kernelcad-authoring/SKILL.md @@ -828,8 +828,10 @@ When you need a canonical pattern, call MCP tool `lookup_cookbook(query, k?)` to | ID | Trigger | |---|---| +| assembly-connector-and-revolute-mate | The model has multiple mechanical parts (not a single fused body) that need a named pivot between them. Declare an axis connector on each part with partRef.connector(name, { type: 'axis', origin: { kind: 'vec3', value: [...] }, axis: [...] }), then join the connectors with arm.mate(name, 'partA.conn', 'partB.conn', 'revolute', { limitsDeg: [min, max] }). This is the canonical assembly-topology vocabulary for hinges, elbows, and any revolute joint — connector origins must use the tagged { kind: 'vec3', value: [...] } form, not a bare [x, y, z] array. | | blind-pocket-from-top | You want a pocket cut into the top face only — the cylinder is shorter than the plate so it does not reach the bottom face. | | chamfer-rotated-face | You rotated a primitive and now want to chamfer one of its canonical faces by name (face-name semantics survive transforms). | +| clamshell-hinge-two-part-assembly | You are modeling a laptop-lid-style clamshell hinge, or any assembly where one rigid body swings about a fixed pivot line on another rigid body. Declare a matching axis connector on both bodies along the physical hinge line, then join them with a revolute mate and limitsDeg to bound the swing angle. Relevant for hinge, revolute joint, or assembly with connectors and mates. | | clearance-hole-through-plate | You need a through-hole sized for a bolt with a small clearance margin; cylinder height extends beyond the plate so the cut is unambiguous. | | extrude-rounded-rect-plate | You want a flat plate with rounded corners; use the dedicated rounded-rect extrude rather than building corners by hand. | | fillet-face-after-subtract | After subtracting a hole or pocket, you want to round only the rim of the resulting opening — not every edge in the part. |