@vcad/mcp 0.9.4-main.35 → 0.9.4-main.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,4 +6,4 @@ vcad's MCP server as a self-contained bundle (server + BRep kernel WASM).
6
6
  { "mcpServers": { "vcad": { "command": "npx", "args": ["-y", "@vcad/mcp"] } } }
7
7
  ```
8
8
 
9
- Built from 05b902db41f28989ab2bae377db68a55dd2a7121 at 2026-07-26T13:07:16.613Z. Source: https://github.com/ecto/vcad
9
+ Built from 254d4e9d13825ac74a895590703959d77898e1f3 at 2026-07-26T13:42:07.736Z. Source: https://github.com/ecto/vcad
package/index.mjs CHANGED
@@ -11559,6 +11559,7 @@ var init_dist = __esm({
11559
11559
  circuitTransient: wasmModule7.circuitTransient,
11560
11560
  circuitTune: wasmModule7.circuitTune,
11561
11561
  feaAnalyzeMesh: wasmModule7.feaAnalyzeMesh,
11562
+ feaCheckBeam: wasmModule7.feaCheckBeam,
11562
11563
  emSimulate: wasmModule7.emSimulate,
11563
11564
  antennaAnalyze: wasmModule7.antennaAnalyze,
11564
11565
  photonicsSimulate: wasmModule7.photonicsSimulate,
@@ -11887,6 +11888,22 @@ var init_dist = __esm({
11887
11888
  }
11888
11889
  return fn(specJson, optionsJson, positions, indices);
11889
11890
  }
11891
+ /**
11892
+ * Closed-form check of a prismatic member: exact section properties,
11893
+ * beam bending with the Timoshenko shear term, Bredt thin-wall (or
11894
+ * Saint-Venant series) torsion, and Euler buckling — the route for
11895
+ * thin-walled sheet-metal and tube-frame members, which no affordable
11896
+ * lattice pitch can resolve. Same fail-closed contract and
11897
+ * `vcad.fea-claims/1` predicted claims as `feaAnalyzeMesh`; see
11898
+ * `vcad-kernel-fea::section`.
11899
+ */
11900
+ feaCheckBeam(caseJson) {
11901
+ const fn = this.kernel.feaCheckBeam;
11902
+ if (typeof fn !== "function") {
11903
+ throw new Error("feaCheckBeam is not exported by this kernel WASM build \u2014 rebuild packages/kernel-wasm");
11904
+ }
11905
+ return fn(caseJson);
11906
+ }
11890
11907
  /**
11891
11908
  * EM field simulation (2D/axisymmetric finite-volume magnetostatics or
11892
11909
  * electrostatics): inductance, force, torque, capacitance extraction and
@@ -40914,6 +40931,25 @@ var init_CHANGELOG = __esm({
40914
40931
  CHANGELOG_default = {
40915
40932
  $schema: "./changelog.schema.json",
40916
40933
  entries: [
40934
+ {
40935
+ id: "2026-07-26-swept-and-all-pairs-clearance",
40936
+ version: "0.9.4",
40937
+ date: "2026-07-26",
40938
+ category: "feat",
40939
+ title: "check_clearance sweeps the range of motion",
40940
+ summary: "Pass `sweep` to drive joints through their travel and get the worst pose, or omit both groups to audit every part pair in the document for interference.",
40941
+ features: [
40942
+ "clearance",
40943
+ "assembly",
40944
+ "kinematics",
40945
+ "mcp"
40946
+ ],
40947
+ mcpTools: [
40948
+ "check_clearance",
40949
+ "build_receipt",
40950
+ "verify_receipt"
40951
+ ]
40952
+ },
40917
40953
  {
40918
40954
  id: "2026-07-26-render-loon-directly",
40919
40955
  version: "0.9.4",
@@ -40965,6 +41001,25 @@ var init_CHANGELOG = __esm({
40965
41001
  "export_cad"
40966
41002
  ]
40967
41003
  },
41004
+ {
41005
+ id: "2026-07-26-fastener-forms",
41006
+ version: "0.9.4",
41007
+ date: "2026-07-26",
41008
+ category: "feat",
41009
+ title: "Fasteners are a first-class form",
41010
+ summary: "bolt, bolt-circle, clearance-hole and tapped-hole: catalog-backed hardware oriented by its own axis, with real countersunk heads, length checks, and BOM lines counted off the geometry.",
41011
+ features: [
41012
+ "loon",
41013
+ "fasteners",
41014
+ "bom",
41015
+ "assembly"
41016
+ ],
41017
+ mcpTools: [
41018
+ "create_cad_loon",
41019
+ "bom_create",
41020
+ "search_mechanical_parts"
41021
+ ]
41022
+ },
40968
41023
  {
40969
41024
  id: "2026-07-26-document-source-provenance",
40970
41025
  version: "0.9.4",
@@ -40984,6 +41039,23 @@ var init_CHANGELOG = __esm({
40984
41039
  "load_document"
40985
41040
  ]
40986
41041
  },
41042
+ {
41043
+ id: "2026-07-26-beam-check-thin-walled-fea",
41044
+ version: "0.9.4",
41045
+ date: "2026-07-26",
41046
+ category: "feat",
41047
+ title: "beam_check: audited structural answer for thin walls",
41048
+ summary: "Sheet-metal and tube-frame members had no audited FEA answer at any resolution; beam_check prices them in closed form (Bredt torsion, beam bending, Euler buckling), and analyze_structure now measures the wall and refuses with the cell arithmetic instead of a bare Unverifiable.",
41049
+ features: [
41050
+ "fea",
41051
+ "sheet-metal",
41052
+ "receipts"
41053
+ ],
41054
+ mcpTools: [
41055
+ "beam_check",
41056
+ "analyze_structure"
41057
+ ]
41058
+ },
40987
41059
  {
40988
41060
  id: "2026-07-26-backwards-extrude-inverted-winding",
40989
41061
  version: "0.9.4",
@@ -53305,6 +53377,7 @@ var init_tool_metadata = __esm({
53305
53377
  solve_thermal: { title: "Solve Thermal", annotations: RO },
53306
53378
  simulate_flow: { title: "Simulate Flow", annotations: RO },
53307
53379
  analyze_structure: { title: "Analyze Structure", annotations: RO },
53380
+ beam_check: { title: "Check Beam / Section", annotations: RO },
53308
53381
  simulate_neutron_shield: {
53309
53382
  title: "Simulate Neutron Shield",
53310
53383
  annotations: RO
@@ -55576,10 +55649,23 @@ function clearanceVerdict(distanceMm) {
55576
55649
  if (distanceMm <= CONTACT_EPS_MM) return "touching";
55577
55650
  return "clear";
55578
55651
  }
55579
- function clearanceHolds(distanceMm, minMm, allowContact) {
55652
+ function clearanceHolds(distanceMm, minMm, allowContact, intersecting = false) {
55653
+ if (intersecting) return false;
55580
55654
  return distanceMm >= minMm || allowContact && clearanceVerdict(distanceMm) === "touching";
55581
55655
  }
55582
- function partCandidates(doc, engine) {
55656
+ function worseThan(a, b) {
55657
+ if (a.intersecting !== b.intersecting) return a.intersecting;
55658
+ return a.distance < b.distance;
55659
+ }
55660
+ function placeMesh(mesh, transform2) {
55661
+ if (!transform2) return mesh;
55662
+ return transformMesh(mesh, {
55663
+ translate: transform2.translation,
55664
+ rotate: transform2.rotation,
55665
+ scale: transform2.scale
55666
+ });
55667
+ }
55668
+ function poseableParts(doc, engine) {
55583
55669
  const scene = engine.evaluate(doc);
55584
55670
  const visibleRoots = doc.roots.filter((e) => e.visible !== false);
55585
55671
  const out = [];
@@ -55588,23 +55674,30 @@ function partCandidates(doc, engine) {
55588
55674
  const node = doc.nodes[String(rootId)];
55589
55675
  const mesh = scene.parts[i].mesh;
55590
55676
  if (!mesh || mesh.positions.length === 0) continue;
55591
- out.push({ id: String(rootId), name: node?.name ?? void 0, mesh });
55677
+ out.push({ id: String(rootId), name: node?.name ?? void 0, localMesh: mesh });
55592
55678
  }
55593
55679
  for (const inst of scene.instances ?? []) {
55594
- const mesh = inst.transform ? transformMesh(inst.mesh, {
55595
- translate: inst.transform.translation,
55596
- rotate: inst.transform.rotation,
55597
- scale: inst.transform.scale
55598
- }) : inst.mesh;
55599
- if (!mesh || mesh.positions.length === 0) continue;
55680
+ if (!inst.mesh || inst.mesh.positions.length === 0) continue;
55600
55681
  out.push({
55601
55682
  id: inst.instanceId,
55602
55683
  name: inst.name ?? void 0,
55603
- mesh
55684
+ localMesh: inst.mesh,
55685
+ transform: inst.transform ?? void 0,
55686
+ instanceId: inst.instanceId
55604
55687
  });
55605
55688
  }
55606
55689
  return out;
55607
55690
  }
55691
+ function placeParts(parts, worldTransforms) {
55692
+ const out = [];
55693
+ for (const p of parts) {
55694
+ const transform2 = (p.instanceId ? worldTransforms?.get(p.instanceId) : void 0) ?? p.transform;
55695
+ const mesh = placeMesh(p.localMesh, transform2);
55696
+ if (!mesh || mesh.positions.length === 0) continue;
55697
+ out.push({ id: p.id, ...p.name ? { name: p.name } : {}, mesh });
55698
+ }
55699
+ return out;
55700
+ }
55608
55701
  function resolveGroup(candidates, ids) {
55609
55702
  const parts = /* @__PURE__ */ new Map();
55610
55703
  const missing = [];
@@ -55616,16 +55709,115 @@ function resolveGroup(candidates, ids) {
55616
55709
  }
55617
55710
  return { parts: [...parts.values()], missing };
55618
55711
  }
55619
- function computeGroupClearance(doc, engine, groupA, groupB) {
55712
+ function resolveSweepAxes(doc, axes) {
55713
+ const joints = doc.joints ?? [];
55714
+ const resolved = [];
55715
+ for (const axis of axes) {
55716
+ const wanted = String(axis.joint);
55717
+ const found = joints.find((j) => j.id === wanted) ?? joints.find((j) => j.name === wanted);
55718
+ if (!found) {
55719
+ const available = joints.map((j) => `${j.id}${j.name ? ` (${j.name})` : ""}`).join(", ");
55720
+ return { error: `No joint with id or name "${wanted}". Available: ${available || "none"}` };
55721
+ }
55722
+ if (!Number.isFinite(axis.from) || !Number.isFinite(axis.to)) {
55723
+ return { error: `Sweep axis "${wanted}" needs finite \`from\` and \`to\`.` };
55724
+ }
55725
+ const steps = Math.trunc(axis.steps);
55726
+ if (!Number.isFinite(steps) || steps < 1) {
55727
+ return { error: `Sweep axis "${wanted}" needs \`steps\` >= 1.` };
55728
+ }
55729
+ resolved.push({ joint: found.id, from: axis.from, to: axis.to, steps });
55730
+ }
55731
+ if (resolved.length === 0) return { error: "`sweep` needs at least one joint axis." };
55732
+ const total = resolved.reduce((n, a) => n * (a.steps + 1), 1);
55733
+ if (total > MAX_SWEEP_POSES) {
55734
+ return {
55735
+ error: `Sweep grid is ${total} poses (limit ${MAX_SWEEP_POSES}). Lower \`steps\` or sweep fewer joints at once.`
55736
+ };
55737
+ }
55738
+ return { axes: resolved };
55739
+ }
55740
+ function poseGrid(axes) {
55741
+ let poses = [[]];
55742
+ for (const axis of axes) {
55743
+ const n = Math.trunc(axis.steps);
55744
+ const next = [];
55745
+ for (const pose of poses) {
55746
+ for (let i = 0; i <= n; i++) {
55747
+ const state = axis.from + (axis.to - axis.from) * i / n;
55748
+ next.push([...pose, { joint: axis.joint, state }]);
55749
+ }
55750
+ }
55751
+ poses = next;
55752
+ }
55753
+ return poses;
55754
+ }
55755
+ function poseTransforms(doc, poses) {
55756
+ const joints = doc.joints ?? [];
55757
+ const byId = new Map(joints.map((j) => [j.id, j]));
55758
+ const saved = joints.map((j) => j.state);
55759
+ try {
55760
+ return poses.map((pose) => {
55761
+ for (const { joint, state } of pose) {
55762
+ const j = byId.get(joint);
55763
+ if (j) j.state = state;
55764
+ }
55765
+ return solveForwardKinematics2(doc);
55766
+ });
55767
+ } finally {
55768
+ joints.forEach((j, i) => {
55769
+ j.state = saved[i];
55770
+ });
55771
+ }
55772
+ }
55773
+ function computeGroupClearance(doc, engine, groupA, groupB, sweep) {
55620
55774
  if (groupA.length === 0 || groupB.length === 0) {
55621
55775
  return { error: "Both `group_a` and `group_b` need at least one part id." };
55622
55776
  }
55623
- let candidates;
55777
+ let base;
55778
+ try {
55779
+ base = poseableParts(doc, engine);
55780
+ } catch (e) {
55781
+ return { error: e instanceof Error ? e.message : String(e) };
55782
+ }
55783
+ if (!sweep || sweep.length === 0) {
55784
+ return measureGroups(engine, placeParts(base), groupA, groupB);
55785
+ }
55786
+ const { axes, error: sweepError } = resolveSweepAxes(doc, sweep);
55787
+ if (sweepError || !axes) return { error: sweepError ?? "sweep could not be resolved" };
55788
+ const poses = poseGrid(axes);
55789
+ let transforms;
55624
55790
  try {
55625
- candidates = partCandidates(doc, engine);
55791
+ transforms = poseTransforms(doc, poses);
55626
55792
  } catch (e) {
55627
55793
  return { error: e instanceof Error ? e.message : String(e) };
55628
55794
  }
55795
+ let worst2;
55796
+ for (let i = 0; i < poses.length; i++) {
55797
+ const { result, error: error2 } = measureGroups(
55798
+ engine,
55799
+ placeParts(base, transforms[i]),
55800
+ groupA,
55801
+ groupB
55802
+ );
55803
+ if (error2 || !result) return { error: error2 };
55804
+ if (!worst2 || worseThan(
55805
+ { distance: result.distance_mm, intersecting: result.intersecting },
55806
+ { distance: worst2.result.distance_mm, intersecting: worst2.result.intersecting }
55807
+ )) {
55808
+ worst2 = { result, pose: poses[i] };
55809
+ }
55810
+ }
55811
+ if (!worst2) return { error: "Sweep produced no poses to measure." };
55812
+ return {
55813
+ result: {
55814
+ ...worst2.result,
55815
+ worst_pose: worst2.pose.map((p) => ({ joint: p.joint, state: round6(p.state) })),
55816
+ poses_checked: poses.length
55817
+ }
55818
+ };
55819
+ }
55820
+ function measureGroups(engine, candidates, groupA, groupB) {
55629
55821
  const a = resolveGroup(candidates, groupA);
55630
55822
  const b = resolveGroup(candidates, groupB);
55631
55823
  const missing = [...a.missing, ...b.missing];
@@ -55652,7 +55844,7 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
55652
55844
  return { error: e instanceof Error ? e.message : String(e) };
55653
55845
  }
55654
55846
  pairs += 1;
55655
- if (!worst2 || r.distance < worst2.r.distance) {
55847
+ if (!worst2 || worseThan(r, worst2.r)) {
55656
55848
  worst2 = { a: pa, b: pb, r };
55657
55849
  }
55658
55850
  }
@@ -55677,6 +55869,132 @@ function computeGroupClearance(doc, engine, groupA, groupB) {
55677
55869
  }
55678
55870
  };
55679
55871
  }
55872
+ function meshAabb(mesh) {
55873
+ const p = mesh.positions;
55874
+ const min = [Infinity, Infinity, Infinity];
55875
+ const max = [-Infinity, -Infinity, -Infinity];
55876
+ for (let i = 0; i + 2 < p.length; i += 3) {
55877
+ for (let k = 0; k < 3; k++) {
55878
+ const v = p[i + k];
55879
+ if (v < min[k]) min[k] = v;
55880
+ if (v > max[k]) max[k] = v;
55881
+ }
55882
+ }
55883
+ return { min, max };
55884
+ }
55885
+ function aabbNear(a, b, margin) {
55886
+ for (let k = 0; k < 3; k++) {
55887
+ if (a.min[k] - margin > b.max[k]) return false;
55888
+ if (b.min[k] - margin > a.max[k]) return false;
55889
+ }
55890
+ return true;
55891
+ }
55892
+ function computeInterferenceAudit(doc, engine, opts) {
55893
+ let base;
55894
+ try {
55895
+ base = poseableParts(doc, engine);
55896
+ } catch (e) {
55897
+ return { error: e instanceof Error ? e.message : String(e) };
55898
+ }
55899
+ let poses = [];
55900
+ let transforms = [void 0];
55901
+ if (opts.sweep && opts.sweep.length > 0) {
55902
+ const { axes, error: error2 } = resolveSweepAxes(doc, opts.sweep);
55903
+ if (error2 || !axes) return { error: error2 ?? "sweep could not be resolved" };
55904
+ poses = poseGrid(axes);
55905
+ try {
55906
+ transforms = poseTransforms(doc, poses);
55907
+ } catch (e) {
55908
+ return { error: e instanceof Error ? e.message : String(e) };
55909
+ }
55910
+ }
55911
+ const first = placeParts(base, transforms[0]);
55912
+ if (first.length < 2) {
55913
+ return { error: "Fewer than two measurable parts \u2014 nothing to audit." };
55914
+ }
55915
+ const ignore = /* @__PURE__ */ new Set();
55916
+ const unresolved = [];
55917
+ const resolveId = (raw) => {
55918
+ const wanted = String(raw);
55919
+ return (first.find((c) => c.id === wanted) ?? first.find((c) => c.name === wanted))?.id;
55920
+ };
55921
+ for (const [rawA, rawB] of opts.ignorePairs) {
55922
+ const a = resolveId(rawA);
55923
+ const b = resolveId(rawB);
55924
+ if (!a) unresolved.push(String(rawA));
55925
+ if (!b) unresolved.push(String(rawB));
55926
+ if (a && b) ignore.add(pairKey(a, b));
55927
+ }
55928
+ for (const joint of doc.joints ?? []) {
55929
+ const parent = joint.parentInstanceId;
55930
+ if (!parent) continue;
55931
+ const fixed = joint.kind?.type === "Fixed";
55932
+ if (opts.ignoreAdjacent || fixed && opts.ignoreFixedJoints) {
55933
+ ignore.add(pairKey(parent, joint.childInstanceId));
55934
+ }
55935
+ }
55936
+ const margin = Math.max(opts.minMm, 0);
55937
+ const worstByPair = /* @__PURE__ */ new Map();
55938
+ const broadphase = /* @__PURE__ */ new Set();
55939
+ let queries = 0;
55940
+ let ignored = 0;
55941
+ for (let p = 0; p < transforms.length; p++) {
55942
+ const parts = p === 0 ? first : placeParts(base, transforms[p]);
55943
+ const boxes = parts.map((c) => meshAabb(c.mesh));
55944
+ for (let i = 0; i < parts.length; i++) {
55945
+ for (let j = i + 1; j < parts.length; j++) {
55946
+ const key = pairKey(parts[i].id, parts[j].id);
55947
+ if (ignore.has(key)) {
55948
+ if (p === 0) ignored += 1;
55949
+ continue;
55950
+ }
55951
+ if (!aabbNear(boxes[i], boxes[j], margin + CONTACT_EPS_MM)) continue;
55952
+ broadphase.add(key);
55953
+ let r;
55954
+ try {
55955
+ r = engine.meshClearance(parts[i].mesh, parts[j].mesh);
55956
+ } catch (e) {
55957
+ return { error: e instanceof Error ? e.message : String(e) };
55958
+ }
55959
+ queries += 1;
55960
+ const prev = worstByPair.get(key);
55961
+ if (!prev || worseThan(r, prev.r)) {
55962
+ worstByPair.set(key, {
55963
+ a: parts[i],
55964
+ b: parts[j],
55965
+ r,
55966
+ ...poses.length > 0 ? { pose: poses[p] } : {}
55967
+ });
55968
+ }
55969
+ }
55970
+ }
55971
+ }
55972
+ const named = (p) => ({ id: p.id, ...p.name ? { name: p.name } : {} });
55973
+ const findings = [...worstByPair.values()].filter((w) => !clearanceHolds(round6(w.r.distance), opts.minMm, false, w.r.intersecting)).map((w) => ({
55974
+ a: named(w.a),
55975
+ b: named(w.b),
55976
+ distance_mm: round6(w.r.distance),
55977
+ verdict: w.r.intersecting ? "intersecting" : clearanceVerdict(round6(w.r.distance)),
55978
+ point_a: w.r.pointA.map(round6),
55979
+ point_b: w.r.pointB.map(round6),
55980
+ ...w.pose ? { worst_pose: w.pose.map((s) => ({ joint: s.joint, state: round6(s.state) })) } : {}
55981
+ })).sort(
55982
+ (x, y) => Number(y.verdict === "intersecting") - Number(x.verdict === "intersecting") || x.distance_mm - y.distance_mm
55983
+ );
55984
+ const n = first.length;
55985
+ return {
55986
+ result: {
55987
+ parts_checked: n,
55988
+ pairs_total: n * (n - 1) / 2,
55989
+ pairs_ignored: ignored,
55990
+ pairs_broadphase: broadphase.size,
55991
+ queries,
55992
+ ...poses.length > 0 ? { poses_checked: poses.length } : {},
55993
+ findings,
55994
+ unresolved_ignores: [...new Set(unresolved)]
55995
+ }
55996
+ };
55997
+ }
55680
55998
  function upsertClearanceSpec(doc, spec) {
55681
55999
  const specs = doc.clearance_specs ?? [];
55682
56000
  const idx = specs.findIndex((s) => s.label === spec.label);
@@ -55689,16 +56007,13 @@ async function checkClearance(args, engine) {
55689
56007
  if (!documentId) return err3("Pass `document_id` (the CAD session).");
55690
56008
  const groupA = stringArray(args.group_a);
55691
56009
  const groupB = stringArray(args.group_b);
55692
- if (!groupA || !groupB) {
55693
- return err3("Pass `group_a` and `group_b` as arrays of part ids (or part names).");
55694
- }
55695
- const minMm = typeof args.min_mm === "number" ? args.min_mm : NaN;
55696
- if (!Number.isFinite(minMm)) return err3("Pass `min_mm`, the required minimum separation in mm.");
55697
56010
  const label = typeof args.label === "string" && args.label.trim() ? args.label.trim() : void 0;
55698
56011
  const allowContact = asBool(args.allow_contact);
56012
+ const { sweep, error: sweepParseError } = parseSweep(args.sweep);
56013
+ if (sweepParseError) return err3(sweepParseError);
55699
56014
  if (label && args.joint_state !== void 0 && args.joint_state !== null) {
55700
56015
  return err3(
55701
- "`label` and `joint_state` cannot be combined: a persisted clearance spec is re-measured at the document's stored joint states, so an assertion captured at an ad-hoc pose could not be re-verified. Either drop `label` (one-off pose measurement), or set the joint states on the document and assert there."
56016
+ "`label` and `joint_state` cannot be combined: a persisted clearance spec is re-measured at the document's stored joint states, so an assertion captured at an ad-hoc pose could not be re-verified. Either drop `label` (one-off pose measurement), use `sweep` (persisted with the spec and re-verified over the same range), or set the joint states on the document and assert there."
55702
56017
  );
55703
56018
  }
55704
56019
  const stored = getSession(documentId);
@@ -55709,10 +56024,20 @@ async function checkClearance(args, engine) {
55709
56024
  } catch (e) {
55710
56025
  return err3(e instanceof Error ? e.message : String(e));
55711
56026
  }
55712
- const { result, error: error2 } = computeGroupClearance(doc, engine, groupA, groupB);
56027
+ if (!groupA && !groupB) {
56028
+ return auditClearance(documentId, doc, engine, args, sweep, label, pose);
56029
+ }
56030
+ if (!groupA || !groupB) {
56031
+ return err3(
56032
+ "Pass both `group_a` and `group_b` as arrays of part ids (or part names) \u2014 or neither, to audit every pair in the document."
56033
+ );
56034
+ }
56035
+ const minMm = typeof args.min_mm === "number" ? args.min_mm : NaN;
56036
+ if (!Number.isFinite(minMm)) return err3("Pass `min_mm`, the required minimum separation in mm.");
56037
+ const { result, error: error2 } = computeGroupClearance(doc, engine, groupA, groupB, sweep);
55713
56038
  if (error2 || !result) return err3(error2 ?? "Clearance could not be computed.");
55714
56039
  const verdict = clearanceVerdict(result.distance_mm);
55715
- const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact);
56040
+ const pass2 = clearanceHolds(result.distance_mm, minMm, allowContact, result.intersecting);
55716
56041
  let specSaved = false;
55717
56042
  if (label) {
55718
56043
  upsertClearanceSpec(stored, {
@@ -55720,7 +56045,8 @@ async function checkClearance(args, engine) {
55720
56045
  group_a: result.group_a.map((p) => p.id),
55721
56046
  group_b: result.group_b.map((p) => p.id),
55722
56047
  min_mm: minMm,
55723
- ...allowContact ? { allow_contact: true } : {}
56048
+ ...allowContact ? { allow_contact: true } : {},
56049
+ ...sweep && sweep.length > 0 ? { sweep } : {}
55724
56050
  });
55725
56051
  specSaved = true;
55726
56052
  }
@@ -55736,6 +56062,8 @@ async function checkClearance(args, engine) {
55736
56062
  ...pose ? { pose } : {},
55737
56063
  intersecting: result.intersecting,
55738
56064
  worst_pair: result.worst_pair,
56065
+ ...result.worst_pose ? { worst_pose: result.worst_pose } : {},
56066
+ ...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
55739
56067
  pairs_checked: result.pairs_checked,
55740
56068
  group_a: result.group_a,
55741
56069
  group_b: result.group_b,
@@ -55752,11 +56080,92 @@ async function checkClearance(args, engine) {
55752
56080
  }
55753
56081
  };
55754
56082
  }
56083
+ function parseSweep(raw) {
56084
+ if (raw === void 0 || raw === null) return {};
56085
+ if (!Array.isArray(raw)) {
56086
+ return { error: "`sweep` must be an array of {joint, from, to, steps} axes." };
56087
+ }
56088
+ const sweep = [];
56089
+ for (const entry of raw) {
56090
+ if (!entry || typeof entry !== "object") {
56091
+ return { error: "Each `sweep` axis must be an object {joint, from, to, steps}." };
56092
+ }
56093
+ const e = entry;
56094
+ const joint = typeof e.joint === "string" ? e.joint.trim() : "";
56095
+ const from = Number(e.from);
56096
+ const to = Number(e.to);
56097
+ const steps = Number(e.steps);
56098
+ if (!joint || !Number.isFinite(from) || !Number.isFinite(to) || !Number.isFinite(steps)) {
56099
+ return {
56100
+ error: "Each `sweep` axis needs a `joint` id/name plus numeric `from`, `to`, and `steps`."
56101
+ };
56102
+ }
56103
+ sweep.push({ joint, from, to, steps });
56104
+ }
56105
+ return sweep.length > 0 ? { sweep } : {};
56106
+ }
56107
+ function parseIgnorePairs(raw) {
56108
+ if (raw === void 0 || raw === null) return { pairs: [] };
56109
+ if (!Array.isArray(raw)) {
56110
+ return { pairs: [], error: "`ignore_pairs` must be an array of [part_a, part_b] pairs." };
56111
+ }
56112
+ const pairs = [];
56113
+ for (const entry of raw) {
56114
+ if (!Array.isArray(entry) || entry.length !== 2) {
56115
+ return { pairs: [], error: "Each `ignore_pairs` entry must be a two-element array." };
56116
+ }
56117
+ pairs.push([String(entry[0]), String(entry[1])]);
56118
+ }
56119
+ return { pairs };
56120
+ }
56121
+ function auditClearance(documentId, doc, engine, args, sweep, label, pose) {
56122
+ const minMm = typeof args.min_mm === "number" && Number.isFinite(args.min_mm) ? args.min_mm : 0;
56123
+ const { pairs: ignorePairs, error: ignoreError } = parseIgnorePairs(args.ignore_pairs);
56124
+ if (ignoreError) return err3(ignoreError);
56125
+ const ignoreFixedJoints = args.ignore_fixed_joints === void 0 ? true : asBool(args.ignore_fixed_joints);
56126
+ const ignoreAdjacent = asBool(args.ignore_adjacent);
56127
+ const { result, error: error2 } = computeInterferenceAudit(doc, engine, {
56128
+ minMm,
56129
+ ignorePairs,
56130
+ ignoreFixedJoints,
56131
+ ignoreAdjacent,
56132
+ sweep
56133
+ });
56134
+ if (error2 || !result) return err3(error2 ?? "Interference audit could not be computed.");
56135
+ const payload = {
56136
+ success: true,
56137
+ document_id: documentId,
56138
+ mode: "audit",
56139
+ required_mm: minMm,
56140
+ ...pose ? { pose } : {},
56141
+ pass: result.findings.length === 0,
56142
+ findings: result.findings,
56143
+ interference_count: result.findings.length,
56144
+ parts_checked: result.parts_checked,
56145
+ pairs_total: result.pairs_total,
56146
+ pairs_ignored: result.pairs_ignored,
56147
+ pairs_broadphase: result.pairs_broadphase,
56148
+ queries: result.queries,
56149
+ ...result.poses_checked !== void 0 ? { poses_checked: result.poses_checked } : {},
56150
+ ...result.unresolved_ignores.length > 0 ? {
56151
+ unresolved_ignores: result.unresolved_ignores,
56152
+ note: `ignore_pairs named parts that do not exist: ${result.unresolved_ignores.join(", ")} \u2014 those pairs were NOT whitelisted.`
56153
+ } : {},
56154
+ ...label ? {
56155
+ spec_saved: false,
56156
+ label_note: "`label` persists only pairwise assertions; an audit is a whole-document scan, so nothing was saved. Name the offending pair and re-run with group_a/group_b to persist it."
56157
+ } : {}
56158
+ };
56159
+ return {
56160
+ content: [{ type: "text", text: JSON.stringify(payload) }],
56161
+ structuredContent: { clearance: payload, document_id: documentId }
56162
+ };
56163
+ }
55755
56164
  function clearanceReceiptClaims(doc, engine) {
55756
56165
  const specs = doc.clearance_specs ?? [];
55757
56166
  return specs.map((spec) => {
55758
56167
  const id = `${CLEARANCE_CLAIM_PREFIX}${spec.label}`;
55759
- const description = `clearance "${spec.label}" at least ${spec.min_mm} mm`;
56168
+ const description = spec.sweep?.length ? `clearance "${spec.label}" at least ${spec.min_mm} mm across the swept range of motion` : `clearance "${spec.label}" at least ${spec.min_mm} mm`;
55760
56169
  const subject = `${spec.group_a.join("+")} vs ${spec.group_b.join("+")}`;
55761
56170
  if (!engine) {
55762
56171
  return {
@@ -55770,7 +56179,13 @@ function clearanceReceiptClaims(doc, engine) {
55770
56179
  subject
55771
56180
  };
55772
56181
  }
55773
- const { result, error: error2 } = computeGroupClearance(doc, engine, spec.group_a, spec.group_b);
56182
+ const { result, error: error2 } = computeGroupClearance(
56183
+ doc,
56184
+ engine,
56185
+ spec.group_a,
56186
+ spec.group_b,
56187
+ spec.sweep
56188
+ );
55774
56189
  if (error2 || !result) {
55775
56190
  return {
55776
56191
  ...unverifiableClaim(
@@ -55790,8 +56205,15 @@ function clearanceReceiptClaims(doc, engine) {
55790
56205
  group_b: spec.group_b,
55791
56206
  required_mm: spec.min_mm,
55792
56207
  measured_mm: result.distance_mm,
55793
- holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact),
55794
- ...allowContact ? { allow_contact: true } : {}
56208
+ holds: clearanceHolds(result.distance_mm, spec.min_mm, allowContact, result.intersecting),
56209
+ ...allowContact ? { allow_contact: true } : {},
56210
+ // A swept claim carries its grid so verify_receipt re-checks the same
56211
+ // range of motion — re-verifying at one pose would silently weaken it.
56212
+ ...spec.sweep?.length ? {
56213
+ sweep: spec.sweep,
56214
+ worst_pose: result.worst_pose ?? [],
56215
+ poses_checked: result.poses_checked ?? 0
56216
+ } : {}
55795
56217
  };
55796
56218
  return {
55797
56219
  id,
@@ -55820,7 +56242,13 @@ function verifyClearanceClaims(doc, engine, receipt) {
55820
56242
  });
55821
56243
  continue;
55822
56244
  }
55823
- const { result, error: error2 } = computeGroupClearance(doc, engine, stored.group_a, stored.group_b);
56245
+ const { result, error: error2 } = computeGroupClearance(
56246
+ doc,
56247
+ engine,
56248
+ stored.group_a,
56249
+ stored.group_b,
56250
+ stored.sweep
56251
+ );
55824
56252
  if (error2 || !result) {
55825
56253
  checks.push({
55826
56254
  label,
@@ -55832,32 +56260,28 @@ function verifyClearanceClaims(doc, engine, receipt) {
55832
56260
  continue;
55833
56261
  }
55834
56262
  const measured = result.distance_mm;
55835
- if (!clearanceHolds(measured, stored.required_mm, stored.allow_contact === true)) {
56263
+ const sweptFields = result.poses_checked ? { worst_pose: result.worst_pose, poses_checked: result.poses_checked } : {};
56264
+ const common = {
56265
+ label,
56266
+ required_mm: stored.required_mm,
56267
+ stored_mm: stored.measured_mm,
56268
+ measured_mm: measured,
56269
+ ...sweptFields
56270
+ };
56271
+ if (!clearanceHolds(measured, stored.required_mm, stored.allow_contact === true, result.intersecting)) {
55836
56272
  checks.push({
55837
- label,
56273
+ ...common,
55838
56274
  status: "Violated",
55839
- required_mm: stored.required_mm,
55840
- stored_mm: stored.measured_mm,
55841
- measured_mm: measured,
55842
- reason: `measured ${measured} mm is below the required ${stored.required_mm} mm`
56275
+ reason: result.poses_checked ? `measured ${measured} mm is below the required ${stored.required_mm} mm somewhere in the swept range of motion` : `measured ${measured} mm is below the required ${stored.required_mm} mm`
55843
56276
  });
55844
56277
  } else if (Math.abs(measured - stored.measured_mm) > STALE_EPS_MM) {
55845
56278
  checks.push({
55846
- label,
56279
+ ...common,
55847
56280
  status: "Stale",
55848
- required_mm: stored.required_mm,
55849
- stored_mm: stored.measured_mm,
55850
- measured_mm: measured,
55851
56281
  reason: "geometry changed since the receipt was built, but the clearance still holds"
55852
56282
  });
55853
56283
  } else {
55854
- checks.push({
55855
- label,
55856
- status: "Holds",
55857
- required_mm: stored.required_mm,
55858
- stored_mm: stored.measured_mm,
55859
- measured_mm: measured
55860
- });
56284
+ checks.push({ ...common, status: "Holds" });
55861
56285
  }
55862
56286
  }
55863
56287
  const status = checks.some((c) => c.status === "Violated") ? "Violated" : checks.some((c) => c.status === "Stale") ? "Stale" : "Holds";
@@ -55887,7 +56311,7 @@ function err3(text) {
55887
56311
  isError: true
55888
56312
  };
55889
56313
  }
55890
- var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, toolDefs11;
56314
+ var CLEARANCE_CLAIM_PREFIX, MECH_DOMAIN2, CLEARANCE_ORACLE, STALE_EPS_MM, CONTACT_EPS_MM, round6, checkClearanceSchema, MAX_SWEEP_POSES, pairKey, toolDefs11;
55891
56315
  var init_clearance = __esm({
55892
56316
  "src/tools/clearance.ts"() {
55893
56317
  "use strict";
@@ -55935,15 +56359,47 @@ var init_clearance = __esm({
55935
56359
  type: "boolean",
55936
56360
  description: "Treat exact surface contact (measured distance within 0.001 mm of zero) as passing even though it is below `min_mm` \u2014 for parts designed to touch, e.g. a stage bolted flush to the chamber floor. Penetration beyond the tolerance still fails. Persisted with the spec when `label` is given."
55937
56361
  },
55938
- joint_state: jointStateSchemaProp
56362
+ joint_state: jointStateSchemaProp,
56363
+ sweep: {
56364
+ type: "array",
56365
+ description: "Range-of-motion sweep: drive these joints across their travel and report the WORST pose, not the authored one. Each axis is {joint, from, to, steps}; multiple axes form a Cartesian grid (capped at 4096 poses). Joint states are restored afterwards \u2014 the sweep never edits the document.",
56366
+ items: {
56367
+ type: "object",
56368
+ properties: {
56369
+ joint: { type: "string", description: "Joint id or name to drive." },
56370
+ from: { type: "number", description: "Start of travel (degrees for revolute, mm for prismatic)." },
56371
+ to: { type: "number", description: "End of travel." },
56372
+ steps: { type: "number", description: "Number of intervals; steps + 1 poses are sampled." }
56373
+ },
56374
+ required: ["joint", "from", "to", "steps"]
56375
+ }
56376
+ },
56377
+ ignore_pairs: {
56378
+ type: "array",
56379
+ description: "Audit mode: part id/name pairs whose contact is intended (a bolt in its own hole, a bearing pressed into its bore). Each entry is a two-element array.",
56380
+ items: {
56381
+ type: "array",
56382
+ items: { type: "string" }
56383
+ }
56384
+ },
56385
+ ignore_fixed_joints: {
56386
+ type: "boolean",
56387
+ description: "Audit mode: skip pairs joined by a Fixed joint \u2014 they are bolted together and always 'interfere'. Default true."
56388
+ },
56389
+ ignore_adjacent: {
56390
+ type: "boolean",
56391
+ description: "Audit mode: skip every directly-jointed parent/child pair, not just Fixed ones. Default false \u2014 adjacent links can and do clash away from the joint axis."
56392
+ }
55939
56393
  },
55940
- required: ["document_id", "group_a", "group_b", "min_mm"]
56394
+ required: ["document_id"]
55941
56395
  };
56396
+ MAX_SWEEP_POSES = 4096;
56397
+ pairKey = (a, b) => a < b ? `${a}\0${b}` : `${b}\0${a}`;
55942
56398
  toolDefs11 = [
55943
56399
  {
55944
56400
  name: "check_clearance",
55945
56401
  pack: null,
55946
- description: "Measure the minimum distance between two groups of parts in a CAD session and assert it stays above `min_mm` \u2014 air gaps, press fits, screw-head clearances. Reports the measured minimum (negative = penetration depth), a clear/touching/intersecting verdict, the worst part pair, and pass/fail. Pass `allow_contact: true` for parts designed to touch (e.g. bolted flush) so exact contact passes instead of reading as an intersection. Pass `joint_state` to measure at a real pose (joint id or name \u2192 degrees, or mm for sliders) instead of the stored zero pose \u2014 that is how you check a mechanism clears through its travel. Give it a `label` to persist the assertion on the document: build_receipt then emits it as a mech.clearance claim and verify_receipt re-verifies it as Holds / Stale / Violated when geometry changes.",
56402
+ description: "Measure the minimum distance between two groups of parts in a CAD session and assert it stays above `min_mm` \u2014 air gaps, press fits, screw-head clearances. Reports the measured minimum (negative = penetration depth), a clear/touching/intersecting verdict, the worst part pair, and pass/fail. Pass `allow_contact: true` for parts designed to touch (e.g. bolted flush) so exact contact passes instead of reading as an intersection. Pass `joint_state` to measure at one real pose (joint id or name \u2192 degrees, or mm for sliders) instead of the stored one. Give it a `label` to persist the assertion on the document: build_receipt then emits it as a mech.clearance claim and verify_receipt re-verifies it as Holds / Stale / Violated when geometry changes. Two modes beyond the single-pair snapshot: (1) `sweep` \u2014 pass [{joint, from, to, steps}] to drive the mechanism through its range of motion and report the WORST pose, not the one it was authored in (a linkage modelled at mid-travel routinely clears there and collides at both ends); persisted with the label, so the assertion re-verifies over the same range. (2) audit \u2014 omit `group_a`/`group_b` entirely to broadphase EVERY part pair in the document and list everything that interpenetrates, with penetration depth; whitelist intended contact with `ignore_pairs` (Fixed-joint pairs are skipped by default). Combine both for 'does this machine ever hit itself anywhere in its workspace'.",
55947
56403
  inputSchema: checkClearanceSchema,
55948
56404
  handler: (a, c) => checkClearance(a, c.engine),
55949
56405
  behavior: behavior({ writesDoc: true })
@@ -56746,6 +57202,14 @@ function totalsSummary(totals) {
56746
57202
  currency: totals.currency
56747
57203
  };
56748
57204
  }
57205
+ function documentHardware(documentId) {
57206
+ try {
57207
+ const doc = documents.get(documentId);
57208
+ return Array.isArray(doc?.hardware) ? doc.hardware : [];
57209
+ } catch {
57210
+ return [];
57211
+ }
57212
+ }
56749
57213
  async function bomCreate(input, store, user) {
56750
57214
  const args = input ?? {};
56751
57215
  const owner = ownerId(user);
@@ -56760,7 +57224,23 @@ async function bomCreate(input, store, user) {
56760
57224
  created_at: now,
56761
57225
  updated_at: now
56762
57226
  };
56763
- const lineInputs = Array.isArray(args.lines) ? args.lines : [];
57227
+ const fromGeometry = args.from_geometry !== false;
57228
+ const geometryLines = [];
57229
+ if (fromGeometry && bom.document_id) {
57230
+ for (const hw of documentHardware(bom.document_id)) {
57231
+ geometryLines.push({
57232
+ kind: "cots",
57233
+ name: hw.spec,
57234
+ catalog_id: hw.catalog_id ?? void 0,
57235
+ qty: hw.qty,
57236
+ notes: "counted from the model geometry"
57237
+ });
57238
+ }
57239
+ }
57240
+ const lineInputs = [
57241
+ ...geometryLines,
57242
+ ...Array.isArray(args.lines) ? args.lines : []
57243
+ ];
56764
57244
  for (let i = 0; i < lineInputs.length; i++) {
56765
57245
  const built = await buildLine(lineInputs[i] ?? {}, store, owner);
56766
57246
  if ("error" in built) return err4(`lines[${i}]: ${built.error}`);
@@ -56936,6 +57416,7 @@ var init_bom = __esm({
56936
57416
  init_pricing();
56937
57417
  init_types4();
56938
57418
  init_mech_parts();
57419
+ init_session_core();
56939
57420
  init_tool_def();
56940
57421
  PRICING_NOTE = "All prices are ESTIMATES (Phase-0 quote estimates, catalog price bands, or caller-supplied numbers) \u2014 not binding quotes.";
56941
57422
  memBoms = /* @__PURE__ */ new Map();
@@ -57001,6 +57482,10 @@ var init_bom = __esm({
57001
57482
  type: "array",
57002
57483
  items: { type: "object", properties: lineProperties, required: ["kind"] },
57003
57484
  description: "Optional full line list to build the BOM in ONE call (recommended on serverless \u2014 BOMs are in-memory per instance, so one-shot creation is the robust path)."
57485
+ },
57486
+ from_geometry: {
57487
+ type: "boolean",
57488
+ description: "Seed COTS lines from the hardware the document's geometry declares \u2014 every loon `bolt`/`bolt-circle` (plus its washers and nuts), with quantities already multiplied through patterns. Default true when document_id is given."
57004
57489
  }
57005
57490
  },
57006
57491
  required: []
@@ -57035,7 +57520,7 @@ var init_bom = __esm({
57035
57520
  {
57036
57521
  name: "bom_create",
57037
57522
  pack: "bom",
57038
- description: "Create a project bill of materials \u2014 the deliverable that collects every manufactured part (PCBs, sheet metal, 3D prints; link quote_manufacturing quotes by quote_id) and COTS part (bearings, shafts, screws, magnets; link search_mechanical_parts entries by catalog_id) in one place. Optionally attach a document_id and assembly notes. Pass the full `lines` array to build the whole BOM in ONE call \u2014 recommended, since BOMs are in-memory per server instance. All prices are estimates and flagged as such.",
57523
+ description: "Create a project bill of materials \u2014 the deliverable that collects every manufactured part (PCBs, sheet metal, 3D prints; link quote_manufacturing quotes by quote_id) and COTS part (bearings, shafts, screws, magnets; link search_mechanical_parts entries by catalog_id) in one place. Optionally attach a document_id and assembly notes. Pass the full `lines` array to build the whole BOM in ONE call \u2014 recommended, since BOMs are in-memory per server instance. When document_id names a resident session, fastener lines are seeded from the geometry itself (every loon `bolt`/`bolt-circle`, quantities multiplied through patterns) \u2014 pass from_geometry:false to opt out. All prices are estimates and flagged as such.",
57039
57524
  inputSchema: bomCreateSchema,
57040
57525
  handler: (a, c) => bomCreate(a, c.fabricateStore, c.user),
57041
57526
  behavior: behavior({})
@@ -71828,7 +72313,7 @@ var init_structure = __esm({
71828
72313
  {
71829
72314
  name: "analyze_structure",
71830
72315
  pack: null,
71831
- description: 'Static structural FEA on a part\'s real geometry with fail-closed mesh-convergence gating: linear-tet fill of the tessellated interior at 2+ refinement levels, linear-elastic solve (PCG), returning max von Mises stress, max displacement, discretization-error estimates, and (with yield_strength_mpa) a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims. If QoIs disagree across levels the verdict is Unverifiable and NO stress/displacement claim is emitted (raise resolution, or fillet the singular corner it points at). Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: small-displacement linear elasticity, one isotropic material; no plasticity, buckling, contact, or dynamic loads; boundary is staircase-approximated at the lattice pitch. Use predict_physics for the fast coarse steering loop; use this for the audited answer.',
72316
+ description: 'Static structural FEA on a part\'s real geometry with fail-closed mesh-convergence gating: linear-tet fill of the tessellated interior at 2+ refinement levels, linear-elastic solve (PCG), returning max von Mises stress, max displacement, discretization-error estimates, and (with yield_strength_mpa) a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims. If QoIs disagree across levels the verdict is Unverifiable and NO stress/displacement claim is emitted (raise resolution, or fillet the singular corner it points at). Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: small-displacement linear elasticity, one isotropic material; no plasticity, buckling, contact, or dynamic loads; boundary is staircase-approximated at the lattice pitch. THIN-WALLED PARTS (sheet metal, tube frame, plate) are outside this tool: it measures the thinnest load-bearing section and fails closed with the cell arithmetic and a pointer to beam_check, which is the more accurate answer for a prismatic member anyway. Use predict_physics for the fast coarse steering loop; use this for the audited answer on chunky geometry.',
71832
72317
  inputSchema: {
71833
72318
  type: "object",
71834
72319
  required: ["document_id", "part", "loads", "supports"],
@@ -71893,7 +72378,7 @@ var init_structure = __esm({
71893
72378
  },
71894
72379
  resolution: {
71895
72380
  type: "number",
71896
- description: "Coarse-level lattice cells along the longest bbox axis (default 24). The finest level is resolution * 2^(levels-1), capped at 160 for this tier. Keep >= ~6 cells through the thinnest load-bearing section."
72381
+ description: "Coarse-level lattice cells along the longest bbox axis (default 24). The finest level is resolution * 2^(levels-1), capped at 160 for this tier. Keep >= ~6 cells through the thinnest load-bearing section \u2014 the tool measures the part and refuses below ~4, with the cell arithmetic attached. Raising this is NOT the lever for a thin wall (a 2 mm wall on a 300 mm member needs ~950 cells): use beam_check."
71897
72382
  },
71898
72383
  levels: {
71899
72384
  type: "number",
@@ -71944,6 +72429,89 @@ var init_structure = __esm({
71944
72429
  });
71945
72430
  },
71946
72431
  behavior: behavior({})
72432
+ },
72433
+ {
72434
+ name: "beam_check",
72435
+ pack: null,
72436
+ description: 'Closed-form structural check of a PRISMATIC member \u2014 the audited answer for thin-walled geometry, where the lattice in analyze_structure cannot put enough cells through the wall at any affordable resolution. Give it a profile (rect, rect_tube, round, round_tube, i_beam), a span, an end condition, and a load case; get section properties (A, I_y, I_z, J, section moduli, torsional stiffness), stresses (bending, axial, torsional and transverse shear, von Mises), deflection with the Timoshenko shear term, twist, the Euler buckling load under compression, and a safety factor \u2014 plus `vcad.fea-claims/1` and unified-receipt claims under `structure.beam.*`. Exact integrals where they exist (round, round tube, rectangle bending), the convergent Saint-Venant series for solid-rectangle torsion, Bredt closed thin-wall theory for tube torsion \u2014 every approximation is stated in the returned notes. For a constant cross-section this is not a fallback: it beats a staircased lattice on accuracy and costs microseconds. Fail-closed: too stubby for beam theory (L/depth < 5), too thick-walled for Bredt, deflecting past a tenth of the span, torque on an open section, or buckling before it yields \u2014 any of those makes the verdict Unverifiable and NO QoI is claimed, with the reason naming the route forward. Claims carry basis "predicted" and roll up Provisional until hardware is load-tested. Scope: one uniform section over the whole span, idealized ends, no stress concentrations (holes, welds, corner radii), no local wall buckling, no fatigue. Needs no document \u2014 it is geometry-by-description, so it works before the part exists.',
72437
+ inputSchema: {
72438
+ type: "object",
72439
+ required: ["profile", "length_mm", "end_condition"],
72440
+ properties: {
72441
+ profile: {
72442
+ type: "object",
72443
+ required: ["type"],
72444
+ description: 'Cross-section. The member runs along X and the section lives in (Y, Z): width_mm is the Y extent, height_mm the Z extent. One of: {"type":"rect",width_mm,height_mm}, {"type":"rect_tube",width_mm,height_mm,wall_mm}, {"type":"round",diameter_mm}, {"type":"round_tube",diameter_mm,wall_mm}, {"type":"i_beam",width_mm,height_mm,flange_mm,web_mm}. Tube dimensions are OUTSIDE dimensions.',
72445
+ properties: {
72446
+ type: {
72447
+ type: "string",
72448
+ enum: ["rect", "rect_tube", "round", "round_tube", "i_beam"]
72449
+ },
72450
+ width_mm: { type: "number", description: "Y extent, mm." },
72451
+ height_mm: { type: "number", description: "Z extent, mm." },
72452
+ wall_mm: { type: "number", description: "Wall thickness, mm." },
72453
+ diameter_mm: { type: "number", description: "Outside diameter, mm." },
72454
+ flange_mm: { type: "number", description: "Flange thickness, mm." },
72455
+ web_mm: { type: "number", description: "Web thickness, mm." }
72456
+ }
72457
+ },
72458
+ length_mm: {
72459
+ type: "number",
72460
+ description: "Free span between supports, mm."
72461
+ },
72462
+ end_condition: {
72463
+ type: "string",
72464
+ enum: [
72465
+ "cantilever_tip",
72466
+ "cantilever_uniform",
72467
+ "simple_center",
72468
+ "simple_uniform",
72469
+ "fixed_fixed_center",
72470
+ "fixed_fixed_uniform"
72471
+ ],
72472
+ description: "Support and load arrangement. `*_uniform` spreads the transverse force over the span; `*_center`/`*_tip` concentrates it. Also sets the Euler effective-length factor (cantilever K=2, simple K=1, fixed-fixed K=0.5)."
72473
+ },
72474
+ transverse_force_n: {
72475
+ type: "number",
72476
+ description: "Total transverse force, N (magnitude). Default 0."
72477
+ },
72478
+ bend_axis: {
72479
+ type: "string",
72480
+ enum: ["y", "z"],
72481
+ description: "Which principal axis to bend about: `y` uses I_y and deflects along Z (default), `z` uses I_z and deflects along Y. Matters for any non-square section."
72482
+ },
72483
+ torque_nmm: {
72484
+ type: "number",
72485
+ description: "Torque about the member axis, N\xB7mm (1 N\xB7m = 1000 N\xB7mm). Default 0. This is the case closed-form theory answers best and the lattice answers worst."
72486
+ },
72487
+ axial_force_n: {
72488
+ type: "number",
72489
+ description: "Axial force, N \u2014 positive tension, negative compression. Compression is also checked against Euler buckling, and a member that buckles is reported Unverifiable rather than 'safe'. Default 0."
72490
+ },
72491
+ youngs_modulus_mpa: {
72492
+ type: "number",
72493
+ description: "Young's modulus, MPa. Default 69000 (6061 aluminum); steel ~200000, PLA ~2300."
72494
+ },
72495
+ poisson: {
72496
+ type: "number",
72497
+ description: "Poisson's ratio in [0, 0.5). Default 0.33; sets G = E/(2(1+nu))."
72498
+ },
72499
+ yield_strength_mpa: {
72500
+ type: "number",
72501
+ description: "Yield strength, MPa. When given, safety_factor = yield / von Mises is computed and claimed (applicable checks only)."
72502
+ }
72503
+ }
72504
+ },
72505
+ handler: (args, ctx) => {
72506
+ const a = args;
72507
+ const started = performance.now();
72508
+ const out = ctx.engine.feaCheckBeam(JSON.stringify(a));
72509
+ return textResult7({
72510
+ solve_ms: Math.round(performance.now() - started),
72511
+ ...out
72512
+ });
72513
+ },
72514
+ behavior: behavior({})
71947
72515
  }
71948
72516
  ];
71949
72517
  }
@@ -79647,8 +80215,8 @@ var init_server3 = __esm({
79647
80215
  init_order_feed();
79648
80216
  init_animate();
79649
80217
  PKG_VERSION = (() => {
79650
- if ("0.9.4-main.35") {
79651
- return "0.9.4-main.35";
80218
+ if ("0.9.4-main.38") {
80219
+ return "0.9.4-main.38";
79652
80220
  }
79653
80221
  try {
79654
80222
  const req = createRequire2(import.meta.url);
@@ -79657,8 +80225,8 @@ var init_server3 = __esm({
79657
80225
  return "0.0.0";
79658
80226
  }
79659
80227
  })();
79660
- BUILD_SHA = "05b902db41f28989ab2bae377db68a55dd2a7121";
79661
- BUILD_TIME = "2026-07-26T13:07:16.613Z";
80228
+ BUILD_SHA = "254d4e9d13825ac74a895590703959d77898e1f3";
80229
+ BUILD_TIME = "2026-07-26T13:42:07.736Z";
79662
80230
  SHORT_SHA = BUILD_SHA === "unknown" ? "unknown" : BUILD_SHA.slice(0, 7);
79663
80231
  VERSION_WITH_BUILD = SHORT_SHA === "unknown" ? PKG_VERSION : `${PKG_VERSION}+${SHORT_SHA}`;
79664
80232
  INSTANCE_ID = randomUUID6().slice(0, 8);
@@ -79806,6 +80374,7 @@ var init_server3 = __esm({
79806
80374
  "solve_thermal",
79807
80375
  "simulate_flow",
79808
80376
  "analyze_structure",
80377
+ "beam_check",
79809
80378
  "simulate_neutron_shield",
79810
80379
  "simulate_lattice_gauge",
79811
80380
  "analyze_tolerance_stackup",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vcad/mcp",
3
- "version": "0.9.4-main.35",
3
+ "version": "0.9.4-main.38",
4
4
  "description": "vcad MCP server — parametric CAD + PCB design tools for AI agents (self-contained: bundled server + kernel WASM)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "homepage": "https://vcad.io",
21
21
  "vcadBuild": {
22
- "sha": "05b902db41f28989ab2bae377db68a55dd2a7121",
23
- "builtAt": "2026-07-26T13:07:16.613Z"
22
+ "sha": "254d4e9d13825ac74a895590703959d77898e1f3",
23
+ "builtAt": "2026-07-26T13:42:07.736Z"
24
24
  }
25
25
  }
Binary file