新增cdsl
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
---
|
||||
name: cad-engine
|
||||
description: Generate and revise executable CDSL CAD models through the local schema, runtime, library references, and injected part-family planning bridges.
|
||||
---
|
||||
|
||||
# CAD Engine Skill
|
||||
|
||||
## Purpose
|
||||
@@ -6,15 +11,33 @@ Help an AI agent generate CAD models through the repository's CDSL engine.
|
||||
|
||||
## Required workflow
|
||||
|
||||
1. Read the engine README, `profile_schema.json`, `cdsl_schema.json`, and the
|
||||
relevant engine modules. `cdsl_schema.json` is the executable CDSL input
|
||||
contract; its nested object and array shapes are mandatory.
|
||||
2. Search the official CDSL library for similar parts, profiles, and feature
|
||||
sequences.
|
||||
3. Produce or revise parameterized CDSL.
|
||||
4. Validate that the CDSL can run through the `cdsl_only` path.
|
||||
5. Compile the CDSL and generate a STEP file.
|
||||
6. Return the generated artifact, validation result, and library references.
|
||||
1. Use any injected CDSL part-skill bridge to establish the part structure,
|
||||
parameter roles, and feature order. It is planning guidance only.
|
||||
2. Submit a complete DesignIntent before reading CDSL references or generating
|
||||
CDSL. It contains semantic structures and mappings, never sketch coordinates
|
||||
or raw CAD code. The backend records canonical selected part skills.
|
||||
3. Read the engine README, `profile_schema.json`, and `cdsl_schema.json`, then
|
||||
search the official CDSL library for schema-valid profiles and feature
|
||||
expressions. `cdsl_schema.json` is the executable input contract.
|
||||
4. Produce parameterized CDSL in feature dependency order, using the accepted
|
||||
DesignIntent's feature IDs and mappings plus the part
|
||||
plan for intent and CDSL references for concrete schema expressions.
|
||||
5. Call the generation tool only when every chosen feature is executable by
|
||||
the current schema and runtime. Validate through the `cdsl_only` path,
|
||||
then generate STEP and GLB artifacts.
|
||||
|
||||
## Part-skill boundary
|
||||
|
||||
- User requirements and the CDSL schema/runtime override bridge guidance;
|
||||
CDSL examples are lower-priority expression references.
|
||||
- Do not expose upstream source skills directly or treat them as code. Use
|
||||
only the injected CDSL bridge content.
|
||||
- Never emit build123d source or invent an atomic, profile, selector, or
|
||||
capability that the local contract does not support.
|
||||
- Express circular bolt layouts as explicit circles when supported; preserve
|
||||
thread intent as a cylindrical-bore approximation; omit fillets/chamfers
|
||||
without a unique selector. These decisions must be recorded in the build
|
||||
audit rather than hidden in generated geometry.
|
||||
|
||||
## Hard constraints
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# DesignIntent Planning Recipe
|
||||
|
||||
## Requirement analysis
|
||||
|
||||
1. Treat the backend-injected part-family skill as the selected family before
|
||||
identifying structures. It provides planning guidance only.
|
||||
2. Decompose the requested part into structures with roles: `base`,
|
||||
`reference`, `additive`, `subtractive`, `dressup`, or `pattern`.
|
||||
3. Every structure must state a purpose, structural dependencies, parameter
|
||||
roles, selector roles, an executable CDSL atomic ID, and a named profile
|
||||
type when its atomic needs a sketch.
|
||||
4. Put every structure exactly once in `feature_order`, after its dependencies.
|
||||
Its `cdsl_feature_id` is the immutable ID that the later CDSL feature must use.
|
||||
5. Do not put coordinates, raw CDSL, Build123d, or low-level sketch data in a
|
||||
DesignIntent.
|
||||
6. Missing essential dimensions are blocking `open_questions`. Unsupported
|
||||
geometry is a `capability_gaps` item. A blocking item requires
|
||||
`status: needs_clarification`; do not approximate silently.
|
||||
|
||||
## Part family boundary
|
||||
|
||||
- Use only the backend-injected primary and support skills. Never submit
|
||||
`part_skill_ids`; the backend records the canonical selection for audit.
|
||||
- Explicit user requirements override part skills. The executable CDSL
|
||||
schema/runtime overrides both part skills and library examples.
|
||||
- The current primary family is inherited during revisions unless the user
|
||||
explicitly asks to replace the whole part. A family conflict needs a concise
|
||||
clarification before a ready plan can be submitted.
|
||||
- CDSL library examples show only expression patterns. They never add or remove
|
||||
a requested DesignIntent structure.
|
||||
|
||||
## CDSL generation
|
||||
|
||||
- Submit `propose_design_intent` before any library or CDSL-generation call.
|
||||
- After its ready result includes `intent_id`, inspect references and generate
|
||||
complete CDSL. Use every planned `cdsl_feature_id` once, in `feature_order`.
|
||||
- Do not add undeclared CDSL features, delete planned features, change planned
|
||||
atomics/profiles, or omit declared selector evidence.
|
||||
- Keep `cdsl_only` as the only build path. Never use `compiler_context` or a
|
||||
legacy fallback.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 earthtojake
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Part Skill Provenance
|
||||
|
||||
The files under `source/` are copied from the local `cadSet` checkout:
|
||||
|
||||
`/Users/lk/Projects/cadSet/text-to-cad/skills/cad/references/cad-skillx/`
|
||||
|
||||
The source planning, functional, and atomic documents are MIT licensed by
|
||||
earthtojake. The files under `bridge/` are local CDSL-specific adaptations.
|
||||
They preserve the structural guidance while documenting the current CDSL
|
||||
schema/runtime boundary. Keep the source copies unchanged when possible and
|
||||
review future upstream updates manually.
|
||||
|
||||
Each local `source/` path maps to the same relative path below the upstream
|
||||
root above. The copied source documents are:
|
||||
|
||||
- `planning/mounting-plate.planning.md`
|
||||
- `planning/mounting-bracket.planning.md`
|
||||
- `planning/flange.planning.md`
|
||||
- `planning/simple-shaft-or-cylindrical-rod.planning.md`
|
||||
- `planning/bearing-housing-or-seat.planning.md`
|
||||
- `planning/hexagonal-nut.planning.md`
|
||||
- `functional/axisymmetric-revolve-strategy.functional.md`
|
||||
- `functional/bearing-bore-seat.functional.md`
|
||||
- `functional/flange-bolt-circle.functional.md`
|
||||
- `functional/mounting-plate-hole-layout.functional.md`
|
||||
- `functional/slotted-adjustment-feature.functional.md`
|
||||
- `functional/standard-hole-wizard.functional.md`
|
||||
- `functional/symmetric-feature-layout.functional.md`
|
||||
- `atomic/coaxial-bore-rule.atomic.md`
|
||||
- `atomic/counterbored-hole-creation.atomic.md`
|
||||
- `atomic/extrude-base-profile.atomic.md`
|
||||
- `atomic/fillet-chamfer-last.atomic.md`
|
||||
- `atomic/pattern-holes-from-datum.atomic.md`
|
||||
- `atomic/revolve-profile-around-axis.atomic.md`
|
||||
- `atomic/threaded-hole-creation.atomic.md`
|
||||
- `atomic/through-hole-cut.atomic.md`
|
||||
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Coaxial Bore
|
||||
|
||||
- Share one explicit axis or workplane origin across bore, seat, hub, sleeve, and clearance features.
|
||||
- Keep each diameter role distinct. Use sequential cuts for a through clearance plus a deeper/larger seat.
|
||||
- Prefer `reference_axis`, explicit frames, `revolve_cut`, or circular cut profiles; do not infer a new axis from arbitrary topology.
|
||||
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Counterbored Hole
|
||||
|
||||
- A counterbore is a smaller functional hole plus a larger, shallow coaxial recess.
|
||||
- Use `hole_counterbore` or `hole_wizard` with `counterbore` fields when host placement is resolvable.
|
||||
- Preserve main diameter, depth, counterbore diameter, and counterbore depth separately.
|
||||
- Do not use a plain blind hole when the screw-head seat is part of the request.
|
||||
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Extrude Base
|
||||
|
||||
- Start with a closed named profile and a workplane. Use `extrude_add_blind` for a plate, block, annulus, hex body, or constant cylinder.
|
||||
- Keep the base simple; add holes, slots, bosses, and finishing as dependent features.
|
||||
- Use one coherent body unless separate solids are explicitly requested.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Fillet And Chamfer Last
|
||||
|
||||
- Apply `fillet` and `chamfer` only after primary adds, bores, holes, pockets, and patterns.
|
||||
- Both operations require selectors that resolve uniquely on the active body.
|
||||
- If no stable selector is available, omit the finishing operation and record `omitted: selector_unavailable`; never apply it to every edge.
|
||||
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Pattern Holes From Datum
|
||||
|
||||
- Build a valid seed hole first and preserve its host frame and functional subtype.
|
||||
- Use `pattern_linear` for one/two-dimensional rows and `pattern_mirror` for symmetry.
|
||||
- For circular layouts, use an explicit `circles` profile expansion and record the audit status; do not invent a circular-pattern atomic.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Revolve Profile
|
||||
|
||||
- Use a closed side profile and explicit axis with `revolve_add` or `revolve_cut`.
|
||||
- Use 360 degrees unless a partial sector is requested. Keep the profile from crossing the axis invalidly.
|
||||
- Add flats, slots, keyways, and bolt holes only after the rotational body is valid.
|
||||
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Threaded Hole
|
||||
|
||||
- Preserve thread size/depth as user intent and use the supported hole contract for the bore.
|
||||
- Current CDSL runtime does not generate helical thread topology. Do not claim that it does.
|
||||
- Record `approximated: thread_geometry_omitted` in the revision audit and include the bore dimensions used.
|
||||
@@ -0,0 +1,5 @@
|
||||
# CDSL Bridge: Through Hole
|
||||
|
||||
- Use a host face/frame and an explicit diameter. Prefer `hole_wizard` with a through end condition when its selector contract is complete.
|
||||
- Otherwise use a circular sketch and a cut depth that is explicitly sufficient for the body, preserving the through-hole intent in the feature name/assumptions.
|
||||
- Keep central bores on the main axis and repeated holes in a pattern or explicit circle group.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Axisymmetric Revolve
|
||||
|
||||
- Choose a closed half-profile and an explicit `axis` for `revolve_add` or `revolve_cut`.
|
||||
- Use 360 degrees for a complete body. Keep the profile on one valid side of the axis.
|
||||
- Prefer revolve for steps, shoulders, tapers, grooves, sleeves, and turned parts; use circle extrusion for a constant cylinder.
|
||||
- Validate one coherent solid and shared axis before adding non-axisymmetric cuts.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Bearing Bore Seat
|
||||
|
||||
- Build enough support material first, then cut the smaller shaft clearance and larger bearing seat on the same frame.
|
||||
- Use a defined seat depth and shoulder; never collapse both diameters into one hole.
|
||||
- Express coaxial cuts with explicit frames or a stable reference axis/plane. Mounting holes follow the seat relationship.
|
||||
- If a selector cannot identify the seat face uniquely, stop the dependent feature and report the blocker.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Flange Bolt Circle
|
||||
|
||||
- Keep bolt-circle diameter, count, start angle, and hole diameter explicit.
|
||||
- Since the runtime has no circular-pattern atomic, expand the layout into explicit circle centers in a `circles` profile and cut them together.
|
||||
- This is a semantic expansion, not an arbitrary approximation; report `expanded: circular_pattern_to_explicit_circles`.
|
||||
- Do not use a rectangular grid for a circular flange.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Mounting Plate Hole Layout
|
||||
|
||||
- Establish plate edge/center datums before hole placement.
|
||||
- Use one seed hole or explicit hole group, then `pattern_linear` or `pattern_mirror` for rows and symmetry.
|
||||
- Select clearance, threaded, counterbored, or countersunk intent from the request; do not reduce every hole to a generic cylinder.
|
||||
- Preserve margins and pitch. Finish after all functional holes and slots.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Slotted Adjustment
|
||||
|
||||
- A slot is an obround cut with equal end radii, dimensioned by width, length, and a datum frame.
|
||||
- Use an `obround` sketch and `extrude_cut_blind`/through-depth after the base exists.
|
||||
- A slot is not a shaft keyway or revolved groove. Pattern or mirror only after the seed cut is valid.
|
||||
- Leave material at slot ends and finish slot edges last when selectors are available.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Standard Hole Wizard
|
||||
|
||||
- Choose the hole subtype from function: clearance, blind, through, counterbore, countersink, or threaded intent.
|
||||
- Use `hole_wizard` only with its exact schema fields and a resolvable host face, or use a self-contained circular cut when the frame is explicit.
|
||||
- Positions are objects such as `{"mm": [u, v, w]}`; never emit bare coordinate arrays.
|
||||
- Thread metadata is retained as intent but current geometry is cylindrical; report the approximation.
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# CDSL Bridge: Symmetric Feature Layout
|
||||
|
||||
- Define a centerline, plane, or datum before placing symmetric features.
|
||||
- Use `pattern_mirror` for a mirrored source feature and `pattern_linear` for regular rows.
|
||||
- The source feature must execute first and its transformed selectors/frames must remain uniquely resolvable.
|
||||
- Never create symmetry by independently guessed coordinates when a pattern contract is available.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Bearing Housing
|
||||
|
||||
- Use for a bearing seat, bearing support, pillow-block-like body, or bushing seat, not a rolling-bearing assembly.
|
||||
- Invariants: one bearing axis, distinct bearing seat and shaft-clearance diameters, a locating shoulder/depth, and a mounting base or flange.
|
||||
- Plan: base/support add -> coaxial shaft bore -> larger bearing pocket/counterbore -> mounting holes -> ribs/bosses -> late finishing.
|
||||
- Keep bearing outer diameter, seat depth, shaft clearance, center height, wall thickness, and mounting pattern separate.
|
||||
- Ribs must connect base and support; never add floating decorative ribs.
|
||||
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Flange
|
||||
|
||||
- Use for a circular flange, flanged sleeve, or coupling flange with a shared central axis.
|
||||
- Invariants: coaxial outside diameter, bore/hub, sealing land, and fastener holes on a pitch circle.
|
||||
- Plan: `revolve_add` for stepped axisymmetric bodies or circular `extrude_add_blind` -> coaxial bore -> one bolt-hole definition -> explicit circle set or supported repetition -> late finishing.
|
||||
- No circular-pattern atomic exists. Expand a bolt circle into explicit circles in one cut profile and record `expanded` in the audit.
|
||||
- Keep bolt-circle diameter, count, start angle, bore, and hub dimensions distinct.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Hexagonal Nut
|
||||
|
||||
- Use for a hex body with a centered internal threaded-bore intent.
|
||||
- Invariants: across-flats, thickness, centered bore, and functional lead-in chamfers.
|
||||
- Plan: polygon/hex `extrude_add_blind` -> centered bore or `hole_wizard` -> thread audit -> finishing when selectors are valid.
|
||||
- Preserve across-flats separately from thread/bore diameter. Do not create a mating bolt.
|
||||
- Current runtime does not create helical threads; represent the bore and record `approximated: thread_geometry_omitted`.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Mounting Bracket
|
||||
|
||||
- Use for an L/U/T bracket or a support with a base plus web, lug, tab, or boss.
|
||||
- Invariants: one coherent body, a load path from base to web/boss, datum-driven holes, and no invented fastener assembly.
|
||||
- Plan: base `extrude_add_blind` -> web/tab/boss add features -> clearance/slot cuts -> holes -> mirror/linear repetition -> late edge treatment.
|
||||
- Use named planar profiles and explicit workplanes. Keep holes after the support geometry so they do not float.
|
||||
- Do not flatten a 3D bracket into a plate or add fillets without a valid selector.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Mounting Plate
|
||||
|
||||
- Use for a mostly uniform plate, base, adapter, fixture, or panel.
|
||||
- Invariants: a named rectangular/rounded profile, thickness, datum-based holes, and functional cutouts.
|
||||
- Plan: `extrude_add_blind` base -> `hole_wizard` or cut-hole features -> `pattern_linear`/`pattern_mirror` -> slots and pockets -> late `fillet`/`chamfer` only with selectors.
|
||||
- Prefer `rectangle`, `rectangle_with_fillets`, or `obround` profiles. Keep edge margins and pitch as named numeric parameters.
|
||||
- Rectangular hole groups use linear/mirror patterns; never use circular-pattern semantics for a plate.
|
||||
@@ -0,0 +1,7 @@
|
||||
# CDSL Bridge: Simple Shaft
|
||||
|
||||
- Use for a single cylindrical rod, pin, spacer, or stepped shaft, not an assembly.
|
||||
- Invariants: one main axis, one coherent solid, axial segment lengths/diameters, and secondary features referenced to the axis.
|
||||
- Use a circle plus `extrude_add_blind` for a uniform rod; use a closed side profile plus `revolve_add` for steps, shoulders, tapers, or grooves.
|
||||
- Add end holes, flats, slots, or keyway-like cuts only after the primary body. Finish last.
|
||||
- Do not invent bearings, gears, couplings, or real thread geometry when not requested.
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"max_planning": 1,
|
||||
"max_support": 3,
|
||||
"skills": [
|
||||
{
|
||||
"id": "planning/mounting-plate",
|
||||
"kind": "planning",
|
||||
"title": "Mounting Plate",
|
||||
"priority": 100,
|
||||
"triggers": ["mounting plate", "base plate", "adapter plate", "fixture plate", "安装底板", "底板", "转接板", "连接板", "面板"],
|
||||
"exclude": ["flange", "法兰", "3d bracket", "mounting bracket", "轴承座"],
|
||||
"bridge": "bridge/planning-mounting-plate.md",
|
||||
"source": "source/planning/mounting-plate.planning.md",
|
||||
"capability_translation_rules": ["exact: schema_valid_feature_sequence"],
|
||||
"related": ["functional/mounting-plate-hole-layout", "functional/slotted-adjustment-feature", "functional/standard-hole-wizard", "functional/symmetric-feature-layout", "atomic/extrude-base-profile", "atomic/pattern-holes-from-datum", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
{
|
||||
"id": "planning/mounting-bracket",
|
||||
"kind": "planning",
|
||||
"title": "Mounting Bracket",
|
||||
"priority": 99,
|
||||
"triggers": ["mounting bracket", "support bracket", "l bracket", "u bracket", "t bracket", "bracket with holes", "安装支架", "支撑架", "L型支架", "U型支架"],
|
||||
"exclude": ["flat mounting plate only", "仅底板", "sheet metal"],
|
||||
"bridge": "bridge/planning-mounting-bracket.md",
|
||||
"source": "source/planning/mounting-bracket.planning.md",
|
||||
"capability_translation_rules": ["exact: schema_valid_feature_sequence"],
|
||||
"related": ["functional/slotted-adjustment-feature", "functional/standard-hole-wizard", "functional/symmetric-feature-layout", "atomic/extrude-base-profile", "atomic/pattern-holes-from-datum", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
{
|
||||
"id": "planning/flange",
|
||||
"kind": "planning",
|
||||
"title": "Flange",
|
||||
"priority": 98,
|
||||
"triggers": ["flange", "circular flange", "pipe flange", "coupling flange", "flanged sleeve", "法兰", "法兰盘", "管法兰", "联轴器法兰"],
|
||||
"exclude": ["flange nut", "法兰螺母", "side tab"],
|
||||
"bridge": "bridge/planning-flange.md",
|
||||
"source": "source/planning/flange.planning.md",
|
||||
"capability_translation_rules": ["expanded: circular_pattern_to_explicit_circles"],
|
||||
"related": ["functional/flange-bolt-circle", "functional/axisymmetric-revolve-strategy", "functional/standard-hole-wizard", "atomic/coaxial-bore-rule", "atomic/revolve-profile-around-axis", "atomic/pattern-holes-from-datum", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
{
|
||||
"id": "planning/simple-shaft",
|
||||
"kind": "planning",
|
||||
"title": "Simple Shaft Or Cylindrical Rod",
|
||||
"priority": 97,
|
||||
"triggers": ["simple shaft", "cylindrical rod", "stepped shaft", "shaft", "rod", "pin", "standoff", "轴", "阶梯轴", "圆柱杆", "销", "支柱"],
|
||||
"exclude": ["spindle assembly", "bearing assembly", "gearbox shaft assembly", "主轴组件", "轴承组件"],
|
||||
"bridge": "bridge/planning-simple-shaft.md",
|
||||
"source": "source/planning/simple-shaft-or-cylindrical-rod.planning.md",
|
||||
"capability_translation_rules": ["exact: revolve_or_extrude_supported"],
|
||||
"related": ["functional/axisymmetric-revolve-strategy", "atomic/coaxial-bore-rule", "atomic/revolve-profile-around-axis", "atomic/through-hole-cut", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
{
|
||||
"id": "planning/bearing-housing",
|
||||
"kind": "planning",
|
||||
"title": "Bearing Housing Or Seat",
|
||||
"priority": 96,
|
||||
"triggers": ["bearing housing", "bearing seat", "bearing support", "bearing block", "bushing seat", "pillow block", "轴承座", "轴承支架", "轴承块", "衬套座", "带座轴承"],
|
||||
"exclude": ["complete rolling bearing", "bearing assembly", "完整轴承", "轴承组件"],
|
||||
"bridge": "bridge/planning-bearing-housing.md",
|
||||
"source": "source/planning/bearing-housing-or-seat.planning.md",
|
||||
"capability_translation_rules": ["blocked: selector_required_for_dependent_features"],
|
||||
"related": ["functional/bearing-bore-seat", "functional/standard-hole-wizard", "atomic/coaxial-bore-rule", "atomic/extrude-base-profile", "atomic/revolve-profile-around-axis", "atomic/counterbored-hole-creation", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
{
|
||||
"id": "planning/hexagonal-nut",
|
||||
"kind": "planning",
|
||||
"title": "Hexagonal Nut",
|
||||
"priority": 95,
|
||||
"triggers": ["hex nut", "hexagonal nut", "threaded nut", "hexagonal threaded insert", "六角螺母", "六角螺帽", "螺纹螺母"],
|
||||
"exclude": ["bolt", "screw", "wing nut", "cap nut", "flange nut", "螺栓", "螺钉", "蝶形螺母", "法兰螺母"],
|
||||
"bridge": "bridge/planning-hexagonal-nut.md",
|
||||
"source": "source/planning/hexagonal-nut.planning.md",
|
||||
"capability_translation_rules": ["approximated: thread_geometry_omitted"],
|
||||
"related": ["functional/axisymmetric-revolve-strategy", "functional/standard-hole-wizard", "atomic/extrude-base-profile", "atomic/coaxial-bore-rule", "atomic/threaded-hole-creation", "atomic/fillet-chamfer-last"]
|
||||
},
|
||||
|
||||
{"id": "functional/axisymmetric-revolve-strategy", "kind": "functional", "title": "Axisymmetric Revolve", "priority": 80, "triggers": ["axisymmetric", "revolve", "revolved body", "rotational part", "turned part", "回转体", "轴对称", "车削件"], "exclude": ["simple extruded cylinder", "rectangular plate"], "bridge": "bridge/functional-axisymmetric-revolve.md", "source": "source/functional/axisymmetric-revolve-strategy.functional.md", "capability_translation_rules": ["exact: explicit_revolve_axis"], "related": []},
|
||||
{"id": "functional/bearing-bore-seat", "kind": "functional", "title": "Bearing Bore Seat", "priority": 79, "triggers": ["bearing bore", "bearing pocket", "bearing seat", "bushing bore", "shaft clearance bore", "轴承孔", "轴承座孔", "轴承腔", "轴间隙孔"], "exclude": [], "bridge": "bridge/functional-bearing-bore-seat.md", "source": "source/functional/bearing-bore-seat.functional.md", "capability_translation_rules": ["blocked: selector_required_for_dependent_features"], "related": ["atomic/coaxial-bore-rule", "atomic/counterbored-hole-creation"]},
|
||||
{"id": "functional/flange-bolt-circle", "kind": "functional", "title": "Flange Bolt Circle", "priority": 78, "triggers": ["flange bolt circle", "bolt circle", "pitch circle", "pcd", "circular bolt pattern", "螺栓圆", "节圆", "圆周孔阵列"], "exclude": ["rectangular hole grid", "矩形孔阵列"], "bridge": "bridge/functional-flange-bolt-circle.md", "source": "source/functional/flange-bolt-circle.functional.md", "capability_translation_rules": ["expanded: circular_pattern_to_explicit_circles", "blocked: explicit_circle_layout_required"], "related": ["atomic/pattern-holes-from-datum"]},
|
||||
{"id": "functional/mounting-plate-hole-layout", "kind": "functional", "title": "Mounting Plate Hole Layout", "priority": 77, "triggers": ["mounting plate hole layout", "base plate hole pattern", "rectangular hole grid", "symmetric mounting holes", "底板孔布局", "矩形孔阵列", "安装孔阵列"], "exclude": ["flange bolt circle", "bearing bore"], "bridge": "bridge/functional-mounting-plate-hole-layout.md", "source": "source/functional/mounting-plate-hole-layout.functional.md", "capability_translation_rules": ["exact: linear_or_mirror_pattern"], "related": ["atomic/pattern-holes-from-datum", "functional/symmetric-feature-layout"]},
|
||||
{"id": "functional/slotted-adjustment-feature", "kind": "functional", "title": "Slotted Adjustment Feature", "priority": 76, "triggers": ["slot", "slotted hole", "adjustment slot", "elongated hole", "guide slot", "槽", "长圆槽", "调节槽", "导向槽"], "exclude": ["shaft keyway", "revolved groove", "轴键槽", "回转槽"], "bridge": "bridge/functional-slotted-adjustment.md", "source": "source/functional/slotted-adjustment-feature.functional.md", "capability_translation_rules": ["exact: obround_extrude_cut"], "related": []},
|
||||
{"id": "functional/standard-hole-wizard", "kind": "functional", "title": "Standard Hole Wizard", "priority": 75, "triggers": ["hole wizard", "standard holes", "threaded holes", "counterbored holes", "countersunk holes", "tapped holes", "孔向导", "标准孔", "螺纹孔", "沉孔", "沉头孔"], "exclude": [], "bridge": "bridge/functional-standard-hole-wizard.md", "source": "source/functional/standard-hole-wizard.functional.md", "capability_translation_rules": ["approximated: thread_geometry_omitted", "exact: supported_hole_subtype"], "related": ["atomic/counterbored-hole-creation", "atomic/threaded-hole-creation", "atomic/through-hole-cut"]},
|
||||
{"id": "functional/symmetric-feature-layout", "kind": "functional", "title": "Symmetric Feature Layout", "priority": 74, "triggers": ["symmetric holes", "mirror pattern", "symmetry", "mirrored features", "centerline layout", "对称孔", "镜像阵列", "对称布局", "中心线布局"], "exclude": [], "bridge": "bridge/functional-symmetric-layout.md", "source": "source/functional/symmetric-feature-layout.functional.md", "capability_translation_rules": ["exact: pattern_mirror_or_pattern_linear"], "related": []},
|
||||
|
||||
{"id": "atomic/coaxial-bore-rule", "kind": "atomic", "title": "Coaxial Bore", "priority": 60, "triggers": ["coaxial bore", "central bore", "concentric", "shaft clearance", "同轴孔", "中心孔", "同心", "轴间隙"], "exclude": [], "bridge": "bridge/atomic-coaxial-bore.md", "source": "source/atomic/coaxial-bore-rule.atomic.md", "capability_translation_rules": ["exact: explicit_axis_or_frame"], "related": []},
|
||||
{"id": "atomic/counterbored-hole-creation", "kind": "atomic", "title": "Counterbored Hole", "priority": 59, "triggers": ["counterbored hole", "counterbore", "socket head screw", "cap screw", "recessed screw head", "沉孔", "内六角螺钉", "螺钉头沉孔"], "exclude": [], "bridge": "bridge/atomic-counterbore.md", "source": "source/atomic/counterbored-hole-creation.atomic.md", "capability_translation_rules": ["exact: hole_counterbore"], "related": []},
|
||||
{"id": "atomic/extrude-base-profile", "kind": "atomic", "title": "Extrude Base Profile", "priority": 58, "triggers": ["extrude base profile", "extrude rectangle", "extrude circle", "base extrusion", "simple cylinder", "rectangular block", "基础拉伸", "矩形拉伸", "圆柱拉伸"], "exclude": [], "bridge": "bridge/atomic-extrude-base.md", "source": "source/atomic/extrude-base-profile.atomic.md", "capability_translation_rules": ["exact: extrude_add_blind"], "related": []},
|
||||
{"id": "atomic/fillet-chamfer-last", "kind": "atomic", "title": "Fillet And Chamfer Last", "priority": 57, "triggers": ["fillet chamfer last", "edge treatment", "chamfered edges", "filleted edges", "最后倒角", "最后圆角", "边缘处理"], "exclude": [], "bridge": "bridge/atomic-fillet-chamfer-last.md", "source": "source/atomic/fillet-chamfer-last.atomic.md", "capability_translation_rules": ["omitted: selector_unavailable", "exact: unique_selector"], "related": []},
|
||||
{"id": "atomic/pattern-holes-from-datum", "kind": "atomic", "title": "Pattern Holes From Datum", "priority": 56, "triggers": ["hole pattern", "pattern holes from datum", "repeated holes", "symmetric holes", "孔阵列", "重复孔", "基准孔阵列", "对称孔"], "exclude": [], "bridge": "bridge/atomic-pattern-holes.md", "source": "source/atomic/pattern-holes-from-datum.atomic.md", "capability_translation_rules": ["expanded: explicit_circle_layout", "exact: pattern_linear_or_pattern_mirror"], "related": []},
|
||||
{"id": "atomic/revolve-profile-around-axis", "kind": "atomic", "title": "Revolve Profile Around Axis", "priority": 55, "triggers": ["revolve profile", "revolve around axis", "revolved cut", "axisymmetric profile", "截面回转", "绕轴回转", "回转切除"], "exclude": [], "bridge": "bridge/atomic-revolve-profile.md", "source": "source/atomic/revolve-profile-around-axis.atomic.md", "capability_translation_rules": ["exact: revolve_add_or_revolve_cut"], "related": []},
|
||||
{"id": "atomic/threaded-hole-creation", "kind": "atomic", "title": "Threaded Hole", "priority": 54, "triggers": ["threaded hole", "tapped hole", "internal thread", "m3", "m4", "m5", "m6", "螺纹孔", "攻丝孔", "内螺纹"], "exclude": [], "bridge": "bridge/atomic-threaded-hole.md", "source": "source/atomic/threaded-hole-creation.atomic.md", "capability_translation_rules": ["approximated: thread_geometry_omitted"], "related": []},
|
||||
{"id": "atomic/through-hole-cut", "kind": "atomic", "title": "Through Hole Cut", "priority": 53, "triggers": ["through hole", "clearance hole", "central hole", "circular opening", "cut through", "bore through", "通孔", "间隙孔", "贯穿孔", "切穿"], "exclude": [], "bridge": "bridge/atomic-through-hole.md", "source": "source/atomic/through-hole-cut.atomic.md", "capability_translation_rules": ["exact: through_hole_or_explicit_cut"], "related": []}
|
||||
]
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# Coaxial Bore Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A model contains nested cylindrical features that must share one axis: bore, pocket, hub, sleeve, shaft clearance, bearing seat, pilot diameter, or counterbore.
|
||||
- The request mentions concentric, coaxial, central bore, shaft hole, bearing pocket, or flanged cylindrical geometry.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- Circular holes are independent mounting holes on a plate face.
|
||||
- The circular features intentionally use different centers.
|
||||
- The shape is decorative and has no functional axis.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Central bore or shaft clearance through a rotational or support body.
|
||||
- Concentric circles in sketch evidence.
|
||||
- Cylindrical faces nested around one axis.
|
||||
- Bearing seat, flange hub, sleeve, bushing, washer, spacer, or shaft-like part.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Use one explicit construction axis or shared sketch origin for all central cylindrical features.
|
||||
- Distinguish each diameter by role: clearance, seat, counterbore, outer diameter, pilot, groove.
|
||||
- Cut through holes through all relevant material; cut seats and counterbores only to their functional depth.
|
||||
- Do not create separate sketches whose centers can drift.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `axis` is the shared centerline.
|
||||
- `bore_diameter` controls the through opening.
|
||||
- `seat_diameter` controls bearing/pilot/counterbore fit.
|
||||
- `outer_diameter` controls surrounding material.
|
||||
- `depth` controls blind pocket, counterbore, or groove depth.
|
||||
- `lead_in_chamfer` controls assembly entry.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the shared axis using the sketch origin, construction line, or selected cylindrical face.
|
||||
2. Sketch concentric circles or a revolved profile from that axis.
|
||||
3. Cut the smallest through bore first when it establishes shaft clearance.
|
||||
4. Cut larger seats, pockets, or counterbores to controlled depth.
|
||||
5. Add grooves, chamfers, and fillets after the primary coaxial cuts are stable.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Off-axis bore because each circle was sketched from a different center.
|
||||
- Confusing through bore diameter with bearing seat diameter.
|
||||
- Cutting the bearing pocket through all and deleting the shoulder.
|
||||
- Treating unrelated bolt holes as coaxial features.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 50 reviewed SCAD58 flange and bearing-seat models.
|
||||
- Latest run evidence: accepted insights for central bore alignment, concentric circles, bearing seats, flanges, washers, bushings, and axisymmetric bodies.
|
||||
- Frequent operations in the latest run: Cut, RevCut, Revolution, Extrusion of concentric circles, Chamfer.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `coaxial bore`
|
||||
- `central bore`
|
||||
- `concentric`
|
||||
- `shaft clearance`
|
||||
|
||||
Secondary triggers:
|
||||
- `bearing seat`
|
||||
- `pilot bore`
|
||||
- `counterbore`
|
||||
- `hub`
|
||||
- `sleeve`
|
||||
|
||||
Operation triggers:
|
||||
- `concentric sketch`
|
||||
- `cut through bore`
|
||||
- `counterbore to depth`
|
||||
- `revolved cut`
|
||||
|
||||
Exclusions:
|
||||
- `random face holes`
|
||||
- `rectangular hole grid`
|
||||
- `offset holes`
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# Counterbored Hole Creation Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A fastener hole needs a cylindrical recess so a socket head, cap screw head, or flat seating surface sits flush or below the top face.
|
||||
- The request mentions counterbore, counterbored hole, socket head screw, cap screw, recessed screw head, or flat-bottom screw seat.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The screw head requires a conical countersink rather than a cylindrical counterbore.
|
||||
- The feature is a bearing pocket, large cylindrical recess, decorative ring, or simple through hole.
|
||||
- The part has no fastener-seat function.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Through hole plus larger coaxial shallow cylindrical recess.
|
||||
- HoleWzd evidence for counterbored screw holes.
|
||||
- Descriptions mention recessed screw heads, socket head screws, counterbore depth, or flush mounting.
|
||||
- Often appears on mounting plates, brackets, flanges, and covers.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Counterbore axis and through-hole axis are identical.
|
||||
- Counterbore diameter must be larger than through-hole diameter.
|
||||
- Counterbore depth must be enough for the screw head but not cut through the part unless requested.
|
||||
- Create the hole as one standard feature when possible to preserve fastener intent.
|
||||
- Pattern the complete counterbored hole feature when repeated.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `through_hole_diameter` defines screw clearance.
|
||||
- `counterbore_diameter` defines head clearance.
|
||||
- `counterbore_depth` defines head seating depth.
|
||||
- `fastener_size` defines standard screw selection.
|
||||
- `placement_point` defines hole center.
|
||||
- `end_condition` defines through, blind, or up-to-next behavior.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the mounting face where the screw head sits.
|
||||
2. Place the hole center from datums or pattern references.
|
||||
3. Create a counterbored hole feature with through diameter, counterbore diameter, and counterbore depth.
|
||||
4. Confirm the counterbore opens on the correct face.
|
||||
5. Pattern or mirror the full counterbored hole feature if repeated.
|
||||
6. Add small entrance chamfers only after the counterbore is correct.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Modeling only a shallow large circle without the through hole.
|
||||
- Reversing the counterbore to the wrong side of the part.
|
||||
- Making counterbore and through-hole diameters equal.
|
||||
- Using countersink geometry for a socket head screw.
|
||||
- Patterning only the through hole and forgetting the recess.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 49 accepted atomic insights matched counterbored hole creation.
|
||||
- Strong repeated names include `create_counterbored_hole`, `use_hole_wizard_for_counterbored_holes`, and `create_counterbore_hole_for_socket_head_screw`.
|
||||
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
|
||||
- Source model examples: 020555, 028735, 031527, 079425, 080632, 086114, 096301, 144289, 146696, 174675, 202122, 211326, 217613, 225627, 227232.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `counterbored hole`
|
||||
- `counterbore`
|
||||
- `socket head screw`
|
||||
- `cap screw`
|
||||
- `recessed screw head`
|
||||
|
||||
Secondary triggers:
|
||||
- `flush screw`
|
||||
- `counterbore depth`
|
||||
- `counterbore diameter`
|
||||
- `mounting hole`
|
||||
- `fastener seat`
|
||||
|
||||
Operation triggers:
|
||||
- `hole wizard counterbore`
|
||||
- `cut counterbore`
|
||||
- `pattern counterbored hole`
|
||||
- `chamfer hole edge`
|
||||
|
||||
Exclusions:
|
||||
- `countersink`
|
||||
- `bearing pocket`
|
||||
- `decorative ring`
|
||||
- `plain through hole`
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Extrude Base Profile Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The first solid body is a simple prismatic shape made from a closed 2D profile: rectangle, rounded rectangle, circle, annulus, hexagon, or simple contour.
|
||||
- The part is a plate, block, simple cylinder, bracket base, washer blank, spacer blank, or nut body made by extrusion.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The primary body has multiple coaxial diameter steps better made by revolve.
|
||||
- The shape is freeform, lofted, swept, bent sheet metal, or surface-based.
|
||||
- The extrusion profile is open or ambiguous.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- SolidWorks evidence such as Extrusion/Boss-Extrude after a single sketch.
|
||||
- STEP evidence dominated by planar faces for prismatic parts or cylindrical surfaces for simple cylinders.
|
||||
- Descriptions mention rectangular block, plate, simple cylinder, extruded profile, or uniform thickness.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Start from a fully closed, constrained sketch.
|
||||
- Extrude normal to the sketch plane by the intended thickness or length.
|
||||
- Use symmetric or mid-plane extrusion when the part should stay centered on a datum.
|
||||
- Keep the base body simple; add holes, pockets, bosses, slots, and edge treatments as later features.
|
||||
- Use concentric circles for annular extrusions such as washers or tubes.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `profile_type` selects rectangle, circle, annulus, hexagon, or custom closed contour.
|
||||
- `length`, `width`, `radius`, `outer_radius`, `inner_radius`, and `across_flats` define the sketch.
|
||||
- `extrude_depth` or `thickness` defines the solid depth.
|
||||
- `extrude_direction` and `midplane` define placement relative to datums.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the reference plane or face for the base profile.
|
||||
2. Draw and constrain the closed profile.
|
||||
3. Apply dimensional constraints for width, length, radius, or diameter.
|
||||
4. Extrude the profile to the required depth.
|
||||
5. Keep the resulting body merged as the main solid unless separate bodies are explicitly requested.
|
||||
6. Add secondary cuts and edge treatments afterward.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Using an open sketch for solid extrusion.
|
||||
- Combining holes and detailed cutouts into the first sketch when separate features would be clearer.
|
||||
- Using multiple separate extrusions for what should be one base body.
|
||||
- Extruding a stepped shaft when revolve would preserve axial intent better.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 70 accepted atomic insights matched base extrusion from rectangular/circular profiles.
|
||||
- Strong repeated names include `extrude_rectangle`, `extrude_circle`, `extrude_rectangle_from_sketch`, `extrude_rectangular_profile`, and `extrude_circular_profile`.
|
||||
- Frequent operations in the latest run: Extrusion, Sketch, Cut, Chamfer.
|
||||
- Source model examples: 000166, 026303, 050972, 053267, 088851, 114800, 148444, 157630, 162045, 188147, 209994, 220726, 237160, 238926.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `extrude base profile`
|
||||
- `extrude rectangle`
|
||||
- `extrude circle`
|
||||
- `base extrusion`
|
||||
- `simple cylinder`
|
||||
- `rectangular block`
|
||||
|
||||
Secondary triggers:
|
||||
- `plate blank`
|
||||
- `bracket base`
|
||||
- `washer blank`
|
||||
- `annular extrusion`
|
||||
- `uniform thickness`
|
||||
|
||||
Operation triggers:
|
||||
- `sketch closed profile`
|
||||
- `boss extrude`
|
||||
- `extrude to thickness`
|
||||
- `midplane extrusion`
|
||||
|
||||
Exclusions:
|
||||
- `stepped shaft`
|
||||
- `revolve profile`
|
||||
- `loft`
|
||||
- `sweep`
|
||||
- `sheet metal bend`
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Fillet Chamfer Last Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A machined part needs edge breaks, deburring chamfers, assembly lead-ins, stress-relief fillets, or small finishing rounds.
|
||||
- The edge treatment is secondary to the main body, holes, pockets, slots, and bores.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The rounded or chamfered profile is the primary defining shape, such as an O-ring groove, cam surface, ergonomic shell, or turbine blade.
|
||||
- A sharp edge is explicitly required for sealing, locating, scraping, or mating.
|
||||
- The chamfer/fillet would remove critical wall thickness around a hole, shoulder, or thin rib.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- SolidWorks feature evidence such as Chamfer, Fillet, radius values, or angle-distance chamfers.
|
||||
- STEP evidence with conical or toroidal surfaces near final edges.
|
||||
- Descriptions mentioning chamfered edges, rounded corners, smooth transitions, edge relief, assembly ease, or stress reduction.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Apply edge treatments after primary solids, holes, bores, pockets, slots, and patterns are stable.
|
||||
- Keep edge treatment small relative to nearby feature size and wall thickness.
|
||||
- Use chamfer for lead-in/deburring and fillet for stress relief or smooth load transition.
|
||||
- Select specific edges; do not blindly fillet or chamfer every edge.
|
||||
- Preserve functional shoulders, datum faces, seal lands, and fastener seats.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `chamfer_distance` defines bevel size.
|
||||
- `chamfer_angle` defines bevel angle, often 45 degrees when not otherwise specified.
|
||||
- `fillet_radius` defines round size.
|
||||
- `edge_set` defines which edges receive treatment.
|
||||
- `minimum_wall_after_edge_treatment` protects small holes, ribs, and pockets.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Complete the main body and all functional cuts.
|
||||
2. Identify functional edges that need lead-in, deburring, stress relief, or handling safety.
|
||||
3. Apply chamfers to hole entrances, exposed outer edges, or insertion edges.
|
||||
4. Apply fillets to internal corners, web transitions, and load-bearing transitions.
|
||||
5. Verify that holes, slots, shoulders, and bearing seats remain intact.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Adding large fillets early and causing later cuts or patterns to fail.
|
||||
- Chamfering a bearing shoulder, seal land, or datum face that should remain crisp.
|
||||
- Using one global fillet radius across tiny holes and large outer edges.
|
||||
- Treating decorative edge rounding as more important than functional geometry.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 58 reviewed SCAD58 machined parts with final chamfer/fillet operations.
|
||||
- Latest run evidence: high-frequency accepted insights for `chamfer_edges`, `chamfer_edges_for_safety`, `apply_chamfer_to_edges`, `apply_fillet_to_edges`, and stress-relief fillets.
|
||||
- Frequent operations in the latest run: Chamfer and Fillet after Extrusion, Cut, HoleWzd, Pattern, and Revolution.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `fillet chamfer last`
|
||||
- `edge treatment`
|
||||
- `chamfered edges`
|
||||
- `filleted edges`
|
||||
|
||||
Secondary triggers:
|
||||
- `deburr`
|
||||
- `assembly lead in`
|
||||
- `stress relief`
|
||||
- `smooth transition`
|
||||
- `edge break`
|
||||
|
||||
Operation triggers:
|
||||
- `chamfer`
|
||||
- `fillet`
|
||||
- `apply edge treatment after cuts`
|
||||
|
||||
Exclusions:
|
||||
- `primary rounded profile`
|
||||
- `sharp datum edge`
|
||||
- `seal land`
|
||||
- `cam surface`
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Pattern Holes From Datum Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A model has repeated holes or repeated cut features that should be controlled by pitch, angle, symmetry, or a construction datum.
|
||||
- The design intent is a hole row, hole grid, mirrored pair, circular bolt circle, or repeated mounting pattern.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- There is only one custom non-repeated hole.
|
||||
- Holes are intentionally irregular or individually dimensioned.
|
||||
- The feature is decorative and not constrained by functional layout.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Multiple holes with equal spacing, equal angular spacing, or mirrored positions.
|
||||
- SolidWorks evidence such as LinearPattern, CircularPattern, MirrorPattern, or repeated HoleWzd features.
|
||||
- Datum clues such as centerlines, edge offsets, pitch dimensions, central axis, or symmetry planes.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Build one correct seed hole or seed cut feature first.
|
||||
- Constrain the seed from a datum: edge offset, centerline, pitch circle, or axis.
|
||||
- Pattern the feature/cut, not only sketch circles.
|
||||
- Choose pattern type from geometry: linear for rows/grids, circular for flange bolt circles, mirror for symmetric pairs.
|
||||
- Keep the pattern editable by count, pitch, angle, and axis rather than hard-coded coordinates.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `seed_position` defines the first hole from the datum.
|
||||
- `pitch` defines linear spacing.
|
||||
- `count` defines number of repeated instances.
|
||||
- `pattern_direction` defines row/grid direction.
|
||||
- `pattern_axis` defines circular pattern axis.
|
||||
- `total_angle` is usually 360 degrees for full bolt circles.
|
||||
- `mirror_plane` defines symmetric duplication.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the datum: edge offset, centerline, symmetry plane, or central axis.
|
||||
2. Create and validate one seed hole or cut feature.
|
||||
3. Select linear, circular, or mirror pattern based on the part family and layout.
|
||||
4. Pattern the feature with explicit count and spacing/angle.
|
||||
5. Add shared counterbore/countersink/thread detail before patterning when all instances match.
|
||||
6. Verify clearance to edges, central bores, slots, and neighboring holes.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Duplicating sketch circles without actual cuts.
|
||||
- Using linear pattern for a flange bolt circle.
|
||||
- Using circular pattern for rectangular plate holes.
|
||||
- Patterning counterbore circles without patterning the underlying through hole.
|
||||
- Missing the datum so later size changes break alignment.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 58 reviewed SCAD58 models with repeated hole patterns.
|
||||
- Latest run evidence: accepted insights for linear repeated holes, mirror symmetry, symmetric hole placement, circular bolt patterns, and datum-driven construction geometry.
|
||||
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, CircularPattern.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `hole pattern`
|
||||
- `pattern holes from datum`
|
||||
- `repeated holes`
|
||||
- `symmetric holes`
|
||||
|
||||
Secondary triggers:
|
||||
- `linear pattern`
|
||||
- `circular pattern`
|
||||
- `mirror pattern`
|
||||
- `bolt holes`
|
||||
- `hole grid`
|
||||
|
||||
Operation triggers:
|
||||
- `pattern cut feature`
|
||||
- `linear pattern`
|
||||
- `circular pattern`
|
||||
- `mirror`
|
||||
|
||||
Exclusions:
|
||||
- `single custom hole`
|
||||
- `decorative circles`
|
||||
- `irregular holes`
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Revolve Profile Around Axis Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A single feature is created by revolving a 2D profile around an axis: stepped cylinder, shaft shoulder, sleeve, bushing, washer groove, nut blank, knob, or circular recess.
|
||||
- The feature has a rotational cross-section that is easier to define in side view.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- A plain constant-diameter cylinder can be created more simply by extruding a circle.
|
||||
- The profile is open, crosses the axis incorrectly, or represents non-axisymmetric geometry.
|
||||
- The feature is a rectangular plate, bracket wall, or arbitrary cutout.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Revolution/Revolve/RevCut feature evidence.
|
||||
- Sketch contains an axis line or construction centerline.
|
||||
- Multiple coaxial cylindrical, conical, or toroidal surfaces are expected.
|
||||
- Profile controls axial steps, shoulders, tapers, grooves, or rounded transitions.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- The revolve axis must be explicit and stable.
|
||||
- The profile must be closed for material-adding revolve.
|
||||
- Keep the profile on the correct side of the axis to avoid self-intersection.
|
||||
- Use 360 degrees for complete rotational solids unless a partial sector is requested.
|
||||
- Use revolved cut, not boss revolve, when removing a groove or internal recess.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `axis` defines the rotation center.
|
||||
- `profile` defines the closed side-view outline.
|
||||
- `revolve_angle` defines full or partial rotation.
|
||||
- `diameter_steps`, `axial_lengths`, `groove_depth`, and `taper_angle` define shape.
|
||||
- `operation_type` selects add or cut.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Create a sketch on a plane that contains the desired axis.
|
||||
2. Draw a construction axis or select a stable centerline.
|
||||
3. Draw the closed profile for added material, or the removal profile for a revolved cut.
|
||||
4. Revolve the profile around the axis, usually 360 degrees.
|
||||
5. Validate that the result is one solid and all intended circular features are coaxial.
|
||||
6. Add later holes, flats, slots, patterns, chamfers, and fillets as separate features.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Revolving around the wrong edge or temporary line.
|
||||
- Letting the sketch cross the axis and causing invalid geometry.
|
||||
- Making several extruded cylinders instead of a single coherent turned profile.
|
||||
- Using revolve for non-axisymmetric bracket or plate features.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 81 accepted atomic insights matched revolve, revolved profile, or revolved cut operations.
|
||||
- Strong repeated names include `revolve_profile`, `create_revolved_cut`, `revolve_profile_around_axis`, and `revolved_cut_for_internal_thread`.
|
||||
- Frequent operations in the latest run: Revolution, RevCut, Sketch, Cut, Chamfer, Fillet.
|
||||
- Source model examples: 009934, 010137, 015133, 026624, 053895, 079633, 086237, 094412, 139073, 146478, 161210, 162241, 222550, 238044.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `revolve profile`
|
||||
- `revolve around axis`
|
||||
- `revolved cut`
|
||||
- `axisymmetric profile`
|
||||
|
||||
Secondary triggers:
|
||||
- `shaft shoulder`
|
||||
- `turned groove`
|
||||
- `bushing`
|
||||
- `washer`
|
||||
- `nut blank`
|
||||
- `stepped diameter`
|
||||
|
||||
Operation triggers:
|
||||
- `draw construction axis`
|
||||
- `revolve 360 degrees`
|
||||
- `revolved cut`
|
||||
- `sketch half profile`
|
||||
|
||||
Exclusions:
|
||||
- `plain extruded cylinder`
|
||||
- `rectangular plate`
|
||||
- `bracket wall`
|
||||
- `loft`
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# Threaded Hole Creation Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part needs an internal thread, tapped hole, threaded mounting hole, or screw hole with a specified metric/imperial size.
|
||||
- The request mentions M3, M4, M5, M6, M8, M10, tapped hole, threaded hole, or thread depth.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The feature is an external thread on a bolt, screw, or shaft.
|
||||
- The hole is only a smooth clearance hole.
|
||||
- The model should avoid thread geometry and only show a simple bore unless the prompt asks for thread detail.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- SolidWorks evidence such as HoleWzd threaded hole or thread parameters.
|
||||
- Descriptions mention fastening, tapped holes, internal thread, or screw mounting.
|
||||
- Hole is located on a planar face, boss face, or shaft end face.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Use a standard threaded hole feature or thread callout when available.
|
||||
- Keep the threaded hole axis normal to the placement face unless angled threading is requested.
|
||||
- Separate tap drill diameter, nominal thread size, hole depth, and thread depth.
|
||||
- Keep enough wall material around the threaded hole.
|
||||
- Add lead-in chamfer after the thread/hole is placed.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `thread_size` defines nominal size such as M3/M4/M5/M6/M8/M10.
|
||||
- `thread_pitch` defines fine/coarse pitch when specified.
|
||||
- `thread_depth` defines threaded length.
|
||||
- `hole_depth` defines blind hole depth if not through.
|
||||
- `placement_point` and `face_normal` define location and direction.
|
||||
- `lead_in_chamfer` defines assembly entry.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the planar face or end face for the threaded hole.
|
||||
2. Place the hole center from datums, centerlines, or axis references.
|
||||
3. Create the hole using a threaded HoleWzd/hole feature with size and depth.
|
||||
4. If the backend cannot model threads, create the correct pilot bore and preserve thread intent in naming/comments.
|
||||
5. Pattern or mirror the threaded hole feature when repeated.
|
||||
6. Add a small lead-in chamfer after the threaded feature is stable.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Modeling a threaded hole as a plain through cylinder.
|
||||
- Creating external thread geometry on an internal hole.
|
||||
- Placing threaded holes too close to edges, slots, or pockets.
|
||||
- Forgetting blind depth or thread depth.
|
||||
- Adding visual helical threads that make the model heavy or fragile when a callout is enough.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 87 accepted atomic insights matched threaded/tapped hole creation.
|
||||
- Strong repeated names include `create_threaded_hole_with_hole_wizard`, `create_threaded_hole`, `threaded_hole_placement`, and `use_hole_wizard_for_threaded_holes`.
|
||||
- Frequent operations in the latest run: HoleWzd, Cut, RevCut, Chamfer, Pattern.
|
||||
- Source model examples: 000924, 011524, 017151, 047238, 077661, 080683, 095520, 105855, 145389, 160434, 174675, 200790, 206894, 211326, 225767.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `threaded hole`
|
||||
- `tapped hole`
|
||||
- `internal thread`
|
||||
- `M3`
|
||||
- `M4`
|
||||
- `M5`
|
||||
- `M6`
|
||||
- `M8`
|
||||
- `M10`
|
||||
|
||||
Secondary triggers:
|
||||
- `thread depth`
|
||||
- `tap drill`
|
||||
- `screw mounting`
|
||||
- `threaded boss`
|
||||
- `end threaded hole`
|
||||
|
||||
Operation triggers:
|
||||
- `hole wizard threaded hole`
|
||||
- `create tapped hole`
|
||||
- `add thread callout`
|
||||
- `lead in chamfer`
|
||||
|
||||
Exclusions:
|
||||
- `external thread`
|
||||
- `bolt`
|
||||
- `screw`
|
||||
- `clearance hole only`
|
||||
- `decorative helix`
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
# Through Hole Cut Atomic Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part needs a clean circular hole or bore that passes fully through a plate, block, bracket wall, flange, washer, spacer, or boss.
|
||||
- The request mentions through hole, clearance hole, central hole, circular opening, bore through, or cut through all.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The hole is threaded, counterbored, countersunk, or a bearing seat requiring special geometry.
|
||||
- The circular feature is a blind pocket, groove, decorative recess, or non-through relief.
|
||||
- The hole is part of a patterned bolt circle; combine with the pattern skill for repeated holes.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Circular sketch on a planar face followed by Cut-Extrude or HoleWzd through all.
|
||||
- Cylindrical internal face runs through the full thickness.
|
||||
- Used for clearance, alignment, lightening, shaft passage, or central opening.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Hole axis should be normal to the face or coaxial with the main part axis.
|
||||
- Use through-all end condition when the hole must pass through the entire body.
|
||||
- Keep hole diameter and placement explicit.
|
||||
- For central holes, use the part axis or centerline as the placement reference.
|
||||
- For repeated holes, make one seed through hole and pattern it.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `hole_diameter` defines opening size.
|
||||
- `placement_point` defines center on the start face.
|
||||
- `axis` defines coaxial central placement when needed.
|
||||
- `end_condition` is usually through all.
|
||||
- `face` defines the start plane.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the face or reference plane where the hole starts.
|
||||
2. Place a circle center from datums, centerlines, or the main axis.
|
||||
3. Sketch the circle with the required diameter.
|
||||
4. Cut through all material in the intended direction.
|
||||
5. Pattern or mirror the through-hole cut if repeated.
|
||||
6. Add hole edge chamfers only after the cut exists.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Creating a shallow blind pocket when a through hole is required.
|
||||
- Leaving a sketch circle without cutting material.
|
||||
- Placing a central hole off-axis.
|
||||
- Treating threaded/counterbored/bearing holes as plain through holes.
|
||||
- Cutting through unintended bodies in an assembly-like model.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 49 accepted atomic insights matched through-hole and circular-cut creation.
|
||||
- Strong repeated names include `create_through_hole`, `cut_circular_hole`, `cut_circle`, `cut_central_bore_through_plate`, and `create_through_holes_with_cut`.
|
||||
- Frequent operations in the latest run: Cut, HoleWzd, CircularPattern, LinearPattern, Chamfer.
|
||||
- Source model examples: 001563, 001942, 046907, 054633, 059866, 080145, 086131, 096474, 142955, 148675, 153729, 160077, 222192, 230562, 241179.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `through hole`
|
||||
- `clearance hole`
|
||||
- `central hole`
|
||||
- `circular opening`
|
||||
- `cut through`
|
||||
- `bore through`
|
||||
|
||||
Secondary triggers:
|
||||
- `shaft passage`
|
||||
- `alignment hole`
|
||||
- `lightening hole`
|
||||
- `hole through plate`
|
||||
- `hole through boss`
|
||||
|
||||
Operation triggers:
|
||||
- `cut extrude through all`
|
||||
- `cut circle`
|
||||
- `hole wizard through hole`
|
||||
- `pattern through hole`
|
||||
|
||||
Exclusions:
|
||||
- `threaded hole`
|
||||
- `counterbored hole`
|
||||
- `countersink`
|
||||
- `bearing seat`
|
||||
- `blind pocket`
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# Axisymmetric Revolve Strategy Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part or major feature is rotationally symmetric around an axis: shaft, stepped shaft, flange hub, bushing, washer, spacer, sleeve, pulley-like blank, turned knob, or nut blank.
|
||||
- The profile contains steps, shoulders, grooves, tapers, spherical/rounded transitions, or coaxial bores that are easier to define in a half-section.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The body is a simple constant-diameter cylinder that can be created by one circle extrusion.
|
||||
- The part is primarily prismatic with only small cylindrical holes.
|
||||
- The geometry is freeform, non-axisymmetric, or has asymmetric lugs as the dominant feature.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Dominant central axis and circular cross-sections.
|
||||
- SolidWorks features such as Revolution, Revolve, RevCut, or revolved thread/groove cuts.
|
||||
- STEP evidence with multiple cylindrical, conical, or toroidal surfaces sharing an axis.
|
||||
- Functional roles include shafting, spacing, sealing, rotation, alignment, or fastening.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Sketch the axial cross-section on a plane that contains the rotation axis.
|
||||
- Keep the revolved profile on one side of the axis unless the CAD kernel expects a centerline profile.
|
||||
- Use one 360-degree revolve for the main axisymmetric body where possible.
|
||||
- Use revolved cuts for coaxial grooves, internal thread reliefs, and circular recesses.
|
||||
- Add asymmetric holes, flats, keyways, or bolt patterns after the revolved body is complete.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `axis` defines the rotation centerline.
|
||||
- `profile_points` define the half-section outline.
|
||||
- `diameters` and `axial_lengths` define steps and shoulders.
|
||||
- `revolve_angle` is usually 360 degrees for a complete solid.
|
||||
- `groove_width`, `groove_depth`, `taper_angle`, and `fillet_radius` define turned details.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the central axis and an axial sketch plane.
|
||||
2. Draw the half-section profile with all major diameters, steps, shoulders, and bore boundaries.
|
||||
3. Revolve the closed profile 360 degrees to create the main body.
|
||||
4. Use revolved cuts for grooves, recesses, or thread reliefs that are also axisymmetric.
|
||||
5. Add non-axisymmetric secondary features such as flats, slots, keyways, or bolt holes.
|
||||
6. Apply chamfers and fillets at ends and shoulders last.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Extruding several cylinders separately and leaving seams or separate bodies.
|
||||
- Revolving a profile that crosses the axis and creates invalid self-intersections.
|
||||
- Forgetting the construction axis or using an arbitrary edge as axis.
|
||||
- Adding asymmetric features before the revolve and making the base profile ambiguous.
|
||||
- Using revolve for a simple plain cylinder when extrusion is clearer.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 105 accepted functional insights matched axisymmetric revolve and revolved-cut strategies.
|
||||
- Strong repeated names include `use_revolve_for_axisymmetric_parts`, `use_revolve_for_rotational_parts`, `use_revolve_for_axisymmetric_body`, and `use_revolution_for_axisymmetric_features`.
|
||||
- Frequent operations in the latest run: Revolution, RevCut, Cut, Chamfer, Fillet, HoleWzd.
|
||||
- Source model examples: 001563, 001957, 009934, 010137, 025245, 053895, 080683, 086232, 094412, 135604, 144887, 162241, 173716, 218803, 225038.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `axisymmetric`
|
||||
- `revolve`
|
||||
- `revolved body`
|
||||
- `rotational part`
|
||||
- `turned part`
|
||||
|
||||
Secondary triggers:
|
||||
- `shaft`
|
||||
- `bushing`
|
||||
- `washer`
|
||||
- `sleeve`
|
||||
- `flange hub`
|
||||
- `stepped diameter`
|
||||
- `groove`
|
||||
|
||||
Operation triggers:
|
||||
- `sketch half profile`
|
||||
- `revolve profile`
|
||||
- `revolved cut`
|
||||
- `define construction axis`
|
||||
- `chamfer shoulders`
|
||||
|
||||
Exclusions:
|
||||
- `simple extruded cylinder`
|
||||
- `rectangular plate`
|
||||
- `mounting bracket`
|
||||
- `freeform surface`
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# Bearing Bore Seat Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part must locate a bearing, bushing, sleeve, or shaft using a coaxial bore, counterbore, pocket, shoulder, or retaining seat.
|
||||
- The requested model is a housing/seat component, bracketed bearing support, bushing support, or bearing retainer.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The part only has generic decorative holes or a simple washer hole.
|
||||
- The task is to create a full bearing assembly with rolling elements.
|
||||
- The central hole is only a screw clearance hole and not a shaft/bearing datum.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Through bore for shaft clearance.
|
||||
- Larger coaxial pocket for bearing outside diameter.
|
||||
- Shoulder or depth stop for axial location.
|
||||
- Optional retaining groove, cover recess, set-screw hole, lubrication hole, or mounting base.
|
||||
- Bore entry chamfers for assembly.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Shaft clearance bore and bearing seat remain coaxial.
|
||||
- Seat diameter is larger than shaft clearance diameter when a bearing pocket is present.
|
||||
- Seat depth must be represented; a bearing seat is not just a surface ring.
|
||||
- Shoulder geometry should remain after the pocket cut.
|
||||
- Mounting holes must not break into the bearing seat unless explicitly designed.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `axis` defines the bore centerline.
|
||||
- `shaft_clearance_diameter` defines the through hole.
|
||||
- `bearing_outer_diameter` defines the pocket/seat diameter.
|
||||
- `seat_depth` defines bearing insertion depth.
|
||||
- `shoulder_diameter` and `shoulder_height` define axial stop.
|
||||
- `retaining_groove_width` and `retaining_groove_depth` define optional retention.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Create or select the main housing body.
|
||||
2. Establish a construction axis through the intended shaft centerline.
|
||||
3. Cut the shaft clearance bore through all relevant material.
|
||||
4. Cut the bearing seat/counterbore to controlled depth from the assembly side.
|
||||
5. Preserve or create the shoulder that stops the bearing.
|
||||
6. Add retaining grooves, set-screw holes, lubrication holes, and mounting holes only after the bore/seat is correct.
|
||||
7. Apply bore lead-in chamfers and small external fillets last.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Making the seat blind depth ambiguous.
|
||||
- Losing the shoulder by cutting the same diameter through the whole body.
|
||||
- Off-axis bearing pocket relative to shaft bore.
|
||||
- Treating bearing housing as complete bearing assembly.
|
||||
- Adding mounting holes before the seat and accidentally breaking the pocket wall.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 24 reviewed SCAD58 bearing housing/seat models.
|
||||
- Latest run evidence: accepted insights for central bore alignment, coaxial cylindrical features, counterbore/seat construction, and bore chamfering.
|
||||
- Frequent operations in the latest run: Cut, RevCut, Revolution, HoleWzd, Chamfer, Fillet.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `bearing bore seat`
|
||||
- `bearing pocket`
|
||||
- `bearing seat`
|
||||
- `bushing bore`
|
||||
- `shaft clearance bore`
|
||||
|
||||
Secondary triggers:
|
||||
- `coaxial bore`
|
||||
- `counterbore`
|
||||
- `shoulder`
|
||||
- `retaining groove`
|
||||
- `bearing housing`
|
||||
|
||||
Operation triggers:
|
||||
- `cut through bore`
|
||||
- `counterbore to depth`
|
||||
- `preserve shoulder`
|
||||
- `chamfer bore`
|
||||
|
||||
Exclusions:
|
||||
- `flange only`
|
||||
- `plain washer`
|
||||
- `decorative circular hole`
|
||||
- `complete rolling bearing`
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
# Flange Bolt Circle Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A flange, circular cover, annular ring, coupling, or flanged sleeve needs fastener holes evenly spaced around a central bore.
|
||||
- The request mentions bolt circle, pitch circle, PCD, evenly spaced flange holes, or circular pattern of screws.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- Holes are in a rectangular grid, linear row, or mirrored pair on a plate or bracket.
|
||||
- The part has no central axis or central bore.
|
||||
- The holes are decorative circular cutouts rather than fastening holes.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Central bore or hub defines the rotation axis.
|
||||
- Bolt holes lie on a pitch circle concentric with the bore.
|
||||
- Angular spacing is equal: `360 / bolt_count`.
|
||||
- Optional counterbores or countersinks remain concentric with each bolt hole.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Bolt-circle center, central bore, hub, and outer diameter share the same axis.
|
||||
- Create one correct seed hole with the correct diameter and seat type.
|
||||
- Circular-pattern the hole feature, not just sketch circles.
|
||||
- Keep bolt count, pitch-circle diameter, and start angle explicit.
|
||||
- Add counterbore/countersink to the seed hole before patterning when all holes share the same fastener type.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `bolt_count` controls number of fastener holes.
|
||||
- `bolt_circle_diameter` or `pitch_circle_diameter` controls radial placement.
|
||||
- `bolt_hole_diameter` defines clearance or pilot size.
|
||||
- `start_angle` controls angular orientation relative to a datum.
|
||||
- `counterbore_diameter`, `counterbore_depth`, and `countersink_angle` define fastener seating.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the central axis from the flange bore or construction geometry.
|
||||
2. Place one seed hole center at radius `bolt_circle_diameter / 2`.
|
||||
3. Create the seed hole as a cut, HoleWzd hole, counterbore, or countersink according to the fastener.
|
||||
4. Use circular pattern around the central axis for `bolt_count` instances over 360 degrees.
|
||||
5. Verify that every patterned hole cuts through the flange and remains inside the outer diameter.
|
||||
6. Add final chamfers and fillets after the bolt pattern is complete.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Manually placing each hole and producing uneven angular spacing.
|
||||
- Patterning a sketch without cutting material.
|
||||
- Using a linear array on a circular flange.
|
||||
- Choosing pitch circle too close to the bore or outer edge.
|
||||
- Forgetting counterbore/countersink depth for flush fasteners.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 45 reviewed SCAD58 flange-like models.
|
||||
- Latest run evidence: accepted atomic and functional insights for circular pattern bolt holes, central bore alignment, and axisymmetric flange bodies.
|
||||
- Frequent operations in the latest run: CircularPattern, HoleWzd, Cut, Revolution, Chamfer.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `flange bolt circle`
|
||||
- `bolt circle`
|
||||
- `pitch circle`
|
||||
- `PCD`
|
||||
- `circular bolt pattern`
|
||||
|
||||
Secondary triggers:
|
||||
- `flange holes`
|
||||
- `evenly spaced holes`
|
||||
- `central bore`
|
||||
- `counterbored flange holes`
|
||||
|
||||
Operation triggers:
|
||||
- `circular pattern`
|
||||
- `hole wizard`
|
||||
- `pattern cut feature`
|
||||
- `cut bolt holes`
|
||||
|
||||
Exclusions:
|
||||
- `linear hole grid`
|
||||
- `mounting plate row`
|
||||
- `random holes`
|
||||
- `side tab holes`
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
# Mounting Plate Hole Layout Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A plate or base block needs holes, slots, counterbores, countersinks, or threaded holes positioned from datum edges or centerlines.
|
||||
- The layout is rectangular, row-based, symmetric, mirrored, or grid-like rather than circular around a flange axis.
|
||||
- The hole layout controls mounting, alignment, adjustment, or fastening.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- Holes are evenly spaced around a circular bolt circle on a flange.
|
||||
- The holes are decorative, random, or not functionally related.
|
||||
- The main operation is a bearing bore or central shaft seat rather than a mounting-hole layout.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Flat planar top face or side face used as the hole placement plane.
|
||||
- Edge margins, centerline offsets, pitch values, or symmetric pairs.
|
||||
- Repeated hole groups, slots, threaded holes, clearance holes, counterbores, countersinks, or dowel holes.
|
||||
- SolidWorks evidence often appears as HoleWzd, Cut, LPattern, MirrorPattern, and Chamfer.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Define plate datums before hole placement.
|
||||
- Keep hole centers constrained to margins, pitch, and symmetry instead of free coordinates.
|
||||
- Select the hole type from fastening function, not from appearance.
|
||||
- Pattern the cut or hole feature after the seed feature is correct.
|
||||
- Preserve minimum material around holes and between holes.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `datum_edge_x`, `datum_edge_y`, and `centerline` define references.
|
||||
- `edge_margin_x`, `edge_margin_y`, `pitch_x`, `pitch_y`, `row_count`, and `column_count` define repeated layouts.
|
||||
- `hole_diameter`, `thread_size`, `fit`, and `end_condition` define the hole itself.
|
||||
- `counterbore_diameter`, `counterbore_depth`, `countersink_angle`, and `slot_length` define fastener seating or adjustability.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the plate face and define construction centerlines or edge-offset dimensions.
|
||||
2. Create one seed hole or one seed hole group using HoleWzd or a through cut.
|
||||
3. Use linear pattern for rows/grids and mirror for symmetric pairs.
|
||||
4. Use slots when the design requires adjustment rather than fixed bolt location.
|
||||
5. Add counterbore, countersink, or thread data to the seed hole before patterning where possible.
|
||||
6. Add chamfers and deburring features after the functional holes are complete.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Converting counterbored/threaded/countersunk holes into plain cylinders.
|
||||
- Patterning sketch circles instead of the actual cut or hole feature.
|
||||
- Mixing flange bolt-circle logic into rectangular plate layouts.
|
||||
- Missing datum margins, causing holes to float.
|
||||
- Placing slots too close to edges or other holes.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 207 reviewed SCAD58 plate-like models.
|
||||
- Latest run evidence: high-frequency accepted insights for hole wizard use, standard hole types, symmetric hole placement, linear hole patterns, mirror patterns, slots, and edge chamfers.
|
||||
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `mounting plate hole layout`
|
||||
- `base plate hole pattern`
|
||||
- `rectangular hole grid`
|
||||
- `symmetric mounting holes`
|
||||
|
||||
Secondary triggers:
|
||||
- `edge margin`
|
||||
- `linear pitch`
|
||||
- `counterbored holes`
|
||||
- `threaded holes`
|
||||
- `slots`
|
||||
- `dowel holes`
|
||||
|
||||
Operation triggers:
|
||||
- `hole wizard`
|
||||
- `linear pattern`
|
||||
- `mirror holes`
|
||||
- `cut through holes`
|
||||
- `counterbore`
|
||||
- `countersink`
|
||||
|
||||
Exclusions:
|
||||
- `flange bolt circle`
|
||||
- `bearing bore`
|
||||
- `random decorative holes`
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
# Slotted Adjustment Feature Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A plate, bracket, clamp, guide, or mounting part needs an elongated slot for positional adjustment, clearance, sliding, or alignment.
|
||||
- The request mentions slot, slotted hole, adjustment slot, elongated hole, guide slot, keyway-like slot, or obround cutout.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The feature is a simple round hole with no adjustment function.
|
||||
- The slot is actually a shaft keyway or turned groove; use a shaft/keyway-specific rule if available.
|
||||
- The slot is decorative and not tied to fastening, travel, or clearance.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Elongated hole with two semicircular ends and straight sides.
|
||||
- Slot located on a flat plate/bracket face or through a wall.
|
||||
- Often paired, mirrored, or patterned.
|
||||
- Can coexist with counterbored holes, threaded holes, ribs, or edge chamfers.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Slot width should match the fastener/shaft clearance role.
|
||||
- Slot length defines adjustment travel and must be longer than the fastener diameter.
|
||||
- Keep end radii equal to half the slot width for manufacturable milled/slot features.
|
||||
- Dimension slots from datums and centerlines.
|
||||
- Cut slots after the base body exists and before final chamfers/fillets.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `slot_width` defines cutter or fastener clearance.
|
||||
- `slot_length` defines adjustment range.
|
||||
- `end_radius` is usually `slot_width / 2`.
|
||||
- `slot_center`, `slot_angle`, and `edge_margin` locate the slot.
|
||||
- `slot_depth` or `through_all` defines end condition.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Select the planar face or reference plane where the slot starts.
|
||||
2. Sketch an obround slot from centerline, length, and width.
|
||||
3. Cut through all or to the required depth.
|
||||
4. Pattern or mirror the slot if repeated.
|
||||
5. Add small chamfers to slot edges after the cut is complete.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Modeling a slot as a rectangle with sharp internal corners.
|
||||
- Making the slot length equal to the hole diameter, removing adjustment function.
|
||||
- Placing slots without enough wall material at the ends.
|
||||
- Confusing a bracket adjustment slot with a shaft keyway or revolved groove.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 38 accepted functional insights matched slot, slotted adjustment, guide slot, or slot/cutout behavior.
|
||||
- Strong repeated names include `use_slots_for_adjustment`, `use_cut_extrude_for_holes_and_slots`, `central_slot_for_alignment`, and `cut_slots_for_adjustment`.
|
||||
- Frequent operations in the latest run: Cut, Extrusion, LinearPattern, MirrorPattern, Chamfer.
|
||||
- Source model examples: 004764, 005640, 006514, 011853, 026940, 043134, 058682, 096676, 106216, 156844, 163197, 185756, 199739, 213507, 240416.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `slot`
|
||||
- `slotted hole`
|
||||
- `adjustment slot`
|
||||
- `elongated hole`
|
||||
- `guide slot`
|
||||
|
||||
Secondary triggers:
|
||||
- `obround`
|
||||
- `clearance slot`
|
||||
- `mounting adjustment`
|
||||
- `sliding`
|
||||
- `alignment slot`
|
||||
|
||||
Operation triggers:
|
||||
- `sketch slot`
|
||||
- `cut extrude slot`
|
||||
- `mirror slot`
|
||||
- `linear pattern slot`
|
||||
- `chamfer slot edge`
|
||||
|
||||
Exclusions:
|
||||
- `round hole only`
|
||||
- `shaft keyway`
|
||||
- `revolved groove`
|
||||
- `decorative cutout`
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# Standard Hole Wizard Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part needs standard clearance holes, threaded/tapped holes, counterbored holes, countersunk holes, dowel holes, or screw-seat holes.
|
||||
- The request mentions screw size, metric thread, socket head screw, flat head screw, fastener clearance, tapping, counterbore, countersink, or Hole Wizard-like behavior.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The circular feature is a bearing bore, shaft bore, decorative opening, large window, or non-standard milled pocket.
|
||||
- The hole is part of a freeform surface where a standard hole feature cannot be cleanly placed.
|
||||
- The user asks for detailed thread geometry and the CAD backend cannot represent it reliably.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Feature evidence such as HoleWzd, threaded hole, counterbored hole, countersunk hole, through hole, or standard metric sizes like M3, M4, M5, M6, M8, M10.
|
||||
- Holes are used for fastening, alignment, or assembly rather than decoration.
|
||||
- Hole positions are points on a planar face, often patterned or mirrored.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Select hole type before modeling: clearance, threaded, counterbore, countersink, or dowel.
|
||||
- Keep hole axis normal to the placement face unless an angled hole is explicitly requested.
|
||||
- Specify end condition: through all, blind depth, up to next, or thread depth.
|
||||
- Preserve fastener seating geometry: counterbore/countersink is not a decorative ring.
|
||||
- Pattern or mirror the completed hole feature when identical holes repeat.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `hole_type` selects clearance, threaded, counterbore, countersink, dowel, or simple through hole.
|
||||
- `standard` and `thread_size` define metric/imperial thread family when available.
|
||||
- `hole_diameter`, `tap_drill_diameter`, `thread_depth`, and `hole_depth` define cut size.
|
||||
- `counterbore_diameter`, `counterbore_depth`, and `countersink_angle` define screw head seating.
|
||||
- `placement_points`, `edge_offsets`, and `pattern_count` define layout.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Identify the fastening or alignment function for each hole group.
|
||||
2. Choose the matching standard hole type and size.
|
||||
3. Place hole center points on the correct face using datums, centerlines, or coordinates.
|
||||
4. Create the hole feature with diameter, depth, thread, and seating data.
|
||||
5. Pattern or mirror the completed hole feature when the group repeats.
|
||||
6. Add entrance chamfers only after the hole geometry is correct.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Modeling all holes as plain cylinders and losing thread/counterbore/countersink meaning.
|
||||
- Adding visual thread helixes where a thread callout or standard threaded hole is enough.
|
||||
- Forgetting counterbore depth for socket head screws.
|
||||
- Patterning hole center sketches without patterning the hole cut.
|
||||
- Using non-standard sizes when the prompt specifies standard fasteners.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 149 accepted functional insights matched standard hole creation and fastener-hole logic.
|
||||
- Strong repeated names include `use_hole_wizard_for_standard_holes`, `use_counterbored_holes_for_flat_head_screws`, `use_hole_wizard_for_threaded_holes`, and `use_hole_wizard_for_counterbore_holes`.
|
||||
- Frequent operations in the latest run: HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer.
|
||||
- Source model examples: 001082, 001607, 003846, 011721, 014040, 029487, 041650, 061936, 074693, 094929, 102841, 145389, 160434, 211326, 227986.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `hole wizard`
|
||||
- `standard holes`
|
||||
- `threaded holes`
|
||||
- `counterbored holes`
|
||||
- `countersunk holes`
|
||||
- `tapped holes`
|
||||
|
||||
Secondary triggers:
|
||||
- `M3`
|
||||
- `M4`
|
||||
- `M5`
|
||||
- `M6`
|
||||
- `M8`
|
||||
- `socket head screw`
|
||||
- `flat head screw`
|
||||
|
||||
Operation triggers:
|
||||
- `create hole wizard feature`
|
||||
- `create threaded hole`
|
||||
- `create counterbore`
|
||||
- `create countersink`
|
||||
- `pattern hole feature`
|
||||
|
||||
Exclusions:
|
||||
- `bearing bore`
|
||||
- `shaft bore`
|
||||
- `decorative hole`
|
||||
- `large cutout`
|
||||
- `freeform pocket`
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
# Symmetric Feature Layout Functional Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- A part has holes, cutouts, bosses, tabs, ribs, or slots arranged symmetrically about a centerline, midplane, or axis.
|
||||
- The prompt mentions symmetric holes, mirrored features, balanced mounting, centered layout, equal offsets, or left/right duplicates.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- Features are intentionally asymmetric for clearance, indexing, handedness, or mating orientation.
|
||||
- The only symmetry is the natural circular symmetry of a plain shaft or washer with no repeated secondary features.
|
||||
- The model requires unique hand-placed features with different sizes.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Hole pairs, mirrored slots, duplicated bosses, matching tabs, or opposite-side cutouts.
|
||||
- SolidWorks features such as MirrorPattern or sketches with construction centerlines.
|
||||
- Descriptions mention symmetry, balanced fastening, alignment, centered holes, or mirrored geometry.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Define the symmetry plane, centerline, or axis before placing the seed feature.
|
||||
- Model one side or one quadrant carefully, then mirror/pattern it.
|
||||
- Keep symmetric features tied to the same dimensions so edits preserve balance.
|
||||
- Do not mirror features that are explicitly handed or direction-specific.
|
||||
- Combine with hole-type rules when mirrored features are fastener holes.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `symmetry_plane` or `centerline` defines the mirror reference.
|
||||
- `seed_feature` defines the original hole, slot, boss, or cutout.
|
||||
- `offset_from_center`, `edge_margin`, and `pitch` define placement.
|
||||
- `mirror_count`, `pattern_count`, and `quadrant_count` define repetition intent.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Identify the natural datum: plate centerline, bracket midplane, flange axis, or body midplane.
|
||||
2. Create construction geometry for the symmetry reference.
|
||||
3. Build the seed feature on one side with full dimensions.
|
||||
4. Mirror or pattern the completed feature across the reference.
|
||||
5. Verify that all mirrored features remain inside material and keep correct hole type/depth.
|
||||
6. Apply common edge treatments after mirrored features are complete.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Manually placing mirrored holes and getting unequal offsets.
|
||||
- Mirroring asymmetric features such as countersinks that should face a specific side.
|
||||
- Losing thread/counterbore data during mirror or pattern.
|
||||
- Creating a visually symmetric model with unconstrained feature positions.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 228 accepted functional insights contained symmetry, mirror, centerline, or pattern alignment signals.
|
||||
- Strong repeated names include `use_mirror_pattern_for_symmetry`, `position_holes_symmetrically`, `symmetrical_hole_placement`, and `use_mirror_for_symmetry`.
|
||||
- Frequent operations in the latest run: MirrorPattern, LinearPattern, HoleWzd, Cut, construction lines, Chamfer.
|
||||
- Source model examples: 000124, 002620, 008006, 012598, 029447, 032081, 059866, 097101, 125932, 134012, 145543, 154893, 166339, 173306, 202020.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `symmetric holes`
|
||||
- `mirror pattern`
|
||||
- `symmetry`
|
||||
- `mirrored features`
|
||||
- `centerline layout`
|
||||
|
||||
Secondary triggers:
|
||||
- `balanced mounting`
|
||||
- `equal offsets`
|
||||
- `left right holes`
|
||||
- `construction line`
|
||||
- `midplane`
|
||||
|
||||
Operation triggers:
|
||||
- `create construction centerline`
|
||||
- `mirror feature`
|
||||
- `pattern symmetric holes`
|
||||
- `use equal constraints`
|
||||
|
||||
Exclusions:
|
||||
- `asymmetric clearance`
|
||||
- `handed part`
|
||||
- `unique holes`
|
||||
- `plain cylinder only`
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
# Bearing Housing Or Seat Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a bearing housing, bearing seat, bearing support, bearing block, pillow-block-like support, bushing seat, shaft support, or retainer body.
|
||||
- The central functional feature is a precise coaxial bore, pocket, sleeve, shoulder, or counterbore that locates a bearing or bushing.
|
||||
- The part is a single machined component that supports a shaft or bearing, not the rolling bearing assembly itself.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The user asks for a complete bearing with balls, rollers, cage, inner race, and outer race.
|
||||
- The bearing word is only speculative and there is no bore, seat, sleeve, pocket, or retaining geometry.
|
||||
- The part is mainly a flange, washer, simple tube, or rectangular plate with unrelated holes.
|
||||
- The task requires an assembly of shaft, bearing, seals, fasteners, and housing together.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Coaxial shaft clearance bore through the body.
|
||||
- Larger bearing pocket or counterbore with controlled depth.
|
||||
- Shoulder, step, retaining lip, snap-ring groove, or cover seat to locate the bearing axially.
|
||||
- Mounting base, side lugs, flange, bolt holes, ribs, or bosses can appear, but they support the bearing axis rather than define the part family.
|
||||
- Chamfers at bore entrances and seat edges guide assembly.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Bearing seat diameter, shaft clearance diameter, housing outside diameter, and cover features share one axis.
|
||||
- Separate the bearing outer-diameter seat from the shaft clearance bore; they are not the same feature.
|
||||
- Leave a shoulder or defined depth when the bearing must stop axially.
|
||||
- Build enough housing material before cutting the bearing pocket.
|
||||
- Add mounting holes and ribs after the main bore/seat relationship is established.
|
||||
- Do not model balls or rollers unless the prompt explicitly asks for a bearing assembly.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `bearing_outer_diameter` defines the pocket or seat diameter.
|
||||
- `shaft_clearance_diameter` defines the through bore.
|
||||
- `seat_depth` defines how far the bearing is inserted.
|
||||
- `shoulder_height` or `shoulder_diameter` defines axial retention.
|
||||
- `housing_wall_thickness` controls material around the bearing pocket.
|
||||
- `mounting_hole_pattern`, `base_length`, `base_width`, and `base_thickness` define attachment to the frame.
|
||||
- `rib_thickness` and `rib_height` define optional support ribs.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the bearing axis and choose the housing form: block, flanged seat, sleeve, pedestal, or bracketed support.
|
||||
2. Create the main housing/body with enough wall thickness around the bearing axis.
|
||||
3. Cut the through shaft clearance bore on the main axis.
|
||||
4. Cut the bearing pocket/counterbore to its seat depth, leaving a clear shoulder where needed.
|
||||
5. Add retaining grooves, cover recesses, set-screw holes, oil holes, or alignment features only if requested.
|
||||
6. Add base mounting holes, bolt pads, ribs, and bosses using datum-driven positions.
|
||||
7. Finish with bore lead-in chamfers, outside chamfers, and small fillets.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Generating a bearing assembly instead of a housing or seat part.
|
||||
- Making the bearing seat and shaft clearance the same diameter.
|
||||
- Missing the shoulder/depth that locates the bearing.
|
||||
- Off-axis mounting of bore, pocket, or outer sleeve.
|
||||
- Turning every bearing support into a flange even when the evidence suggests a block or pedestal.
|
||||
- Adding decorative ribs or fasteners that obscure the bearing pocket.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 24 previously reviewed SCAD58 bearing housing/seat descriptions.
|
||||
- Latest run evidence: accepted insights for central bore alignment, bearing/bushing-like cylindrical seats, coaxial features, counterbores, mounting holes, and chamfered bore entries.
|
||||
- Frequent operations in the latest run: Extrusion, Revolution, Cut, RevCut, HoleWzd, Chamfer, Fillet.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `bearing housing`
|
||||
- `bearing seat`
|
||||
- `bearing support`
|
||||
- `bearing block`
|
||||
- `bushing seat`
|
||||
|
||||
Secondary triggers:
|
||||
- `bearing bore`
|
||||
- `bearing pocket`
|
||||
- `shaft clearance`
|
||||
- `retaining shoulder`
|
||||
- `coaxial seat`
|
||||
- `pillow block`
|
||||
|
||||
Operation triggers:
|
||||
- `cut coaxial bore`
|
||||
- `counterbore bearing seat`
|
||||
- `create shoulder`
|
||||
- `add mounting base holes`
|
||||
- `chamfer bore entry`
|
||||
|
||||
Exclusions:
|
||||
- `complete rolling bearing`
|
||||
- `plain washer`
|
||||
- `flange only`
|
||||
- `simple tube`
|
||||
- `shaft only`
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# Flange Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a circular flange, coupling flange, pipe flange, gasket-like flange, flanged sleeve, or flanged cylindrical adapter.
|
||||
- The part has a central axis, central bore, and fastener holes arranged around a pitch circle.
|
||||
- The flange connects, spaces, seals, or mounts coaxial parts such as shafts, pipes, bearings, covers, or couplings.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The word flange only means a side tab, flat ear, or rectangular mounting plate extension.
|
||||
- The hole layout is a rectangular grid, edge row, or arbitrary plate pattern.
|
||||
- The model is primarily a plain shaft, washer, pulley, bearing assembly, or bracket without a true circular flange body.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Axisymmetric disc, annular ring, raised hub, sleeve, stepped boss, or cylindrical landing surface.
|
||||
- Central through bore, pilot bore, counterbore, recess, seal land, or shaft clearance on the main axis.
|
||||
- Bolt holes are evenly spaced by angle around the same center as the bore.
|
||||
- Optional recesses, gasket grooves, chamfers, fillets, or counterbores are secondary to the coaxial flange body.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- The outer diameter, bore, hub, sleeve, recesses, and bolt circle share one central construction axis.
|
||||
- Build the rotational body before cutting the bolt holes.
|
||||
- Define bolt-circle diameter separately from outer diameter and bore diameter.
|
||||
- Create one correct bolt hole, then circular-pattern the cut feature over 360 degrees.
|
||||
- Do not replace the bolt circle with a rectangular hole grid.
|
||||
- Keep fillets, chamfers, and cosmetic edge breaks late in the sequence.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `outer_diameter` defines the flange envelope.
|
||||
- `thickness` defines the disc or flange plate depth.
|
||||
- `bore_diameter` defines shaft or pipe clearance.
|
||||
- `hub_diameter`, `hub_height`, and `pilot_diameter` define raised coaxial features.
|
||||
- `bolt_circle_diameter`, `bolt_count`, `bolt_hole_diameter`, and `start_angle` define the fastener pattern.
|
||||
- `counterbore_diameter`, `counterbore_depth`, `gasket_groove_width`, and `gasket_groove_depth` define secondary seats.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Establish the central axis and primary reference plane.
|
||||
2. Create the flange disc and any coaxial hub or sleeve using revolve for stepped profiles, or extrude circles for simple flat discs.
|
||||
3. Cut the central bore through the flange and hub on the same axis.
|
||||
4. Add one bolt hole at the pitch radius using the correct fastener hole type.
|
||||
5. Circular-pattern that cut around the central axis with equal angular spacing.
|
||||
6. Add recesses, gasket grooves, counterbores, chamfers, and fillets after the bore and bolt pattern are stable.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Off-axis bore relative to hub or outer diameter.
|
||||
- Manual bolt hole placement that produces unequal angular spacing.
|
||||
- Confusing a rectangular mounting plate with a flange because both have multiple holes.
|
||||
- Forgetting the central bore and creating only a circular plate with holes.
|
||||
- Applying large fillets before the bolt pattern and causing feature failure.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 45 previously reviewed SCAD58 flange-like models.
|
||||
- Latest run evidence: accepted insights for circular flange bolt holes, circular pattern around a central bore, revolved axisymmetric bodies, and coaxial cylindrical features.
|
||||
- Frequent operations in the latest run: Revolution, Extrusion, Cut, HoleWzd, CircularPattern, Chamfer, Fillet.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `flange`
|
||||
- `circular flange`
|
||||
- `pipe flange`
|
||||
- `coupling flange`
|
||||
- `flanged sleeve`
|
||||
|
||||
Secondary triggers:
|
||||
- `bolt circle`
|
||||
- `pitch circle`
|
||||
- `central bore`
|
||||
- `raised hub`
|
||||
- `gasket groove`
|
||||
- `pilot diameter`
|
||||
|
||||
Operation triggers:
|
||||
- `revolve profile`
|
||||
- `extrude circular disc`
|
||||
- `cut central bore`
|
||||
- `circular pattern`
|
||||
- `counterbore bolt holes`
|
||||
|
||||
Exclusions:
|
||||
- `mounting plate`
|
||||
- `rectangular hole grid`
|
||||
- `side tab`
|
||||
- `plain shaft`
|
||||
- `complete bearing`
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# Hexagonal Nut Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a hex nut, hexagonal nut, machined nut, nut-like fastener, or hexagonal threaded insert.
|
||||
- The key features are wrench flats, a central threaded bore, and chamfered faces/edges.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The part is a bolt, screw, socket head cap screw, threaded rod, wing nut, cap nut, or flange nut unless the prompt explicitly asks for that variant.
|
||||
- The hexagon is only a decorative grip on another part.
|
||||
- The threaded feature is external rather than an internal bore.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Six flat wrench faces or hexagonal prism envelope.
|
||||
- Central internal threaded hole coaxial with the nut thickness.
|
||||
- Chamfers on top and bottom openings and on hex edges.
|
||||
- Optional counterbore, lead-in chamfer, or relieved thread entry.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Keep the threaded bore centered through the hex body.
|
||||
- Preserve the hex across-flats size separately from thread size and thickness.
|
||||
- Model chamfers as functional lead-ins and edge breaks, not as random bevels.
|
||||
- Represent internal thread intent using a threaded hole, thread callout, or helical/revolved cut only when required by output format.
|
||||
- Do not generate the mating bolt unless requested.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `across_flats` defines wrench size.
|
||||
- `thickness` defines nut height.
|
||||
- `thread_size`, `thread_diameter`, and `thread_depth` define the central bore.
|
||||
- `bore_diameter` defines the pre-thread or clearance hole if thread geometry is abstracted.
|
||||
- `face_chamfer` and `edge_chamfer` define lead-in and deburring.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Create the hexagonal body as a hex extrusion or by cutting flats from a revolved/chamfered blank.
|
||||
2. Define the central axis through the nut.
|
||||
3. Create the central bore or threaded hole through the body.
|
||||
4. Add thread representation or thread callout if the CAD output supports it.
|
||||
5. Add top/bottom lead-in chamfers and small edge chamfers.
|
||||
6. Verify the hex flats remain planar and the bore remains coaxial.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Producing a bolt or screw instead of a nut.
|
||||
- Forgetting the central threaded bore.
|
||||
- Placing the bore off-center relative to the hex body.
|
||||
- Making the chamfers so large that wrench flats disappear.
|
||||
- Treating a flange nut, cap nut, or wing nut as a plain hex nut.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 25 accepted planning insights matched hex nut or nut-like threaded fastener descriptions.
|
||||
- Strong repeated names include `plan_hex_nut`, `revolved_hex_nut_with_internal_thread`, `hex_nut_construction`, and `plan_hexagonal_nut_with_threaded_bore`.
|
||||
- Frequent operations in the latest run: Revolution, Extrusion, Cut, RevCut, HoleWzd, Chamfer.
|
||||
- Source model examples: 009934, 010137, 052867, 066148, 067249, 135604, 138372, 150622, 200147, 204805, 222550, 225767, 231397, 238840.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `hex nut`
|
||||
- `hexagonal nut`
|
||||
- `nut`
|
||||
- `threaded nut`
|
||||
- `hexagonal threaded insert`
|
||||
|
||||
Secondary triggers:
|
||||
- `internal thread`
|
||||
- `central threaded bore`
|
||||
- `wrench flats`
|
||||
- `across flats`
|
||||
- `chamfered nut`
|
||||
|
||||
Operation triggers:
|
||||
- `extrude hexagon`
|
||||
- `cut central bore`
|
||||
- `hole wizard threaded hole`
|
||||
- `revolved cut thread`
|
||||
- `chamfer faces`
|
||||
|
||||
Exclusions:
|
||||
- `bolt`
|
||||
- `screw`
|
||||
- `threaded shaft`
|
||||
- `wing nut`
|
||||
- `cap nut`
|
||||
- `flange nut`
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Mounting Bracket Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a mounting bracket, L bracket, U bracket, T bracket, support bracket, clamp bracket, lugged bracket, or machined support with holes.
|
||||
- The part has a load-bearing base plus one or more raised webs, tabs, arms, bosses, lugs, pockets, slots, or cutouts.
|
||||
- The model is a single machined bracket component, not an assembly with screws or attached hardware.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The part is only a flat plate with holes and no bracket-like 3D support structure.
|
||||
- The part is mainly a shaft, flange, bearing housing, complete hinge assembly, or sheet-metal bent bracket.
|
||||
- The prompt asks for a decorative stand or enclosure rather than a functional machined support.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Base block or plate that mounts to another part.
|
||||
- Vertical web, side wall, lug, tab, raised boss, or protruding arm.
|
||||
- Mounting holes, threaded holes, counterbores, slots, or cutouts.
|
||||
- Reinforcing ribs or fillets at transitions may appear when strength matters.
|
||||
- Hole placement is usually datum-driven and often symmetric or mirrored.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Build the main load path first: base, web/tab/lug, and boss geometry before small holes.
|
||||
- Keep bracket features as one coherent body unless the prompt explicitly asks for assembled parts.
|
||||
- Place holes from functional datums, not visual guesses.
|
||||
- Use standard hole features for threaded, counterbored, countersunk, and clearance holes.
|
||||
- Add cutouts/slots for clearance or adjustment only where they make functional sense.
|
||||
- Apply fillets/chamfers late, especially at web-to-base transitions and exposed edges.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `base_length`, `base_width`, and `base_thickness` define the mounting base.
|
||||
- `web_height`, `web_thickness`, `tab_width`, `lug_radius`, and `boss_diameter` define support geometry.
|
||||
- `hole_diameter`, `thread_size`, `counterbore_diameter`, and `slot_length` define fastening and adjustment.
|
||||
- `edge_margin`, `hole_pitch`, `symmetry_plane`, and `boss_offset` define layout.
|
||||
- `fillet_radius` controls stress relief at transitions.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Create the base extrusion or main bracket profile.
|
||||
2. Add vertical webs, tabs, lugs, bosses, or arms as secondary extrusions from clear datum faces.
|
||||
3. Cut clearance pockets, U/L/T-shaped openings, slots, or lightening cutouts.
|
||||
4. Add standard holes using HoleWzd or cut features, positioned from datums and symmetry references.
|
||||
5. Pattern or mirror repeated holes/features when the design is symmetric.
|
||||
6. Add structural fillets at web/base intersections, then finish with small chamfers on exposed edges.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Collapsing a bracket into a simple flat plate and losing the vertical/web structure.
|
||||
- Creating an assembly of screws, pins, or attached parts when only the bracket is requested.
|
||||
- Adding holes before the base/web geometry and producing floating or misaligned holes.
|
||||
- Mixing several unrelated bracket variants into one overloaded model.
|
||||
- Using decorative cutouts that weaken the load path or overlap holes.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 247 accepted planning insights matched bracket-like descriptions and operations.
|
||||
- Strong repeated names include `plan_l_bracket_with_holes`, `bracket_with_holes_and_cutouts`, `bracket_with_holes_planning`, `mounting_bracket_with_holes`, and `plan_bracket_with_holes`.
|
||||
- Frequent operations in the latest run: Extrusion, Cut, HoleWzd, LinearPattern, MirrorPattern, Chamfer, Fillet, Boss.
|
||||
- Source model examples: 000780, 001607, 002620, 004421, 009414, 028735, 032081, 036242, 041396, 047048, 054507, 079425, 096301, 134801, 187546, 195869, 207737, 225658.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `mounting bracket`
|
||||
- `support bracket`
|
||||
- `L bracket`
|
||||
- `U bracket`
|
||||
- `T bracket`
|
||||
- `bracket with holes`
|
||||
|
||||
Secondary triggers:
|
||||
- `web`
|
||||
- `lug`
|
||||
- `tab`
|
||||
- `boss`
|
||||
- `slot`
|
||||
- `cutout`
|
||||
- `threaded holes`
|
||||
|
||||
Operation triggers:
|
||||
- `extrude base`
|
||||
- `extrude tab`
|
||||
- `cut slot`
|
||||
- `hole wizard`
|
||||
- `mirror pattern`
|
||||
- `fillet web transition`
|
||||
|
||||
Exclusions:
|
||||
- `flat mounting plate only`
|
||||
- `flange`
|
||||
- `shaft`
|
||||
- `bearing assembly`
|
||||
- `sheet metal bend`
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
# Mounting Plate Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a flat mounting plate, base plate, adapter plate, panel, fixture plate, or spacer plate.
|
||||
- The dominant body is a prismatic plate with functional holes, slots, counterbores, countersinks, threaded holes, pockets, or a central clearance cutout.
|
||||
- The plate locates, fastens, spaces, or supports other components from planar datum faces.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The primary geometry is an axisymmetric flange with a circular bolt pitch pattern.
|
||||
- The plate is only a secondary base under a bearing housing, bracket, motor body, or cylindrical module.
|
||||
- The part is a true 3D bracket with vertical webs, lugs, arms, or side walls that control the design.
|
||||
- The request is for sheet-metal bends, formed panels, or decorative flat art rather than a machined plate.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Rectangular, square, rounded-rectangle, or simple profile plate with mostly uniform thickness.
|
||||
- Holes are located from datum edges, centerlines, or symmetric offsets instead of arbitrary visual placement.
|
||||
- Common hole types include clearance holes, threaded holes, counterbored holes, countersunk holes, dowel holes, and through holes.
|
||||
- Cutouts can be central circular holes, rectangular windows, slots, pockets, or weight-reduction openings.
|
||||
- Edge chamfers and small fillets are usually finishing operations, not the defining shape.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Establish the base plate envelope, thickness, top face, bottom face, and edge datum references before adding holes.
|
||||
- Keep mounting holes fully constrained by edge margins, centerlines, pitch, symmetry, or functional datums.
|
||||
- Model holes as real cuts or standard hole features; do not leave sketch circles on the face.
|
||||
- Use one seed hole or hole group plus pattern/mirror when the layout is regular.
|
||||
- Choose hole type from function: clearance for bolts, threaded/tapped for fastening into the plate, counterbore/countersink for flush screw heads, slot for adjustment.
|
||||
- Apply chamfers and fillets after functional holes and cutouts so edge treatment does not distort hole geometry.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `length`, `width`, and `thickness` define the blank.
|
||||
- `corner_radius` or `corner_chamfer` controls perimeter treatment when requested.
|
||||
- `edge_margin_x`, `edge_margin_y`, `pitch_x`, and `pitch_y` locate rectangular hole groups.
|
||||
- `central_cutout_width`, `central_cutout_length`, or `central_bore_diameter` define clearance openings.
|
||||
- `hole_diameter`, `thread_size`, `counterbore_diameter`, `counterbore_depth`, `countersink_angle`, and `slot_length` define fastener features.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Create a constrained base sketch for the plate outline and extrude it to thickness.
|
||||
2. Define datum centerlines, edge offsets, and symmetry references on the main face.
|
||||
3. Add the central clearance hole, rectangular cutout, or pocket if it controls component clearance.
|
||||
4. Add the first functional hole using the correct hole type and end condition.
|
||||
5. Replicate regular hole groups with linear pattern, circular pattern, or mirror according to layout intent.
|
||||
6. Add slots, secondary pockets, bosses, or local relief cuts only after primary mounting references are stable.
|
||||
7. Finish with perimeter chamfers, small fillets, and deburring details.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Treating every round mark as the same generic hole and losing counterbore/thread/countersink meaning.
|
||||
- Placing holes by eyeballing coordinates instead of datum margins and symmetry.
|
||||
- Using a flange bolt-circle pattern for a rectangular plate grid.
|
||||
- Adding chamfers before holes, causing holes or counterbores to be clipped.
|
||||
- Over-modeling decorative bevels while missing the functional fastening layout.
|
||||
- Combining unrelated bracket walls or cylindrical housings into the mounting plate body.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Current library evidence: 207 previously reviewed SCAD58 mounting/base plate descriptions.
|
||||
- Latest run evidence: repeated accepted insights for `mounting_plate_with_holes`, `plan_mounting_plate_with_holes`, `extrude_rectangular_plate`, `base_plate_with_mounting_holes`, central cutouts, threaded holes, counterbored holes, linear patterns, and edge chamfers.
|
||||
- Frequent operations in the latest run: Extrusion, HoleWzd, Cut, LinearPattern, MirrorPattern, Chamfer, Fillet.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `mounting plate`
|
||||
- `base plate`
|
||||
- `adapter plate`
|
||||
- `fixture plate`
|
||||
- `flat plate with holes`
|
||||
|
||||
Secondary triggers:
|
||||
- `hole grid`
|
||||
- `edge margins`
|
||||
- `counterbored holes`
|
||||
- `threaded holes`
|
||||
- `central cutout`
|
||||
- `slots`
|
||||
|
||||
Operation triggers:
|
||||
- `extrude base plate`
|
||||
- `hole wizard`
|
||||
- `linear pattern`
|
||||
- `mirror holes`
|
||||
- `cut central opening`
|
||||
- `chamfer edges last`
|
||||
|
||||
Exclusions:
|
||||
- `flange bolt circle`
|
||||
- `bearing housing`
|
||||
- `shaft`
|
||||
- `3d bracket`
|
||||
- `sheet metal bend`
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# Simple Shaft Or Cylindrical Rod Planning Skill
|
||||
|
||||
## When to use
|
||||
|
||||
- The request describes a single cylindrical rod, simple shaft, spacer rod, cylindrical pin, standoff-like cylinder, or shaft-like machined part.
|
||||
- The body is primarily a cylinder along one axis, optionally with end holes, chamfers, simple steps, flats, slots, or threaded holes.
|
||||
- The task is to generate one part, not a spindle cartridge or shaft assembly.
|
||||
|
||||
## Do not use when
|
||||
|
||||
- The request describes a spindle module, motor rotor assembly, gearbox shaft assembly, bearings, seals, pulleys, keys, and fasteners together.
|
||||
- The part is mainly a flange, washer, bushing, screw, bolt, or complex housing.
|
||||
- The body is a hollow tube or bearing sleeve where inner/outer diameter relationship is the main design intent.
|
||||
|
||||
## Recognition features
|
||||
|
||||
- Long cylindrical body with uniform or stepped diameters.
|
||||
- Central axis is the dominant datum.
|
||||
- Optional end chamfers, end fillets, centered threaded holes, transverse holes, flats, slots, grooves, or keyway-like cuts.
|
||||
- Turning and cylindrical surfaces dominate; milling may appear for flats, slots, or keyways.
|
||||
|
||||
## Core invariants
|
||||
|
||||
- Establish one main shaft axis and overall length first.
|
||||
- Use extrude for a uniform simple cylinder; use revolve for stepped diameters, grooves, shoulders, tapers, or turned profiles.
|
||||
- Keep all coaxial diameter changes, end holes, and grooves aligned to the shaft axis.
|
||||
- Treat holes, slots, flats, keyways, and threads as secondary features.
|
||||
- Do not invent bearings, couplings, gears, or housings unless requested.
|
||||
|
||||
## Parameter roles
|
||||
|
||||
- `length` defines the axial span.
|
||||
- `diameter` defines a simple rod.
|
||||
- `step_diameters`, `step_lengths`, `shoulder_positions`, and `groove_width` define turned features.
|
||||
- `end_hole_diameter`, `thread_size`, and `thread_depth` define centered end holes.
|
||||
- `flat_width`, `slot_width`, `slot_depth`, and `keyway_length` define milled secondary features.
|
||||
- `chamfer_distance` and `fillet_radius` define edge finishing.
|
||||
|
||||
## Construction sequence
|
||||
|
||||
1. Define the shaft axis and overall length.
|
||||
2. For a simple rod, sketch a circle and extrude along the axis.
|
||||
3. For stepped or shouldered shafts, sketch the half-profile and revolve it around the axis.
|
||||
4. Add centered end holes, threaded holes, grooves, flats, slots, or keyways as secondary cuts.
|
||||
5. Keep secondary features referenced to the shaft axis or end-face datums.
|
||||
6. Finish with end chamfers, shoulder fillets, and small edge breaks.
|
||||
|
||||
## Common failures
|
||||
|
||||
- Generating a spindle or bearing assembly when only a shaft/rod part was requested.
|
||||
- Creating off-axis end holes.
|
||||
- Using multiple separate bodies for diameter steps instead of one coherent solid.
|
||||
- Treating a threaded shaft, bolt, or screw as the same as a plain shaft without preserving thread/head differences.
|
||||
- Adding arbitrary decorative grooves that were not requested.
|
||||
|
||||
## Evidence summary
|
||||
|
||||
- Latest run evidence: 107 accepted planning insights matched shaft, rod, cylinder, and stepped-shaft construction.
|
||||
- Strong repeated names include `plan_stepped_shaft`, `simple_cylinder_extrusion`, `stepped_shaft_construction`, `cylindrical_rod_planning`, and `plan_revolve_shaft_profile`.
|
||||
- Frequent operations in the latest run: Extrusion, Revolution, Cut, RevCut, HoleWzd, Chamfer, Fillet.
|
||||
- Source model examples: 000166, 001957, 040325, 069352, 080683, 100698, 112997, 144199, 148300, 160726, 172872, 210853, 218803, 225038, 232675, 239358.
|
||||
- Source: CAD-SkillX offline review of SCAD58 descriptions, STEP summaries, and SolidWorks feature evidence.
|
||||
|
||||
## Retrieval metadata
|
||||
|
||||
Triggers:
|
||||
- `cylindrical rod`
|
||||
- `simple shaft`
|
||||
- `shaft`
|
||||
- `rod`
|
||||
- `pin`
|
||||
- `standoff`
|
||||
|
||||
Secondary triggers:
|
||||
- `stepped shaft`
|
||||
- `end hole`
|
||||
- `threaded end`
|
||||
- `shaft groove`
|
||||
- `keyway`
|
||||
- `chamfered ends`
|
||||
|
||||
Operation triggers:
|
||||
- `extrude circle`
|
||||
- `revolve shaft profile`
|
||||
- `cut end hole`
|
||||
- `cut slot`
|
||||
- `chamfer ends`
|
||||
|
||||
Exclusions:
|
||||
- `spindle assembly`
|
||||
- `bearing assembly`
|
||||
- `flange`
|
||||
- `washer`
|
||||
- `bolt head`
|
||||
- `gear`
|
||||
@@ -127,6 +127,30 @@ async def read_task(task_id: str) -> JSONResponse:
|
||||
return JSONResponse(task)
|
||||
|
||||
|
||||
@app.get("/v1/tasks/{task_id}/design-intent")
|
||||
async def read_current_design_intent(task_id: str) -> JSONResponse:
|
||||
try:
|
||||
safe_id = safe_task_id(task_id)
|
||||
result = store.read_design_intent(safe_id)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="No DesignIntent exists for this task")
|
||||
return JSONResponse({"task_id": safe_id, "intent_id": result["record"]["intent_id"], **result})
|
||||
|
||||
|
||||
@app.get("/v1/tasks/{task_id}/design-intents/{intent_id}")
|
||||
async def read_design_intent(task_id: str, intent_id: str) -> JSONResponse:
|
||||
try:
|
||||
safe_id = safe_task_id(task_id)
|
||||
result = store.read_design_intent(safe_id, intent_id)
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="DesignIntent not found")
|
||||
return JSONResponse({"task_id": safe_id, "intent_id": result["record"]["intent_id"], **result})
|
||||
|
||||
|
||||
@app.get("/v1/tasks/{task_id}/artifacts/{artifact_path:path}")
|
||||
async def read_artifact(task_id: str, artifact_path: str) -> StreamingResponse:
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -165,6 +189,10 @@ async def update_parameters(task_id: str, payload: ParameterUpdate) -> JSONRespo
|
||||
safe_id = safe_task_id(task_id)
|
||||
task = store.read_task(safe_id)
|
||||
current_revision_id = str((task or {}).get("current_revision") or "")
|
||||
current_revision = next(
|
||||
(item for item in (task or {}).get("revisions", []) if item.get("revision_id") == current_revision_id),
|
||||
{},
|
||||
)
|
||||
current_path = store.current_cdsl_path(safe_id)
|
||||
if not task or not current_path or not current_revision_id:
|
||||
raise ValueError("Task has no successful CDSL revision")
|
||||
@@ -179,6 +207,11 @@ async def update_parameters(task_id: str, payload: ParameterUpdate) -> JSONRespo
|
||||
summary="Updated CDSL parameters",
|
||||
parent_revision_id=current_revision_id,
|
||||
operation={"type": "parameter_update", "values": payload.values},
|
||||
part_skills=None,
|
||||
generation_assumptions=[],
|
||||
design_intent_id=str(current_revision.get("design_intent_id") or ""),
|
||||
design_intent_path=str(current_revision.get("design_intent_path") or ""),
|
||||
design_intent_status="accepted" if current_revision.get("design_intent_id") else "",
|
||||
)
|
||||
return JSONResponse(result)
|
||||
except ValueError as error:
|
||||
|
||||
@@ -57,6 +57,8 @@ class CadResult(BaseModel):
|
||||
parameters_path: str | None = None
|
||||
selector_path: str | None = None
|
||||
edges_path: str | None = None
|
||||
design_intent_id: str | None = None
|
||||
design_intent_path: str | None = None
|
||||
summary: str
|
||||
reference_ids: list[str] = Field(default_factory=list)
|
||||
engine: str = "cdsl_only"
|
||||
|
||||
@@ -15,6 +15,7 @@ import httpx
|
||||
from app.models.contracts import ChatMessage
|
||||
from app.services.engine_service import build_revision, load_engine, validate_cdsl
|
||||
from app.services.library import CdslLibrary
|
||||
from app.services.part_skills import PartSkillLibrary
|
||||
from app.services.sse import event
|
||||
from app.services.storage import WorkspaceStore, now_iso
|
||||
from app.settings import ProviderConfig, ProviderModel, Settings
|
||||
@@ -141,12 +142,25 @@ def invalid_cdsl_result(error: ValueError) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def invalid_design_intent_result(error: Exception) -> dict[str, Any]:
|
||||
code = str(getattr(error, "code", "INVALID_DESIGN_INTENT"))
|
||||
return {
|
||||
"ok": False,
|
||||
"code": code,
|
||||
"message": f"The DesignIntent plan was rejected: {error}. Correct the complete plan before generating CDSL.",
|
||||
}
|
||||
|
||||
|
||||
def user_visible_tool_message(result: dict[str, Any], user_text: str) -> str:
|
||||
code = str(result.get("code") or "")
|
||||
if code == "INVALID_CDSL":
|
||||
if any("\u4e00" <= char <= "\u9fff" for char in str(user_text or "")):
|
||||
return "CDSL 不符合 engine 的模型契约,正在请求模型按 schema 修正后重新生成。"
|
||||
return "The CDSL model does not match the engine contract. Asking the model to correct it and retry."
|
||||
if code in {"INVALID_DESIGN_INTENT", "INTENT_CDSL_MISMATCH", "DESIGN_INTENT_REQUIRED", "DESIGN_INTENT_BLOCKED"}:
|
||||
if any("\u4e00" <= char <= "\u9fff" for char in str(user_text or "")):
|
||||
return "设计意图尚未通过校验,未进入 CAD 构建。"
|
||||
return "The design intent has not passed validation, so CAD construction has not started."
|
||||
return str(result.get("message") or result.get("summary") or "")
|
||||
|
||||
|
||||
@@ -187,6 +201,22 @@ def _cdsl_tool_schema() -> dict[str, Any]:
|
||||
CDSL_TOOL_SCHEMA = _cdsl_tool_schema()
|
||||
|
||||
|
||||
def _design_intent_tool_schema() -> dict[str, Any]:
|
||||
engine_dir = Path(__file__).resolve().parents[2] / "engine" / "cdsl_engine"
|
||||
try:
|
||||
schema = json.loads((engine_dir / "design_intent_schema.json").read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError("Local DesignIntent JSON Schema is unavailable or invalid") from error
|
||||
# The model cannot forge storage/audit fields. They are added only after
|
||||
# backend validation by WorkspaceStore.create_design_intent().
|
||||
for field in ("intent_id", "created_at", "part_skill_ids", "part_skill_selection"):
|
||||
schema.get("properties", {}).pop(field, None)
|
||||
return schema
|
||||
|
||||
|
||||
DESIGN_INTENT_TOOL_SCHEMA = _design_intent_tool_schema()
|
||||
|
||||
|
||||
TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
{
|
||||
"type": "function",
|
||||
@@ -197,6 +227,7 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string"}, "limit": {"type": "integer", "minimum": 1, "maximum": 8}},
|
||||
"required": ["query"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -205,7 +236,24 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"function": {
|
||||
"name": "read_cdsl_reference",
|
||||
"description": "Read one official CDSL sample by part_id. Use this before creating geometry based on a reference.",
|
||||
"parameters": {"type": "object", "properties": {"part_id": {"type": "string"}}, "required": ["part_id"]},
|
||||
"parameters": {"type": "object", "properties": {"part_id": {"type": "string"}}, "required": ["part_id"], "additionalProperties": False},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "propose_design_intent",
|
||||
"description": "Submit a complete semantic DesignIntent plan before searching CDSL references or generating CDSL.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"intent": DESIGN_INTENT_TOOL_SCHEMA,
|
||||
"summary": {"type": "string", "minLength": 1},
|
||||
"assumptions": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["intent", "summary", "assumptions"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -224,11 +272,12 @@ TOOL_SCHEMAS: list[dict[str, Any]] = [
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"design_intent_id": {"type": "string", "pattern": "^intent_[a-z0-9]{12}$"},
|
||||
"cdsl": CDSL_TOOL_SCHEMA,
|
||||
"summary": {"type": "string", "minLength": 1},
|
||||
"assumptions": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
"required": ["cdsl", "summary"],
|
||||
"required": ["design_intent_id", "cdsl", "summary", "assumptions"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
@@ -242,13 +291,11 @@ def tools_for_model(model: ProviderModel) -> list[dict[str, Any]]:
|
||||
if not model.strict_tool_schema:
|
||||
return tools
|
||||
|
||||
generate_tool = next(
|
||||
tool for tool in tools
|
||||
if tool.get("function", {}).get("name") == "generate_cdsl_model"
|
||||
)
|
||||
# This flag constrains function arguments only. It has no effect on normal
|
||||
# assistant text, the user's prompt, or the natural-language summary.
|
||||
generate_tool["function"]["strict"] = True
|
||||
for tool in tools:
|
||||
if tool.get("function", {}).get("name") in {"propose_design_intent", "generate_cdsl_model"}:
|
||||
# This flag constrains function arguments only. It has no effect on
|
||||
# normal assistant text, the user's prompt, or the summary.
|
||||
tool["function"]["strict"] = True
|
||||
return tools
|
||||
|
||||
|
||||
@@ -352,9 +399,12 @@ def system_prompt(
|
||||
user_text: str,
|
||||
viewer_context: list[dict[str, Any]] | None = None,
|
||||
task_id: str = "",
|
||||
part_skill_context: str = "",
|
||||
) -> str:
|
||||
skill_path = settings.engine_root.parent.parent / "agent" / "skills" / "cad-engine" / "SKILL.md"
|
||||
skill = skill_path.read_text(encoding="utf-8") if skill_path.is_file() else ""
|
||||
planning_recipe_path = skill_path.with_name("planning-recipe.md")
|
||||
planning_recipe = planning_recipe_path.read_text(encoding="utf-8") if planning_recipe_path.is_file() else ""
|
||||
readme_path = settings.engine_root / "README.md"
|
||||
engine_readme = readme_path.read_text(encoding="utf-8") if readme_path.is_file() else ""
|
||||
profile_schema_path = settings.engine_root / "profile_schema.json"
|
||||
@@ -379,6 +429,12 @@ Tool call contract:
|
||||
- Do not append prose, Markdown code fences, comments, or a second JSON value.
|
||||
- For generate_cdsl_model, pass the complete CDSL as the cdsl object directly,
|
||||
not as Markdown and not as a concatenated JSON string.
|
||||
- Call propose_design_intent first. Its `intent` is the complete semantic
|
||||
planning JSON, without sketch coordinates, raw CAD code, storage IDs, or
|
||||
part-skill IDs. The backend chooses and persists part skills itself.
|
||||
- Do not call search_cdsl_library, read_cdsl_reference, or
|
||||
generate_cdsl_model until propose_design_intent returns an accepted
|
||||
`intent_id`. Pass that exact ID to generate_cdsl_model.
|
||||
- The generate_cdsl_model `cdsl` parameter is the complete machine-enforced
|
||||
schema. Satisfy its nested object and array types exactly; do not substitute
|
||||
a shorthand array for an object. For example, every hole position is
|
||||
@@ -399,12 +455,34 @@ Workflow limits:
|
||||
{response_language_instruction(user_text)}
|
||||
|
||||
You generate parameterized CDSL, never raw CAD source code. For new CAD requests:
|
||||
1. Search the local official CDSL library.
|
||||
2. Read at least one relevant reference when a match exists.
|
||||
3. Call generate_cdsl_model only when the request is sufficiently specified.
|
||||
4. Never claim success unless the tool returns a successful CDSL-only STEP and GLB artifact.
|
||||
1. Use the injected part-skill guidance, when present, only to establish the
|
||||
structural plan, feature dependency order, and parameter roles.
|
||||
2. Call propose_design_intent. A blocking question or capability gap must make
|
||||
the plan `needs_clarification`; then ask one concise user-facing question
|
||||
and do not call CDSL tools.
|
||||
3. Only after an accepted ready intent, search the local official CDSL library.
|
||||
Read at least one relevant reference when a match exists; samples provide
|
||||
schema-valid expressions, not higher-priority part intent.
|
||||
4. Generate a complete CDSL whose feature IDs, atomics, dependencies, profiles,
|
||||
and selector evidence exactly realize the accepted DesignIntent, then call
|
||||
generate_cdsl_model with its intent ID.
|
||||
5. Never claim success unless the tool returns a successful CDSL-only STEP and GLB artifact.
|
||||
|
||||
For a revision, call read_current_cdsl first and preserve unrelated features.
|
||||
Precedence is strict: explicit user request, then CDSL schema/runtime, then
|
||||
part-skill guidance, then CDSL-library examples. Part skills never authorize
|
||||
build123d source, an unknown atomic/profile, an invented selector, or a free-
|
||||
coordinate substitute for a capability the runtime cannot express. For an
|
||||
unsupported requested structure, ask one concise clarification question or
|
||||
state the blocker rather than fabricating geometry. If a part-family conflict
|
||||
is injected, preserve the current part unless the user explicitly requests a
|
||||
whole-part replacement. A primary-family conflict is a hard clarification
|
||||
stop: ask one concise question and do not call generate_cdsl_model until the
|
||||
user resolves it.
|
||||
|
||||
For a revision, call read_current_cdsl first and preserve unrelated features,
|
||||
then propose a revise DesignIntent with base_revision_id set to the current
|
||||
successful revision. Do not search references or generate CDSL before that plan
|
||||
is accepted.
|
||||
Do not output compiler_context, unknown_shape, complex_arc_shape, entities,
|
||||
contour_edges_mm, or contour_regions_mm. Use only self-contained named profiles
|
||||
and feature atomic IDs defined in the engine schema below. Read the engine
|
||||
@@ -415,6 +493,9 @@ dimensions or intent are missing. Ordinary explanations must not create CAD.
|
||||
Local skill:
|
||||
{skill}
|
||||
|
||||
DesignIntent planning recipe:
|
||||
{planning_recipe}
|
||||
|
||||
Local engine guide:
|
||||
{engine_readme}
|
||||
|
||||
@@ -423,15 +504,29 @@ Authoritative engine schema:
|
||||
|
||||
Supported named profile types:
|
||||
{supported_profiles}
|
||||
|
||||
Injected part-skill context:
|
||||
{part_skill_context or "No part-family skill guidance was selected for this request."}
|
||||
{viewer_selection_prompt(viewer_context, task_id)}
|
||||
"""
|
||||
|
||||
|
||||
def part_skill_root(settings: Settings) -> Path:
|
||||
return settings.engine_root.parent.parent / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
|
||||
|
||||
|
||||
class AgentService:
|
||||
def __init__(self, settings: Settings, store: WorkspaceStore, library: CdslLibrary) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
settings: Settings,
|
||||
store: WorkspaceStore,
|
||||
library: CdslLibrary,
|
||||
part_skill_library: PartSkillLibrary | None = None,
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
self.store = store
|
||||
self.library = library
|
||||
self.part_skill_library = part_skill_library or PartSkillLibrary(part_skill_root(settings))
|
||||
|
||||
async def stream(
|
||||
self,
|
||||
@@ -489,7 +584,29 @@ class AgentService:
|
||||
yield event("progress", {"step": "analyze_request", "label": "分析需求", "status": "running", "message": "正在整理当前会话和 CAD 需求。"})
|
||||
references: list[str] = []
|
||||
library_searches = 0
|
||||
model_messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt(self.settings, user_text, viewer_context, task_id)}]
|
||||
current_task = self.store.read_task(task_id) if task_id else None
|
||||
inherited_skill_ids = self.part_skill_library.inherited_from_task(current_task)
|
||||
part_skill_selection = self.part_skill_library.select(user_text, inherited_skill_ids)
|
||||
intent_state: dict[str, Any] = {
|
||||
"phase": "WAITING_FOR_INTENT",
|
||||
"design_intent_id": "",
|
||||
}
|
||||
yield event("progress", {
|
||||
"step": "select_part_skill",
|
||||
"label": "识别零件族",
|
||||
"status": "success",
|
||||
"message": "已完成零件族与辅助建模规则识别。",
|
||||
})
|
||||
model_messages: list[dict[str, Any]] = [{
|
||||
"role": "system",
|
||||
"content": system_prompt(
|
||||
self.settings,
|
||||
user_text,
|
||||
viewer_context,
|
||||
task_id,
|
||||
self.part_skill_library.render_context(part_skill_selection),
|
||||
),
|
||||
}]
|
||||
model_messages.extend(messages_for_model(messages))
|
||||
if attachment_message:
|
||||
model_messages.append({"role": "user", "content": attachment_message})
|
||||
@@ -591,14 +708,30 @@ class AgentService:
|
||||
"message": "Agent 正在调用本地 CAD 工具。",
|
||||
})
|
||||
try:
|
||||
result, generated = await self._run_tool(name, arguments, task_id, user_text, references)
|
||||
result, generated = await self._run_tool(
|
||||
name,
|
||||
arguments,
|
||||
task_id,
|
||||
user_text,
|
||||
references,
|
||||
part_skill_selection=part_skill_selection,
|
||||
intent_state=intent_state,
|
||||
)
|
||||
except (ValueError, RuntimeError) as error:
|
||||
if name == "generate_cdsl_model":
|
||||
code = str(getattr(error, "code", ""))
|
||||
if code in {"INVALID_DESIGN_INTENT", "INTENT_CDSL_MISMATCH", "DESIGN_INTENT_REQUIRED", "DESIGN_INTENT_BLOCKED"}:
|
||||
result = invalid_design_intent_result(error)
|
||||
generated = None
|
||||
if name in {"propose_design_intent", "generate_cdsl_model"} and code != "DESIGN_INTENT_BLOCKED":
|
||||
required_tool_name = name
|
||||
elif name == "generate_cdsl_model":
|
||||
result = invalid_cdsl_result(error)
|
||||
generated = None
|
||||
required_tool_name = name
|
||||
else:
|
||||
raise
|
||||
if result.get("task_id"):
|
||||
task_id = str(result["task_id"])
|
||||
if generated:
|
||||
task_id = generated["task_id"]
|
||||
model_messages.append({
|
||||
@@ -612,7 +745,14 @@ class AgentService:
|
||||
"status": "success" if result.get("ok", True) else "error",
|
||||
"message": user_visible_tool_message(result, user_text),
|
||||
})
|
||||
if name == "generate_cdsl_model" and result.get("ok"):
|
||||
if name == "propose_design_intent" and result.get("ok"):
|
||||
yield event("progress", {
|
||||
"step": "validate_design_intent",
|
||||
"label": "校验设计意图",
|
||||
"status": "success",
|
||||
"message": "设计意图已通过结构、依赖和能力边界校验。",
|
||||
})
|
||||
if name in {"propose_design_intent", "generate_cdsl_model"} and result.get("ok"):
|
||||
required_tool_name = None
|
||||
if generated:
|
||||
result_payload = {
|
||||
@@ -625,6 +765,8 @@ class AgentService:
|
||||
"parametersPath": generated.get("parameters_path"),
|
||||
"selectorPath": generated.get("selector_path"),
|
||||
"edgesPath": generated.get("edges_path"),
|
||||
"designIntentId": generated.get("design_intent_id"),
|
||||
"designIntentPath": generated.get("design_intent_path"),
|
||||
"summary": generated["summary"],
|
||||
"referenceIds": generated["reference_ids"],
|
||||
"engine": generated["engine"],
|
||||
@@ -632,6 +774,12 @@ class AgentService:
|
||||
successful_result = result_payload
|
||||
assistant_parts.append({"type": "data-cad-result", "data": result_payload})
|
||||
yield event("cad_result", result_payload)
|
||||
yield event("progress", {
|
||||
"step": "build_cad",
|
||||
"label": "构建 CAD",
|
||||
"status": "success",
|
||||
"message": "已通过 cdsl_only runtime 构建 STEP 和 GLB。",
|
||||
})
|
||||
if iteration == 7:
|
||||
error_payload = {"stage": "agent", "message": "Agent tool loop reached its safety limit."}
|
||||
assistant_parts.append({"type": "data-cad-error", "data": error_payload})
|
||||
@@ -749,15 +897,87 @@ class AgentService:
|
||||
raise RuntimeError(f"LLM request failed ({response.status_code}): {response.text[:800]}")
|
||||
return response.json()
|
||||
|
||||
async def _run_tool(self, name: str, arguments: dict[str, Any], task_id: str, request: str, references: list[str]) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
async def _run_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
task_id: str,
|
||||
request: str,
|
||||
references: list[str],
|
||||
*,
|
||||
part_skill_selection: dict[str, Any] | None = None,
|
||||
intent_state: dict[str, Any] | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
state = intent_state if intent_state is not None else {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
phase = str(state.get("phase") or "WAITING_FOR_INTENT")
|
||||
if name == "propose_design_intent":
|
||||
intent = arguments.get("intent")
|
||||
if not isinstance(intent, dict):
|
||||
raise ValueError("propose_design_intent requires an intent JSON object")
|
||||
if any(field in intent for field in ("intent_id", "created_at", "part_skill_ids", "part_skill_selection")):
|
||||
raise ValueError("DesignIntent audit fields are assigned only by the backend")
|
||||
summary = str(arguments.get("summary") or "").strip()
|
||||
assumptions = arguments.get("assumptions")
|
||||
if not summary or not isinstance(assumptions, list) or not all(isinstance(item, str) for item in assumptions):
|
||||
raise ValueError("propose_design_intent requires a summary and an array of string assumptions")
|
||||
selection = part_skill_selection or self.part_skill_library.select(request)
|
||||
if selection.get("conflict"):
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "DESIGN_INTENT_BLOCKED",
|
||||
"message": str(selection["conflict"].get("message") or "The current part family must be clarified before planning."),
|
||||
}, None
|
||||
engine = load_engine(self.settings)
|
||||
current_task = self.store.read_task(task_id) if task_id else None
|
||||
current_revision_id = str((current_task or {}).get("current_revision") or "")
|
||||
if current_revision_id and intent.get("mode") != "revise":
|
||||
raise engine.DesignIntentError("INVALID_DESIGN_INTENT", "A task with a successful revision requires a revise DesignIntent")
|
||||
if intent.get("mode") == "revise" and not current_revision_id:
|
||||
raise engine.DesignIntentError("INVALID_DESIGN_INTENT", "A revise DesignIntent requires a current successful revision")
|
||||
normalized = deepcopy(intent)
|
||||
if assumptions:
|
||||
normalized["assumptions"] = list(dict.fromkeys([
|
||||
*normalized.get("assumptions", []),
|
||||
*(item.strip() for item in assumptions if item.strip()),
|
||||
]))
|
||||
normalized = engine.validate_design_intent(normalized, engine, current_revision_id=current_revision_id)
|
||||
persisted = self.store.create_design_intent(task_id or None, request, normalized, selection)
|
||||
state["design_intent_id"] = persisted["intent_id"]
|
||||
if normalized["status"] != "ready":
|
||||
state["phase"] = "WAITING_FOR_INTENT"
|
||||
return {
|
||||
"ok": False,
|
||||
"code": "DESIGN_INTENT_BLOCKED",
|
||||
"task_id": persisted["task_id"],
|
||||
"design_intent_id": persisted["intent_id"],
|
||||
"status": normalized["status"],
|
||||
"intent": persisted["intent"],
|
||||
"message": "The DesignIntent is saved but blocked. Ask the user only about its blocking question or capability gap.",
|
||||
}, None
|
||||
state["phase"] = "INTENT_ACCEPTED"
|
||||
return {
|
||||
"ok": True,
|
||||
"task_id": persisted["task_id"],
|
||||
"design_intent_id": persisted["intent_id"],
|
||||
"design_intent_path": persisted["path"],
|
||||
"status": "accepted",
|
||||
"intent": persisted["intent"],
|
||||
"summary": summary,
|
||||
}, None
|
||||
if name == "search_cdsl_library":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before searching CDSL references."}, None
|
||||
results = self.library.search(str(arguments.get("query") or request), int(arguments.get("limit") or 5))
|
||||
state["phase"] = "LIBRARY_REFERENCE"
|
||||
return {"ok": True, "results": results}, None
|
||||
if name == "read_cdsl_reference":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before reading CDSL references."}, None
|
||||
part_id = str(arguments.get("part_id") or "")
|
||||
sample = self.library.read_sample(part_id)
|
||||
if part_id not in references:
|
||||
references.append(part_id)
|
||||
state["phase"] = "WAITING_FOR_CDSL"
|
||||
return {"ok": True, "part_id": part_id, "cdsl": sample}, None
|
||||
if name == "read_current_cdsl":
|
||||
if not task_id:
|
||||
@@ -767,6 +987,17 @@ class AgentService:
|
||||
return {"ok": False, "message": "The current task has no successful CDSL revision."}, None
|
||||
return {"ok": True, "task_id": task_id, "cdsl": json.loads(path.read_text(encoding="utf-8"))}, None
|
||||
if name == "generate_cdsl_model":
|
||||
if phase not in {"INTENT_ACCEPTED", "LIBRARY_REFERENCE", "WAITING_FOR_CDSL"}:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "Submit and receive an accepted DesignIntent before generating CDSL."}, None
|
||||
design_intent_id = str(arguments.get("design_intent_id") or "")
|
||||
if not design_intent_id or design_intent_id != str(state.get("design_intent_id") or "") or not task_id:
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "generate_cdsl_model must use the accepted DesignIntent ID for this task."}, None
|
||||
intent_record = self.store.read_design_intent(task_id, design_intent_id)
|
||||
if not intent_record or intent_record["record"].get("status") != "accepted":
|
||||
return {"ok": False, "code": "DESIGN_INTENT_REQUIRED", "message": "The requested DesignIntent is not accepted for this task."}, None
|
||||
intent = intent_record["intent"]
|
||||
if intent.get("status") != "ready":
|
||||
return {"ok": False, "code": "DESIGN_INTENT_BLOCKED", "message": "The DesignIntent is blocked and cannot be built."}, None
|
||||
cdsl = arguments.get("cdsl")
|
||||
if isinstance(cdsl, str):
|
||||
cdsl = json.loads(cdsl)
|
||||
@@ -775,19 +1006,47 @@ class AgentService:
|
||||
# Reject malformed model output before build_revision allocates a task
|
||||
# directory or revision. build_revision will assign the real task ID.
|
||||
preflight_cdsl = {**cdsl, "part_id": str(cdsl.get("part_id") or "agent_preflight")}
|
||||
validate_cdsl(preflight_cdsl, load_engine(self.settings))
|
||||
engine = load_engine(self.settings)
|
||||
current_path = self.store.current_cdsl_path(task_id)
|
||||
current_cdsl = json.loads(current_path.read_text(encoding="utf-8")) if current_path else None
|
||||
engine.validate_intent_cdsl(intent, preflight_cdsl, engine, current_cdsl=current_cdsl)
|
||||
validate_cdsl(preflight_cdsl, engine)
|
||||
summary = str(arguments.get("summary") or "CDSL CAD model")
|
||||
yieldable = await asyncio.to_thread(
|
||||
build_revision,
|
||||
settings=self.settings,
|
||||
store=self.store,
|
||||
task_id=task_id or None,
|
||||
request=request,
|
||||
cdsl=cdsl,
|
||||
reference_ids=list(references),
|
||||
summary=summary,
|
||||
)
|
||||
return {"ok": True, "summary": summary, "task_id": yieldable["task_id"], "revision_id": yieldable["revision_id"]}, yieldable
|
||||
raw_assumptions = arguments.get("assumptions") or []
|
||||
if not isinstance(raw_assumptions, list) or not all(isinstance(item, str) for item in raw_assumptions):
|
||||
raise ValueError("generate_cdsl_model assumptions must be an array of strings")
|
||||
assumptions = [item.strip() for item in raw_assumptions if item.strip()]
|
||||
selection = part_skill_selection or self.part_skill_library.select(request)
|
||||
part_skill_audit = self.part_skill_library.audit(selection, cdsl, assumptions)
|
||||
state["phase"] = "BUILDING"
|
||||
try:
|
||||
yieldable = await asyncio.to_thread(
|
||||
build_revision,
|
||||
settings=self.settings,
|
||||
store=self.store,
|
||||
task_id=task_id or None,
|
||||
request=request,
|
||||
cdsl=cdsl,
|
||||
reference_ids=list(references),
|
||||
summary=summary,
|
||||
part_skills=part_skill_audit,
|
||||
generation_assumptions=assumptions,
|
||||
design_intent=intent,
|
||||
design_intent_path=str(intent_record["record"].get("path") or ""),
|
||||
)
|
||||
except Exception:
|
||||
# The accepted plan stays current so the model can submit a
|
||||
# corrected implementation without silently replanning.
|
||||
state["phase"] = "INTENT_ACCEPTED"
|
||||
raise
|
||||
state["phase"] = "COMPLETED"
|
||||
return {
|
||||
"ok": True,
|
||||
"summary": summary,
|
||||
"task_id": yieldable["task_id"],
|
||||
"revision_id": yieldable["revision_id"],
|
||||
"design_intent_id": design_intent_id,
|
||||
}, yieldable
|
||||
raise ValueError(f"Unknown agent tool: {name}")
|
||||
|
||||
@staticmethod
|
||||
@@ -800,7 +1059,8 @@ class AgentService:
|
||||
"search_cdsl_library": "检索 CDSL 模型库",
|
||||
"read_cdsl_reference": "读取 CDSL 参考模型",
|
||||
"read_current_cdsl": "读取当前 CDSL",
|
||||
"generate_cdsl_model": "生成 CDSL CAD 模型",
|
||||
"propose_design_intent": "生成设计意图",
|
||||
"generate_cdsl_model": "生成 CDSL",
|
||||
}.get(name, "调用 CAD 工具")
|
||||
|
||||
def _attachment_message(self, conversation: dict[str, Any], model: ProviderModel) -> list[dict[str, Any]] | str:
|
||||
|
||||
@@ -147,6 +147,10 @@ def apply_direct_edit(
|
||||
revision_id = str((task or {}).get("current_revision") or "")
|
||||
if source is None or not revision_id:
|
||||
raise ValueError("Task has no successful CDSL revision")
|
||||
current_revision = next(
|
||||
(item for item in (task or {}).get("revisions", []) if item.get("revision_id") == revision_id),
|
||||
{},
|
||||
)
|
||||
frame = _selection_frame(selection)
|
||||
cdsl = copy.deepcopy(json.loads(source.read_text(encoding="utf-8")))
|
||||
features = cdsl.setdefault("features", [])
|
||||
@@ -184,4 +188,9 @@ def apply_direct_edit(
|
||||
summary=f"Applied {operation}",
|
||||
parent_revision_id=revision_id,
|
||||
operation={"type": operation, "selection": selection, "parameters": parameters},
|
||||
part_skills=None,
|
||||
generation_assumptions=[],
|
||||
design_intent_id=str(current_revision.get("design_intent_id") or ""),
|
||||
design_intent_path=str(current_revision.get("design_intent_path") or ""),
|
||||
design_intent_status="accepted" if current_revision.get("design_intent_id") else "",
|
||||
)
|
||||
|
||||
@@ -319,6 +319,68 @@ def apply_parameter_updates(cdsl: dict[str, Any], values: dict[str, float]) -> t
|
||||
return updated, parameter_contract(updated)
|
||||
|
||||
|
||||
def _part_skill_audit(
|
||||
part_skills: dict[str, Any] | None,
|
||||
request: str,
|
||||
generation_assumptions: list[str] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Normalize the planning audit persisted beside a product revision."""
|
||||
audit = copy.deepcopy(part_skills) if isinstance(part_skills, dict) else {}
|
||||
skills = [item for item in audit.get("skills") or [] if isinstance(item, dict)]
|
||||
skill_ids = [str(item) for item in audit.get("skill_ids") or [] if str(item)]
|
||||
if not skill_ids:
|
||||
skill_ids = [str(item.get("id")) for item in skills if item.get("id")]
|
||||
evidence = audit.get("evidence") if isinstance(audit.get("evidence"), dict) else {}
|
||||
audit.update({
|
||||
"schema_version": str(audit.get("schema_version") or "1.0"),
|
||||
"request": str(audit.get("request") or request),
|
||||
"structural_intent": str(audit.get("structural_intent") or request),
|
||||
"skill_ids": skill_ids,
|
||||
"skills": skills,
|
||||
"inherited_skill_ids": [str(item) for item in audit.get("inherited_skill_ids") or [] if str(item)],
|
||||
"assumptions": [str(item) for item in generation_assumptions or []],
|
||||
"evidence": evidence,
|
||||
"capability_translations": [
|
||||
item for item in audit.get("capability_translations") or [] if isinstance(item, dict)
|
||||
],
|
||||
})
|
||||
return audit
|
||||
|
||||
|
||||
def _generation_context(
|
||||
reference_ids: list[str],
|
||||
part_skill_audit: dict[str, Any],
|
||||
design_intent: dict[str, Any] | None = None,
|
||||
design_intent_path: str = "",
|
||||
design_intent_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
skills = [item for item in part_skill_audit.get("skills") or [] if isinstance(item, dict)]
|
||||
intent = design_intent if isinstance(design_intent, dict) else {}
|
||||
return {
|
||||
"design_intent_id": str(intent.get("intent_id") or design_intent_id or ""),
|
||||
"design_intent_path": design_intent_path,
|
||||
"design_intent_structures": [
|
||||
str(item.get("id") or "")
|
||||
for item in intent.get("structures") or []
|
||||
if isinstance(item, dict) and item.get("id")
|
||||
],
|
||||
"cdsl_reference_ids": list(reference_ids),
|
||||
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
|
||||
"part_skill_paths": [
|
||||
{
|
||||
"id": str(item.get("id") or ""),
|
||||
"bridge": str(item.get("bridge") or ""),
|
||||
"source": str(item.get("source") or ""),
|
||||
}
|
||||
for item in skills
|
||||
],
|
||||
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
|
||||
"design_intent_assumptions": list(part_skill_audit.get("design_intent_assumptions") or []),
|
||||
"design_intent_capability_gaps": list(part_skill_audit.get("design_intent_capability_gaps") or []),
|
||||
"capability_translations": list(part_skill_audit.get("capability_translations") or []),
|
||||
}
|
||||
|
||||
|
||||
def build_revision(
|
||||
*,
|
||||
settings: Settings,
|
||||
@@ -331,18 +393,16 @@ def build_revision(
|
||||
parent_revision_id: str | None = None,
|
||||
operation: dict[str, Any] | None = None,
|
||||
attachments: list[dict[str, Any]] | None = None,
|
||||
part_skills: dict[str, Any] | None = None,
|
||||
generation_assumptions: list[str] | None = None,
|
||||
design_intent: dict[str, Any] | None = None,
|
||||
design_intent_path: str = "",
|
||||
design_intent_id: str = "",
|
||||
design_intent_status: str = "",
|
||||
) -> dict[str, Any]:
|
||||
engine = load_engine(settings)
|
||||
task = store.ensure_task(task_id, request)
|
||||
revision_id, revision_dir = store.next_revision(task["task_id"])
|
||||
cdsl_copy = copy.deepcopy(cdsl)
|
||||
cdsl_copy["part_id"] = task["task_id"]
|
||||
meta = cdsl_copy.setdefault("meta", {})
|
||||
if not isinstance(meta, dict):
|
||||
raise ValueError("CDSL meta must be an object when present")
|
||||
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
|
||||
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
|
||||
validate_cdsl(cdsl_copy, engine)
|
||||
cdsl_path = revision_dir / "model.cdsl.json"
|
||||
step_path = revision_dir / "model.step"
|
||||
glb_path = revision_dir / "model.glb"
|
||||
@@ -352,13 +412,76 @@ def build_revision(
|
||||
parameters_path = revision_dir / "parameters.json"
|
||||
selector_path = revision_dir / "model.selector.json"
|
||||
edges_path = revision_dir / "model.edges.json"
|
||||
part_skills_path = revision_dir / "part-skills.json"
|
||||
intent = copy.deepcopy(design_intent) if isinstance(design_intent, dict) else {}
|
||||
intent_assumptions = [str(item) for item in intent.get("assumptions") or [] if str(item)]
|
||||
merged_assumptions = list(dict.fromkeys([
|
||||
*intent_assumptions,
|
||||
*(str(item) for item in generation_assumptions or [] if str(item)),
|
||||
]))
|
||||
part_skill_audit = _part_skill_audit(part_skills, request, merged_assumptions)
|
||||
intent_id = str(intent.get("intent_id") or design_intent_id or "")
|
||||
if design_intent_path:
|
||||
design_intent_path = str(design_intent_path)
|
||||
part_skill_audit.update({
|
||||
"design_intent_id": intent_id,
|
||||
"design_intent_path": design_intent_path,
|
||||
"intent_status": str(intent.get("status") or design_intent_status or ""),
|
||||
"design_intent_assumptions": intent_assumptions,
|
||||
"design_intent_capability_gaps": [
|
||||
item for item in intent.get("capability_gaps") or [] if isinstance(item, dict)
|
||||
],
|
||||
})
|
||||
generation_context = _generation_context(reference_ids, part_skill_audit, intent, design_intent_path, intent_id)
|
||||
write_json(request_path, {"request": request, "created_at": now_iso()})
|
||||
write_json(references_path, {"reference_ids": reference_ids})
|
||||
write_json(cdsl_path, cdsl_copy)
|
||||
contract = parameter_contract(cdsl_copy)
|
||||
write_json(parameters_path, contract)
|
||||
write_json(part_skills_path, part_skill_audit)
|
||||
|
||||
def revision_record(status: str, *, error: str = "", engine_name: str = "") -> dict[str, Any]:
|
||||
record = {
|
||||
"revision_id": revision_id,
|
||||
"status": status,
|
||||
"created_at": now_iso(),
|
||||
"request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"part_skills_path": part_skills_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"design_intent_id": intent_id,
|
||||
"design_intent_path": design_intent_path,
|
||||
"part_skill_ids": list(part_skill_audit.get("skill_ids") or []),
|
||||
"generation_assumptions": list(part_skill_audit.get("assumptions") or []),
|
||||
"reference_ids": reference_ids,
|
||||
"summary": summary,
|
||||
"parent_revision_id": parent_revision_id or "",
|
||||
"operation": operation or {},
|
||||
"attachments": attachments or [],
|
||||
}
|
||||
if status == "success":
|
||||
record.update({
|
||||
"step_path": step_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"engine": engine_name,
|
||||
})
|
||||
else:
|
||||
record["error"] = error
|
||||
return record
|
||||
|
||||
try:
|
||||
cdsl_copy = copy.deepcopy(cdsl)
|
||||
if not isinstance(cdsl_copy, dict):
|
||||
raise ValueError("CDSL must be a JSON object")
|
||||
cdsl_copy["part_id"] = task["task_id"]
|
||||
meta = cdsl_copy.setdefault("meta", {})
|
||||
if not isinstance(meta, dict):
|
||||
raise ValueError("CDSL meta must be an object when present")
|
||||
if not isinstance(meta.get("editable_parameters"), list) or not meta["editable_parameters"]:
|
||||
meta["editable_parameters"] = _derived_parameters(cdsl_copy)
|
||||
write_json(cdsl_path, cdsl_copy)
|
||||
write_json(parameters_path, parameter_contract(cdsl_copy))
|
||||
validate_cdsl(cdsl_copy, engine)
|
||||
# Product revisions are semantic CDSL artifacts. Do not route them
|
||||
# through the legacy rebuild entry point, which is allowed to use
|
||||
# compiler_context/translator compatibility fallbacks.
|
||||
@@ -369,44 +492,23 @@ def build_revision(
|
||||
selector, edges = topology_sidecars(engine_result, preview)
|
||||
write_json(selector_path, selector)
|
||||
write_json(edges_path, edges)
|
||||
report = {"engine_result": engine_result, "preview": preview, "validated_at": now_iso()}
|
||||
report = {
|
||||
"engine_result": engine_result,
|
||||
"preview": preview,
|
||||
"generation_context": generation_context,
|
||||
"validated_at": now_iso(),
|
||||
}
|
||||
write_json(report_path, report)
|
||||
revision = {
|
||||
"revision_id": revision_id,
|
||||
"status": "success",
|
||||
"created_at": now_iso(),
|
||||
"request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"step_path": step_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"glb_path": glb_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"selector_path": selector_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"edges_path": edges_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"reference_ids": reference_ids,
|
||||
"summary": summary,
|
||||
"engine": engine_result["engine"],
|
||||
"parent_revision_id": parent_revision_id or "",
|
||||
"operation": operation or {},
|
||||
"attachments": attachments or [],
|
||||
}
|
||||
revision = revision_record("success", engine_name=str(engine_result["engine"]))
|
||||
except Exception as error:
|
||||
write_json(report_path, {"error": str(error), "validated_at": now_iso()})
|
||||
revision = {
|
||||
"revision_id": revision_id,
|
||||
"status": "failed",
|
||||
"created_at": now_iso(),
|
||||
"request_path": request_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"cdsl_path": cdsl_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"report_path": report_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"parameters_path": parameters_path.relative_to(store.task_dir(task["task_id"])).as_posix(),
|
||||
"reference_ids": reference_ids,
|
||||
"summary": summary,
|
||||
if not cdsl_path.is_file() and isinstance(cdsl, dict):
|
||||
write_json(cdsl_path, copy.deepcopy(cdsl))
|
||||
write_json(report_path, {
|
||||
"error": str(error),
|
||||
"parent_revision_id": parent_revision_id or "",
|
||||
"operation": operation or {},
|
||||
"attachments": attachments or [],
|
||||
}
|
||||
"generation_context": generation_context,
|
||||
"validated_at": now_iso(),
|
||||
})
|
||||
revision = revision_record("failed", error=str(error))
|
||||
store.update_task(task["task_id"], revision)
|
||||
raise
|
||||
store.update_task(task["task_id"], revision)
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
_WORD = re.compile(r"[a-z0-9]+(?:[-'][a-z0-9]+)*")
|
||||
_REPLACEMENT_INTENT = (
|
||||
"replace the whole part",
|
||||
"replace entire part",
|
||||
"replace the part",
|
||||
"replace this part",
|
||||
"start over as",
|
||||
"replace with a",
|
||||
"替换整个零件",
|
||||
"替换整个部件",
|
||||
"替换零件",
|
||||
"重新生成一个",
|
||||
"改成一个新的",
|
||||
)
|
||||
|
||||
|
||||
def _normalize(value: str) -> str:
|
||||
return unicodedata.normalize("NFKC", str(value or "")).casefold().strip()
|
||||
|
||||
|
||||
def _matches(text: str, phrase: str) -> bool:
|
||||
normalized_text = _normalize(text)
|
||||
normalized_phrase = _normalize(phrase)
|
||||
if not normalized_phrase:
|
||||
return False
|
||||
if any("\u4e00" <= char <= "\u9fff" for char in normalized_phrase):
|
||||
return normalized_phrase in normalized_text
|
||||
words = _WORD.findall(normalized_phrase)
|
||||
if not words:
|
||||
return normalized_phrase in normalized_text
|
||||
return bool(re.search(r"(?<![a-z0-9])" + r"\s+".join(map(re.escape, words)) + r"(?![a-z0-9])", normalized_text))
|
||||
|
||||
|
||||
def _specificity(phrase: str) -> int:
|
||||
normalized = _normalize(phrase)
|
||||
chinese = [char for char in normalized if "\u4e00" <= char <= "\u9fff"]
|
||||
return len(chinese) if chinese else len(_WORD.findall(normalized))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PartSkill:
|
||||
id: str
|
||||
kind: str
|
||||
title: str
|
||||
priority: int
|
||||
triggers: tuple[str, ...]
|
||||
exclude: tuple[str, ...]
|
||||
bridge: str
|
||||
source: str
|
||||
related: tuple[str, ...]
|
||||
capability_translation_rules: tuple[str, ...]
|
||||
|
||||
@classmethod
|
||||
def from_mapping(cls, value: dict[str, Any]) -> "PartSkill":
|
||||
return cls(
|
||||
id=str(value["id"]),
|
||||
kind=str(value["kind"]),
|
||||
title=str(value.get("title") or value["id"]),
|
||||
priority=int(value.get("priority") or 0),
|
||||
triggers=tuple(str(item) for item in value.get("triggers") or []),
|
||||
exclude=tuple(str(item) for item in value.get("exclude") or []),
|
||||
bridge=str(value["bridge"]),
|
||||
source=str(value["source"]),
|
||||
related=tuple(str(item) for item in value.get("related") or []),
|
||||
capability_translation_rules=tuple(
|
||||
str(item) for item in value.get("capability_translation_rules") or []
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PartSkillLibrary:
|
||||
"""Deterministic local selector for CDSL-specific part planning guidance."""
|
||||
|
||||
def __init__(self, root: Path) -> None:
|
||||
self.root = Path(root)
|
||||
payload = json.loads((self.root / "catalog.json").read_text(encoding="utf-8"))
|
||||
self.max_planning = int(payload.get("max_planning") or 1)
|
||||
self.max_support = int(payload.get("max_support") or 3)
|
||||
self.skills = tuple(PartSkill.from_mapping(item) for item in payload.get("skills") or [])
|
||||
self.by_id = {skill.id: skill for skill in self.skills}
|
||||
if len(self.by_id) != len(self.skills):
|
||||
raise ValueError("Part skill catalog contains duplicate ids")
|
||||
for skill in self.skills:
|
||||
for relative in (skill.bridge, skill.source):
|
||||
if not (self.root / relative).is_file():
|
||||
raise ValueError(f"Part skill {skill.id} references missing file: {relative}")
|
||||
|
||||
def _matched(self, skill: PartSkill, request: str) -> tuple[int, tuple[str, ...]]:
|
||||
if any(_matches(request, phrase) for phrase in skill.exclude):
|
||||
return 0, ()
|
||||
triggers = tuple(trigger for trigger in skill.triggers if _matches(request, trigger))
|
||||
if not triggers:
|
||||
return 0, ()
|
||||
# Longer phrases are more specific than generic words such as "shaft"
|
||||
# or "hole". Priority breaks ties between equally specific skills.
|
||||
score = sum(max(1, _specificity(trigger)) * 10 for trigger in triggers)
|
||||
return score + skill.priority, triggers
|
||||
|
||||
def _record(self, skill: PartSkill, *, selection: str, matched: tuple[str, ...] = ()) -> dict[str, Any]:
|
||||
return {
|
||||
"id": skill.id,
|
||||
"kind": skill.kind,
|
||||
"category": skill.kind,
|
||||
"title": skill.title,
|
||||
"source": skill.source,
|
||||
"bridge": skill.bridge,
|
||||
"capability_translation_rules": list(skill.capability_translation_rules),
|
||||
"selection": selection,
|
||||
"matched_triggers": list(matched),
|
||||
}
|
||||
|
||||
def select(self, request: str, inherited_ids: list[str] | tuple[str, ...] = ()) -> dict[str, Any]:
|
||||
inherited = [self.by_id[item] for item in inherited_ids if item in self.by_id]
|
||||
scores = [(self._matched(skill, request), skill) for skill in self.skills]
|
||||
planning_matches = sorted(
|
||||
((score, triggers, skill) for (score, triggers), skill in scores if skill.kind == "planning" and score),
|
||||
key=lambda item: (-item[0], -item[2].priority, item[2].id),
|
||||
)
|
||||
inherited_planning = [skill for skill in inherited if skill.kind == "planning"]
|
||||
planning: list[PartSkill] = inherited_planning[: self.max_planning]
|
||||
conflict: dict[str, Any] | None = None
|
||||
replacement_requested = any(_matches(request, phrase) for phrase in _REPLACEMENT_INTENT)
|
||||
if not planning and planning_matches:
|
||||
planning = [planning_matches[0][2]]
|
||||
elif planning and planning_matches and planning_matches[0][2].id not in {skill.id for skill in planning}:
|
||||
if replacement_requested:
|
||||
planning = [planning_matches[0][2]]
|
||||
else:
|
||||
conflict = {
|
||||
"current": planning[0].id,
|
||||
"matched": planning_matches[0][2].id,
|
||||
"message": "A different primary part family matched this revision request; preserve the current family unless replacement is explicit.",
|
||||
}
|
||||
|
||||
selected_ids = {skill.id for skill in planning}
|
||||
inherited_planning_ids = {skill.id for skill in inherited_planning}
|
||||
records = [
|
||||
self._record(
|
||||
skill,
|
||||
selection="inherited" if skill.id in inherited_planning_ids else "selected",
|
||||
matched=next((triggers for score, triggers, candidate in planning_matches if candidate.id == skill.id), ()),
|
||||
)
|
||||
for skill in planning
|
||||
]
|
||||
support: list[tuple[int, int, int, str, PartSkill, tuple[str, ...]]] = []
|
||||
related_ids = {related for skill in planning for related in skill.related}
|
||||
direct_support: list[tuple[int, PartSkill, tuple[str, ...]]] = []
|
||||
for (score, triggers), skill in scores:
|
||||
if skill.kind == "planning" or not score:
|
||||
continue
|
||||
direct_support.append((score, skill, triggers))
|
||||
bonus = 100 if skill.id in related_ids else 0
|
||||
# Direct request matches always win over inherited context, even
|
||||
# when the current planning family's catalog does not name them.
|
||||
support.append((3, bonus + score, skill.priority, skill.id, skill, triggers))
|
||||
direct_related_ids = {
|
||||
related
|
||||
for _, skill, _ in direct_support
|
||||
for related in skill.related
|
||||
if related in self.by_id and self.by_id[related].kind != "planning"
|
||||
}
|
||||
known_support_ids = {item[3] for item in support}
|
||||
for related_id in sorted(direct_related_ids - known_support_ids):
|
||||
skill = self.by_id[related_id]
|
||||
planning_bonus = 25 if skill.id in related_ids else 0
|
||||
support.append((2, 50 + planning_bonus, skill.priority, skill.id, skill, ()))
|
||||
known_support_ids.add(skill.id)
|
||||
for skill in inherited:
|
||||
if skill.kind != "planning" and skill.id not in known_support_ids:
|
||||
# Keep prior guidance only after direct request matches and
|
||||
# their catalog-declared supporting rules.
|
||||
support.append((1, 0, skill.priority, skill.id, skill, ()))
|
||||
known_support_ids.add(skill.id)
|
||||
support.sort(key=lambda item: (-item[0], -item[1], -item[2], item[3]))
|
||||
for _, _, _, _, skill, triggers in support:
|
||||
if len(records) - len(planning) >= self.max_support or skill.id in selected_ids:
|
||||
continue
|
||||
selected_ids.add(skill.id)
|
||||
inherited_marker = "inherited" if skill in inherited else "selected"
|
||||
records.append(self._record(skill, selection=inherited_marker, matched=triggers))
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"request": str(request or ""),
|
||||
"skills": records,
|
||||
"skill_ids": [record["id"] for record in records],
|
||||
"planning_ids": [record["id"] for record in records if record["kind"] == "planning"],
|
||||
"support_ids": [record["id"] for record in records if record["kind"] != "planning"],
|
||||
"inherited_skill_ids": [skill.id for skill in inherited],
|
||||
"replacement_requested": replacement_requested,
|
||||
"conflict": conflict,
|
||||
}
|
||||
|
||||
def inherited_from_task(self, task: dict[str, Any] | None) -> list[str]:
|
||||
revision_id = str((task or {}).get("current_revision") or "")
|
||||
revisions = (task or {}).get("revisions") or []
|
||||
revision = next((item for item in revisions if item.get("revision_id") == revision_id), None)
|
||||
revision_ids = [str(item) for item in (revision or {}).get("part_skill_ids") or [] if str(item) in self.by_id]
|
||||
if revision_ids:
|
||||
return revision_ids
|
||||
# A valid DesignIntent creates a task before the first runtime build.
|
||||
# Its canonical backend selection must therefore be inheritable too.
|
||||
intent_id = str((task or {}).get("current_design_intent_id") or "")
|
||||
intent = next(
|
||||
(item for item in (task or {}).get("design_intents") or [] if str(item.get("intent_id") or "") == intent_id),
|
||||
None,
|
||||
)
|
||||
return [str(item) for item in (intent or {}).get("part_skill_ids") or [] if str(item) in self.by_id]
|
||||
|
||||
def render_context(self, selection: dict[str, Any]) -> str:
|
||||
records = selection.get("skills") or []
|
||||
if not records:
|
||||
return "No part-family skill matched this request. Use the CDSL schema and library only."
|
||||
sections = [
|
||||
"Selected CDSL part-skill guidance (planning context only):",
|
||||
"Part skill guidance never overrides the user request or the authoritative CDSL schema/runtime.",
|
||||
"Translate the guidance into valid CDSL; do not emit build123d source or invent atomics.",
|
||||
]
|
||||
for record in records:
|
||||
path = self.root / str(record["bridge"])
|
||||
sections.append(f"\n[{record['id']}] ({record['selection']})\n{path.read_text(encoding='utf-8').strip()}")
|
||||
if selection.get("conflict"):
|
||||
sections.append("\nPrimary-family conflict: preserve the current task family. Ask one concise clarification question and do not generate until the user explicitly requests whole-part replacement or confirms the current family.")
|
||||
return "\n".join(sections)
|
||||
|
||||
def audit(self, selection: dict[str, Any], cdsl: dict[str, Any], assumptions: list[str] | None = None) -> dict[str, Any]:
|
||||
features = [item for item in cdsl.get("features") or [] if isinstance(item, dict)]
|
||||
feature_ids = [str(item.get("id")) for item in features if item.get("id")]
|
||||
atomics = [str(item.get("atomic_id")) for item in features]
|
||||
sketches = [item for item in (cdsl.get("geometry") or {}).get("sketches") or [] if isinstance(item, dict)]
|
||||
profiles = [str((item.get("profile") or {}).get("type")) for item in sketches]
|
||||
evidence = {
|
||||
"feature_ids": feature_ids,
|
||||
"atomic_ids": sorted(set(atomics)),
|
||||
"profile_types": sorted(set(profiles)),
|
||||
"features": [
|
||||
{
|
||||
"id": str(item.get("id") or ""),
|
||||
"atomic_id": str(item.get("atomic_id") or ""),
|
||||
"sketch_id": str(item.get("sketch_id") or ""),
|
||||
"params": item.get("params") if isinstance(item.get("params"), dict) else {},
|
||||
}
|
||||
for item in features
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"id": str(item.get("id") or ""),
|
||||
"type": str((item.get("profile") or {}).get("type") or ""),
|
||||
"profile": item.get("profile") if isinstance(item.get("profile"), dict) else {},
|
||||
}
|
||||
for item in sketches
|
||||
],
|
||||
}
|
||||
translations: list[dict[str, Any]] = []
|
||||
for record in selection.get("skills") or []:
|
||||
skill_id = str(record.get("id"))
|
||||
status = "exact"
|
||||
reason = "The selected structural guidance is represented by the submitted CDSL plan."
|
||||
if skill_id == "functional/flange-bolt-circle":
|
||||
if "circles" in profiles and "extrude_cut_blind" in atomics:
|
||||
status, reason = "expanded", "Circular bolt layout is represented by explicit circle geometry because no circular-pattern atomic exists."
|
||||
else:
|
||||
status, reason = "blocked", "No explicit circular bolt layout evidence was found in the CDSL."
|
||||
elif skill_id == "atomic/threaded-hole-creation":
|
||||
status, reason = "approximated", "The current runtime preserves a cylindrical bore but does not generate helical thread topology."
|
||||
elif skill_id == "atomic/fillet-chamfer-last":
|
||||
finishing = [item for item in features if item.get("atomic_id") in {"fillet", "chamfer"}]
|
||||
if not finishing:
|
||||
status, reason = "omitted", "No stable finishing selector was submitted; edge treatment was omitted."
|
||||
elif skill_id == "atomic/pattern-holes-from-datum":
|
||||
if "circles" in profiles or "circle_grid" in profiles:
|
||||
status, reason = "expanded", "The layout is represented by explicit profile circles."
|
||||
elif not any(item in {"pattern_linear", "pattern_mirror"} for item in atomics):
|
||||
status, reason = "blocked", "No supported pattern or explicit circle layout was found."
|
||||
translation: dict[str, Any] = {"skill_id": skill_id, "status": status, "reason": reason, "evidence": evidence}
|
||||
if skill_id == "functional/flange-bolt-circle" and status == "expanded":
|
||||
translation["translation"] = "circular_pattern_to_explicit_circles"
|
||||
elif skill_id == "atomic/threaded-hole-creation":
|
||||
translation["translation"] = "thread_geometry_omitted"
|
||||
elif skill_id == "atomic/fillet-chamfer-last" and status == "omitted":
|
||||
translation["translation"] = "selector_unavailable"
|
||||
translations.append(translation)
|
||||
return {
|
||||
"schema_version": "1.0",
|
||||
"request": selection.get("request", ""),
|
||||
"structural_intent": selection.get("request", ""),
|
||||
"skill_ids": list(selection.get("skill_ids") or []),
|
||||
"skills": list(selection.get("skills") or []),
|
||||
"inherited_skill_ids": list(selection.get("inherited_skill_ids") or []),
|
||||
"conflict": selection.get("conflict"),
|
||||
"assumptions": [str(item) for item in assumptions or []],
|
||||
"evidence": evidence,
|
||||
"capability_translations": translations,
|
||||
}
|
||||
@@ -12,6 +12,7 @@ from app.settings import Settings
|
||||
|
||||
TASK_ID = re.compile(r"^cad_[a-z0-9]{12}$")
|
||||
CONVERSATION_ID = re.compile(r"^conv_[a-z0-9]{12}$")
|
||||
DESIGN_INTENT_ID = re.compile(r"^intent_[a-z0-9]{12}$")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -36,6 +37,13 @@ def safe_conversation_id(conversation_id: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
def safe_design_intent_id(intent_id: str) -> str:
|
||||
value = str(intent_id or "").strip()
|
||||
if not DESIGN_INTENT_ID.fullmatch(value):
|
||||
raise ValueError("Invalid design intent id")
|
||||
return value
|
||||
|
||||
|
||||
def safe_relative_path(value: str) -> str:
|
||||
path = Path(str(value or ""))
|
||||
if not value or path.is_absolute() or ".." in path.parts:
|
||||
@@ -102,7 +110,7 @@ class WorkspaceStore:
|
||||
write_json(path, current)
|
||||
return current
|
||||
record = {
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "1.1",
|
||||
"conversation_id": cid,
|
||||
"created_at": now_iso(),
|
||||
"updated_at": now_iso(),
|
||||
@@ -144,12 +152,14 @@ class WorkspaceStore:
|
||||
task_dir = self.task_dir(tid)
|
||||
(task_dir / "revisions").mkdir(parents=True, exist_ok=True)
|
||||
record = {
|
||||
"schema_version": "1.0",
|
||||
"schema_version": "1.1",
|
||||
"task_id": tid,
|
||||
"request": request,
|
||||
"created_at": now_iso(),
|
||||
"updated_at": now_iso(),
|
||||
"current_revision": "",
|
||||
"current_design_intent_id": "",
|
||||
"design_intents": [],
|
||||
"revisions": [],
|
||||
}
|
||||
write_json(path, record)
|
||||
@@ -171,6 +181,81 @@ class WorkspaceStore:
|
||||
write_json(self.task_path(task_id), task)
|
||||
return task
|
||||
|
||||
def create_design_intent(
|
||||
self,
|
||||
task_id: str | None,
|
||||
request: str,
|
||||
intent: dict[str, Any],
|
||||
part_skill_selection: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Persist a validated plan and atomically make it the current plan."""
|
||||
task = self.ensure_task(task_id, request)
|
||||
task.setdefault("design_intents", [])
|
||||
task.setdefault("current_design_intent_id", "")
|
||||
task["schema_version"] = "1.1"
|
||||
previous_id = str(task.get("current_design_intent_id") or "")
|
||||
if previous_id:
|
||||
for record in task["design_intents"]:
|
||||
if str(record.get("intent_id") or "") == previous_id and record.get("status") != "superseded":
|
||||
record["status"] = "superseded"
|
||||
|
||||
intent_id = new_id("intent")
|
||||
created_at = now_iso()
|
||||
relative = Path("planning") / f"design-intent-{intent_id}.json"
|
||||
skill_ids = [str(item) for item in part_skill_selection.get("skill_ids") or [] if str(item)]
|
||||
persisted = dict(intent)
|
||||
persisted.update({
|
||||
"intent_id": intent_id,
|
||||
"created_at": created_at,
|
||||
"part_skill_ids": skill_ids,
|
||||
"part_skill_selection": part_skill_selection,
|
||||
})
|
||||
write_json(self.artifact_path(task["task_id"], relative.as_posix()), persisted)
|
||||
record = {
|
||||
"intent_id": intent_id,
|
||||
"status": "accepted" if persisted.get("status") == "ready" else "pending",
|
||||
"created_at": created_at,
|
||||
"path": relative.as_posix(),
|
||||
"part_skill_ids": skill_ids,
|
||||
"base_revision_id": str(persisted.get("base_revision_id") or ""),
|
||||
}
|
||||
task["design_intents"].append(record)
|
||||
task["current_design_intent_id"] = intent_id
|
||||
task["updated_at"] = now_iso()
|
||||
write_json(self.task_path(task["task_id"]), task)
|
||||
return {"task_id": task["task_id"], **record, "intent": persisted}
|
||||
|
||||
def read_design_intent(self, task_id: str, intent_id: str | None = None) -> dict[str, Any] | None:
|
||||
task = self.read_task(task_id)
|
||||
if not task:
|
||||
return None
|
||||
chosen = safe_design_intent_id(intent_id) if intent_id else str(task.get("current_design_intent_id") or "")
|
||||
if not chosen:
|
||||
return None
|
||||
record = next(
|
||||
(item for item in task.get("design_intents") or [] if str(item.get("intent_id") or "") == chosen),
|
||||
None,
|
||||
)
|
||||
if not isinstance(record, dict):
|
||||
return None
|
||||
path = self.artifact_path(task_id, str(record.get("path") or ""))
|
||||
intent = read_json(path)
|
||||
if not isinstance(intent, dict):
|
||||
return None
|
||||
return {"task_id": task_id, "record": record, "intent": intent}
|
||||
|
||||
def update_design_intent_status(self, task_id: str, intent_id: str, status: str) -> dict[str, Any]:
|
||||
safe_id = safe_design_intent_id(intent_id)
|
||||
task = self.ensure_task(task_id, "")
|
||||
records = task.setdefault("design_intents", [])
|
||||
record = next((item for item in records if str(item.get("intent_id") or "") == safe_id), None)
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError("Design intent not found")
|
||||
record["status"] = str(status)
|
||||
task["updated_at"] = now_iso()
|
||||
write_json(self.task_path(task_id), task)
|
||||
return record
|
||||
|
||||
def read_task(self, task_id: str) -> dict[str, Any] | None:
|
||||
return read_json(self.task_path(task_id))
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from .rebuild import compare_with_gold, compile_cdsl_to_pack, run_cdsl_only, run
|
||||
from .semantic_validation import validate_semantic_cdsl
|
||||
from .sketch_solver import SHAPE_GENERATORS, resolve_all_sketches, resolve_required_sketches
|
||||
from .runtime import ALL_ATOMIC_IDS, EXECUTORS, RuntimeExecutionError, analyze_cdsl, rebuild_cdsl
|
||||
from .design_intent import DesignIntentError, design_intent_from_cdsl, validate_design_intent, validate_intent_cdsl
|
||||
|
||||
# The package-level runtime contract is the session executor registry. The
|
||||
# older llm_engine dispatcher remains available only for legacy engine packs.
|
||||
@@ -38,6 +39,10 @@ __all__ = [
|
||||
"rebuild_cdsl",
|
||||
"analyze_cdsl",
|
||||
"RuntimeExecutionError",
|
||||
"DesignIntentError",
|
||||
"validate_design_intent",
|
||||
"validate_intent_cdsl",
|
||||
"design_intent_from_cdsl",
|
||||
]
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Validation for the semantic planning artifact between a request and CDSL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
from jsonschema.exceptions import SchemaError
|
||||
|
||||
|
||||
class DesignIntentError(ValueError):
|
||||
"""A plan-contract violation with a stable error code for the Agent."""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
def _schema_path(engine: Any) -> Path:
|
||||
return Path(str(engine.__file__)).with_name("design_intent_schema.json")
|
||||
|
||||
|
||||
def load_design_intent_schema(engine: Any) -> dict[str, Any]:
|
||||
path = _schema_path(engine)
|
||||
try:
|
||||
schema = json.loads(path.read_text(encoding="utf-8"))
|
||||
Draft202012Validator.check_schema(schema)
|
||||
except (OSError, json.JSONDecodeError, SchemaError) as error:
|
||||
raise RuntimeError("The local DesignIntent JSON Schema is unavailable or invalid") from error
|
||||
return schema
|
||||
|
||||
|
||||
def _location(error: Any) -> str:
|
||||
return "$" + "".join(
|
||||
f"[{item}]" if isinstance(item, int) else f".{item}"
|
||||
for item in error.absolute_path
|
||||
)
|
||||
|
||||
|
||||
def _schema_validate(intent: dict[str, Any], engine: Any) -> None:
|
||||
errors = sorted(
|
||||
Draft202012Validator(load_design_intent_schema(engine)).iter_errors(intent),
|
||||
key=lambda error: (list(error.absolute_path), error.message),
|
||||
)
|
||||
if errors:
|
||||
error = errors[0]
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"DesignIntent schema violation at {_location(error)}: {error.message}",
|
||||
)
|
||||
|
||||
|
||||
def _runtime_capabilities(engine: Any) -> tuple[set[str], set[str], dict[str, dict[str, Any]]]:
|
||||
try:
|
||||
profile_schema = json.loads(Path(str(engine.__file__)).with_name("profile_schema.json").read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError("The local CDSL profile schema is unavailable or invalid") from error
|
||||
atomic_ids = set(profile_schema.get("runtime_supported_atomic_ids") or getattr(engine, "SUPPORTED_ATOMIC_IDS", ()))
|
||||
profiles = {
|
||||
name
|
||||
for name, contract in (profile_schema.get("profiles") or {}).items()
|
||||
if isinstance(contract, dict) and contract.get("agent_allowed") is True
|
||||
}
|
||||
atomic_contracts = {
|
||||
str(atomic_id): contract
|
||||
for atomic_id, contract in (profile_schema.get("feature_atomic_ids") or {}).items()
|
||||
if isinstance(contract, dict)
|
||||
}
|
||||
return {str(item) for item in atomic_ids}, {str(item) for item in profiles}, atomic_contracts
|
||||
|
||||
|
||||
def _blocking(intent: dict[str, Any]) -> bool:
|
||||
questions = intent.get("open_questions") or []
|
||||
gaps = intent.get("capability_gaps") or []
|
||||
return any(isinstance(item, dict) and item.get("blocking") is True for item in [*questions, *gaps])
|
||||
|
||||
|
||||
def validate_design_intent(
|
||||
intent: dict[str, Any],
|
||||
engine: Any,
|
||||
*,
|
||||
current_revision_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Validate DesignIntent syntax and semantic ordering against runtime capability."""
|
||||
if not isinstance(intent, dict):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent must be a JSON object")
|
||||
_schema_validate(intent, engine)
|
||||
|
||||
normalized = deepcopy(intent)
|
||||
mode = str(normalized["mode"])
|
||||
base_revision_id = str(normalized["base_revision_id"] or "")
|
||||
if mode == "create" and base_revision_id:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "create DesignIntent must not set base_revision_id")
|
||||
if mode == "revise":
|
||||
if not base_revision_id:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "revise DesignIntent requires base_revision_id")
|
||||
if current_revision_id and base_revision_id != current_revision_id:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"revise DesignIntent base_revision_id must be the current successful revision",
|
||||
)
|
||||
|
||||
structures = normalized["structures"]
|
||||
ids = [str(item["id"]) for item in structures]
|
||||
feature_ids = [str(item["cdsl_feature_id"]) for item in structures]
|
||||
if len(set(ids)) != len(ids):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent structure ids must be unique")
|
||||
if len(set(feature_ids)) != len(feature_ids):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "DesignIntent cdsl_feature_id values must be unique")
|
||||
|
||||
feature_order = [str(item) for item in normalized["feature_order"]]
|
||||
if len(set(feature_order)) != len(feature_order) or set(feature_order) != set(ids):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"feature_order must list every DesignIntent structure exactly once",
|
||||
)
|
||||
order = {structure_id: index for index, structure_id in enumerate(feature_order)}
|
||||
atomic_ids, profile_types, atomic_contracts = _runtime_capabilities(engine)
|
||||
for structure in structures:
|
||||
structure_id = str(structure["id"])
|
||||
for dependency in structure["depends_on"]:
|
||||
dependency_id = str(dependency)
|
||||
if dependency_id not in order:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} depends on an unknown structure {dependency_id}",
|
||||
)
|
||||
if order[dependency_id] >= order[structure_id]:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} must depend only on earlier feature_order entries",
|
||||
)
|
||||
strategy = structure["cdsl_strategy"]
|
||||
atomic_id = str(strategy["atomic_id"])
|
||||
if atomic_id not in atomic_ids:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses unsupported atomic_id {atomic_id}",
|
||||
)
|
||||
profile_type = str(strategy.get("profile_type") or "")
|
||||
if atomic_contracts.get(atomic_id, {}).get("requires_sketch") and not profile_type:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses {atomic_id} and requires a profile_type",
|
||||
)
|
||||
if profile_type and profile_type not in profile_types:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Structure {structure_id} uses unsupported or agent-disallowed profile_type {profile_type}",
|
||||
)
|
||||
|
||||
if normalized["status"] == "ready" and _blocking(normalized):
|
||||
raise DesignIntentError(
|
||||
"DESIGN_INTENT_BLOCKED",
|
||||
"A DesignIntent with blocking questions or capability gaps must need clarification",
|
||||
)
|
||||
if normalized["status"] == "needs_clarification" and not _blocking(normalized):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"needs_clarification requires a blocking question or capability gap",
|
||||
)
|
||||
if normalized["status"] == "ready" and not structures:
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready DesignIntent needs at least one structure")
|
||||
if normalized["status"] == "ready" and mode == "create" and not any(item["role"] == "base" for item in structures):
|
||||
raise DesignIntentError("INVALID_DESIGN_INTENT", "A ready create DesignIntent needs a base structure")
|
||||
nonblocking_gaps = [item for item in normalized["capability_gaps"] if not item["blocking"]]
|
||||
if nonblocking_gaps and not normalized["assumptions"]:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
"Non-blocking capability gaps require an explicit approximation assumption",
|
||||
)
|
||||
for gap in normalized["capability_gaps"]:
|
||||
if str(gap["structure_id"]) not in set(ids):
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"Capability gap references an unknown structure {gap['structure_id']}",
|
||||
)
|
||||
valid_expectation_ids = set(ids) | set(feature_ids)
|
||||
for expectation in normalized["verification_expectations"]:
|
||||
if expectation["type"] == "feature_count" and str(expectation["feature_id"]) not in valid_expectation_ids:
|
||||
raise DesignIntentError(
|
||||
"INVALID_DESIGN_INTENT",
|
||||
f"feature_count expectation references an unknown structure or CDSL feature {expectation['feature_id']}",
|
||||
)
|
||||
return normalized
|
||||
|
||||
|
||||
def _has_selector_evidence(value: Any, role: str) -> bool:
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
if role in {"selector", "selectors"} and key == "selectors" and isinstance(child, list) and child:
|
||||
return True
|
||||
if key == role and child not in (None, "", [], {}):
|
||||
if not (isinstance(child, dict) and set(child) == {"unresolved"}):
|
||||
return True
|
||||
if _has_selector_evidence(child, role):
|
||||
return True
|
||||
elif isinstance(value, list):
|
||||
return any(_has_selector_evidence(item, role) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
def validate_intent_cdsl(
|
||||
intent: dict[str, Any],
|
||||
cdsl: dict[str, Any],
|
||||
engine: Any,
|
||||
*,
|
||||
current_cdsl: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Ensure an executable CDSL document faithfully realizes one accepted plan."""
|
||||
normalized = validate_design_intent(intent, engine)
|
||||
if normalized["status"] != "ready":
|
||||
raise DesignIntentError("DESIGN_INTENT_BLOCKED", "CDSL generation is blocked until the DesignIntent is ready")
|
||||
if not isinstance(cdsl, dict):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must be a JSON object")
|
||||
features = cdsl.get("features")
|
||||
sketches = (cdsl.get("geometry") or {}).get("sketches")
|
||||
if not isinstance(features, list) or not isinstance(sketches, list):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL must contain features and sketches")
|
||||
feature_by_id = {str(feature.get("id") or ""): feature for feature in features if isinstance(feature, dict)}
|
||||
if len(feature_by_id) != len(features):
|
||||
raise DesignIntentError("INTENT_CDSL_MISMATCH", "CDSL features must have unique ids")
|
||||
structure_by_id = {str(item["id"]): item for item in normalized["structures"]}
|
||||
expected_feature_ids = [str(structure_by_id[item]["cdsl_feature_id"]) for item in normalized["feature_order"]]
|
||||
if normalized["mode"] == "revise" and isinstance(current_cdsl, dict):
|
||||
base_feature_ids = {
|
||||
str(feature.get("id") or "")
|
||||
for feature in current_cdsl.get("features") or []
|
||||
if isinstance(feature, dict) and feature.get("id")
|
||||
}
|
||||
if not base_feature_ids.issubset(set(expected_feature_ids)):
|
||||
missing = ", ".join(sorted(base_feature_ids - set(expected_feature_ids)))
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"revise DesignIntent must explicitly preserve current features: {missing}",
|
||||
)
|
||||
actual_feature_ids = [str(feature.get("id") or "") for feature in features]
|
||||
if actual_feature_ids != expected_feature_ids:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
"CDSL feature ids and order must exactly match DesignIntent feature_order",
|
||||
)
|
||||
sketch_by_id = {str(sketch.get("id") or ""): sketch for sketch in sketches if isinstance(sketch, dict)}
|
||||
feature_for_structure = {
|
||||
str(structure["id"]): feature_by_id[str(structure["cdsl_feature_id"])]
|
||||
for structure in normalized["structures"]
|
||||
}
|
||||
for structure in normalized["structures"]:
|
||||
structure_id = str(structure["id"])
|
||||
feature = feature_for_structure[structure_id]
|
||||
strategy = structure["cdsl_strategy"]
|
||||
if feature.get("atomic_id") != strategy["atomic_id"]:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} atomic_id does not match structure {structure_id}",
|
||||
)
|
||||
expected_dependencies = {
|
||||
str(structure_by_id[dependency]["cdsl_feature_id"])
|
||||
for dependency in structure["depends_on"]
|
||||
}
|
||||
actual_dependencies = {str(item) for item in feature.get("depends_on") or []}
|
||||
if not expected_dependencies.issubset(actual_dependencies):
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} is missing planned dependencies",
|
||||
)
|
||||
profile_type = str(strategy.get("profile_type") or "")
|
||||
if profile_type:
|
||||
sketch_id = str(feature.get("sketch_id") or "")
|
||||
sketch = sketch_by_id.get(sketch_id)
|
||||
actual_profile_type = str(((sketch or {}).get("profile") or {}).get("type") or "")
|
||||
if actual_profile_type != profile_type:
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} must use profile_type {profile_type}",
|
||||
)
|
||||
for selector_role in strategy["selector_roles"]:
|
||||
if not _has_selector_evidence(feature, str(selector_role)):
|
||||
raise DesignIntentError(
|
||||
"INTENT_CDSL_MISMATCH",
|
||||
f"CDSL feature {feature.get('id')} is missing selector evidence for {selector_role}",
|
||||
)
|
||||
|
||||
|
||||
def design_intent_from_cdsl(cdsl: dict[str, Any], *, request: str, mode: str = "create", base_revision_id: str = "") -> dict[str, Any]:
|
||||
"""Create a conservative audit-only plan for legacy revisions when needed."""
|
||||
sketches = (cdsl.get("geometry") or {}).get("sketches") or []
|
||||
profile_by_sketch_id = {
|
||||
str(sketch.get("id") or ""): str((sketch.get("profile") or {}).get("type") or "")
|
||||
for sketch in sketches
|
||||
if isinstance(sketch, dict)
|
||||
}
|
||||
structures: list[dict[str, Any]] = []
|
||||
for index, feature in enumerate(cdsl.get("features") or []):
|
||||
if not isinstance(feature, dict):
|
||||
continue
|
||||
feature_id = str(feature.get("id") or f"feature_{index + 1}")
|
||||
atomic_id = str(feature.get("atomic_id") or "")
|
||||
profile_type = profile_by_sketch_id.get(str(feature.get("sketch_id") or ""), "")
|
||||
strategy: dict[str, Any] = {
|
||||
"atomic_id": atomic_id,
|
||||
"parameter_roles": sorted(str(key) for key in (feature.get("params") or {}).keys()),
|
||||
"selector_roles": [],
|
||||
}
|
||||
if profile_type:
|
||||
strategy["profile_type"] = profile_type
|
||||
structures.append({
|
||||
"id": feature_id,
|
||||
"cdsl_feature_id": feature_id,
|
||||
"role": "base" if index == 0 else "subtractive" if "cut" in atomic_id or atomic_id.startswith("hole") else "additive",
|
||||
"purpose": str(feature.get("name") or feature_id),
|
||||
"depends_on": [str(item) for item in feature.get("depends_on") or []],
|
||||
"cdsl_strategy": strategy,
|
||||
})
|
||||
return {
|
||||
"schema": "cad.cdsl.design-intent.v1",
|
||||
"schema_version": "1.0",
|
||||
"mode": mode,
|
||||
"request": request,
|
||||
"base_revision_id": base_revision_id,
|
||||
"structures": structures,
|
||||
"feature_order": [item["id"] for item in structures],
|
||||
"assumptions": ["Synthesized from a legacy CDSL revision."],
|
||||
"open_questions": [],
|
||||
"capability_gaps": [],
|
||||
"verification_expectations": [],
|
||||
"status": "ready",
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cdsl.local/schema/cad.cdsl.design-intent.v1",
|
||||
"title": "CDSL design intent",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {"const": "cad.cdsl.design-intent.v1"},
|
||||
"schema_version": {"const": "1.0"},
|
||||
"mode": {"enum": ["create", "revise"]},
|
||||
"request": {"type": "string", "minLength": 1},
|
||||
"base_revision_id": {"type": "string"},
|
||||
"structures": {"type": "array", "items": {"$ref": "#/$defs/structure"}},
|
||||
"feature_order": {"type": "array", "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"}},
|
||||
"assumptions": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"open_questions": {"type": "array", "items": {"$ref": "#/$defs/openQuestion"}},
|
||||
"capability_gaps": {"type": "array", "items": {"$ref": "#/$defs/capabilityGap"}},
|
||||
"verification_expectations": {"type": "array", "items": {"$ref": "#/$defs/verificationExpectation"}},
|
||||
"status": {"enum": ["ready", "needs_clarification"]},
|
||||
"intent_id": {"type": "string", "pattern": "^intent_[a-z0-9]{12}$"},
|
||||
"created_at": {"type": "string", "minLength": 1},
|
||||
"part_skill_ids": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"part_skill_selection": {"type": "object"}
|
||||
},
|
||||
"required": [
|
||||
"schema",
|
||||
"schema_version",
|
||||
"mode",
|
||||
"request",
|
||||
"base_revision_id",
|
||||
"structures",
|
||||
"feature_order",
|
||||
"assumptions",
|
||||
"open_questions",
|
||||
"capability_gaps",
|
||||
"verification_expectations",
|
||||
"status"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"structure": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"cdsl_feature_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"role": {"enum": ["base", "reference", "additive", "subtractive", "dressup", "pattern"]},
|
||||
"purpose": {"type": "string", "minLength": 1},
|
||||
"depends_on": {"type": "array", "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"}},
|
||||
"cdsl_strategy": {"$ref": "#/$defs/cdslStrategy"}
|
||||
},
|
||||
"required": ["id", "cdsl_feature_id", "role", "purpose", "depends_on", "cdsl_strategy"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"cdslStrategy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"atomic_id": {"type": "string", "minLength": 1},
|
||||
"profile_type": {"type": "string", "minLength": 1},
|
||||
"parameter_roles": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
||||
"selector_roles": {"type": "array", "items": {"type": "string", "minLength": 1}}
|
||||
},
|
||||
"required": ["atomic_id", "parameter_roles", "selector_roles"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"openQuestion": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"question": {"type": "string", "minLength": 1},
|
||||
"blocking": {"type": "boolean"}
|
||||
},
|
||||
"required": ["id", "question", "blocking"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"capabilityGap": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,100}$"},
|
||||
"structure_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"message": {"type": "string", "minLength": 1},
|
||||
"blocking": {"type": "boolean"}
|
||||
},
|
||||
"required": ["code", "structure_id", "message", "blocking"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"verificationExpectation": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "feature_count"},
|
||||
"feature_id": {"type": "string", "pattern": "^[A-Za-z0-9_-]{1,80}$"},
|
||||
"expected": {"type": "integer", "minimum": 0}
|
||||
},
|
||||
"required": ["type", "feature_id", "expected"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {"type": {"const": "symmetry"}, "axis": {"enum": ["x", "y", "z"]}},
|
||||
"required": ["type", "axis"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {"const": "bbox"},
|
||||
"expected_mm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {"type": "number", "exclusiveMinimum": 0},
|
||||
"y": {"type": "number", "exclusiveMinimum": 0},
|
||||
"z": {"type": "number", "exclusiveMinimum": 0}
|
||||
},
|
||||
"required": ["x", "y", "z"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["type", "expected_mm"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,11 +62,11 @@ class ParseToolArgumentsTests(unittest.TestCase):
|
||||
self.assertNotIn("extrude", cdsl["$defs"]["feature_atomic_ids"]["enum"])
|
||||
self.assertEqual(cdsl, CDSL_TOOL_SCHEMA)
|
||||
|
||||
def test_strict_tool_schema_is_limited_to_cdsl_generation_arguments(self) -> None:
|
||||
def test_strict_tool_schema_covers_design_intent_and_cdsl_generation_arguments(self) -> None:
|
||||
tools = tools_for_model(ProviderModel("strict-model", strict_tool_schema=True))
|
||||
strict_tools = [tool["function"]["name"] for tool in tools if tool["function"].get("strict")]
|
||||
|
||||
self.assertEqual(strict_tools, ["generate_cdsl_model"])
|
||||
self.assertEqual(strict_tools, ["propose_design_intent", "generate_cdsl_model"])
|
||||
generate_tool = next(tool for tool in tools if tool["function"]["name"] == "generate_cdsl_model")
|
||||
self.assertEqual(generate_tool["function"]["parameters"]["properties"]["summary"], {"type": "string", "minLength": 1})
|
||||
self.assertEqual(generate_tool["function"]["parameters"]["properties"]["cdsl"], CDSL_TOOL_SCHEMA)
|
||||
@@ -243,7 +243,7 @@ class ToolArgumentsRetryTests(unittest.TestCase):
|
||||
self.assertTrue(any("已修正工具参数" in str(event.get("text", "")) for event in events))
|
||||
self.assertEqual(list(settings.task_root.glob("cad_*")), [])
|
||||
|
||||
def test_incomplete_cdsl_is_returned_to_the_model_without_creating_a_task(self) -> None:
|
||||
def test_direct_cdsl_generation_is_rejected_without_creating_a_task(self) -> None:
|
||||
class RetryAgent(AgentService):
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -328,8 +328,8 @@ class ToolArgumentsRetryTests(unittest.TestCase):
|
||||
|
||||
tool_result = agent.seen_messages[1][-1]
|
||||
self.assertEqual(tool_result["role"], "tool")
|
||||
self.assertEqual(json.loads(str(tool_result["content"]))["code"], "INVALID_CDSL")
|
||||
self.assertEqual(agent.required_tools, [None, "generate_cdsl_model", None])
|
||||
self.assertEqual(json.loads(str(tool_result["content"]))["code"], "DESIGN_INTENT_REQUIRED")
|
||||
self.assertEqual(agent.required_tools, [None, None, None])
|
||||
self.assertEqual(list(settings.task_root.glob("cad_*")), [])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "backend"))
|
||||
|
||||
|
||||
|
||||
def workplane(z: float = 0.0) -> dict[str, list[float]]:
|
||||
return {
|
||||
"origin_mm": [0.0, 0.0, z],
|
||||
"x_dir": [1.0, 0.0, 0.0],
|
||||
"y_dir": [0.0, 1.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
}
|
||||
|
||||
|
||||
def mounting_plate_intent() -> dict:
|
||||
return {
|
||||
"schema": "cad.cdsl.design-intent.v1",
|
||||
"schema_version": "1.0",
|
||||
"mode": "create",
|
||||
"request": "Create a mounting plate with four holes and a center slot",
|
||||
"base_revision_id": "",
|
||||
"structures": [
|
||||
{
|
||||
"id": "base_plate",
|
||||
"cdsl_feature_id": "base_add",
|
||||
"role": "base",
|
||||
"purpose": "Rectangular mounting plate",
|
||||
"depends_on": [],
|
||||
"cdsl_strategy": {
|
||||
"atomic_id": "extrude_add_blind",
|
||||
"profile_type": "rectangle",
|
||||
"parameter_roles": ["width_mm", "height_mm", "thickness_mm"],
|
||||
"selector_roles": [],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "mounting_holes",
|
||||
"cdsl_feature_id": "hole_cut",
|
||||
"role": "subtractive",
|
||||
"purpose": "Four mounting holes",
|
||||
"depends_on": ["base_plate"],
|
||||
"cdsl_strategy": {
|
||||
"atomic_id": "extrude_cut_blind",
|
||||
"profile_type": "circle_grid",
|
||||
"parameter_roles": ["diameter_mm", "count_x", "count_y"],
|
||||
"selector_roles": [],
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "adjustment_slot",
|
||||
"cdsl_feature_id": "slot_cut",
|
||||
"role": "subtractive",
|
||||
"purpose": "Center adjustment slot",
|
||||
"depends_on": ["mounting_holes"],
|
||||
"cdsl_strategy": {
|
||||
"atomic_id": "extrude_cut_blind",
|
||||
"profile_type": "obround",
|
||||
"parameter_roles": ["length_mm", "width_mm", "depth_mm"],
|
||||
"selector_roles": [],
|
||||
},
|
||||
},
|
||||
],
|
||||
"feature_order": ["base_plate", "mounting_holes", "adjustment_slot"],
|
||||
"assumptions": [],
|
||||
"open_questions": [],
|
||||
"capability_gaps": [],
|
||||
"verification_expectations": [
|
||||
{"type": "feature_count", "feature_id": "mounting_holes", "expected": 4},
|
||||
{"type": "symmetry", "axis": "x"},
|
||||
{"type": "bbox", "expected_mm": {"x": 100, "y": 60, "z": 10}},
|
||||
],
|
||||
"status": "ready",
|
||||
}
|
||||
|
||||
|
||||
def mounting_plate_cdsl() -> dict:
|
||||
return {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.0",
|
||||
"kind": "part",
|
||||
"part_id": "mounting-plate",
|
||||
"geometry": {
|
||||
"sketches": [
|
||||
{"id": "base_sketch", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 100, "height_mm": 60}},
|
||||
{"id": "holes_sketch", "workplane": workplane(10), "profile": {"type": "circle_grid", "radius_mm": 3, "count_x": 2, "count_y": 2, "spacing_x_mm": 80, "spacing_y_mm": 40, "center_mm": [0, 0]}},
|
||||
{"id": "slot_sketch", "workplane": workplane(10), "profile": {"type": "obround", "center": [0, 0], "length_mm": 32, "width_mm": 10}},
|
||||
],
|
||||
},
|
||||
"features": [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base_sketch", "params": {"distance_mm": 10}},
|
||||
{"id": "hole_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "sketch_id": "holes_sketch", "params": {"distance_mm": 10, "reverse": True}},
|
||||
{"id": "slot_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["hole_cut"], "sketch_id": "slot_sketch", "params": {"distance_mm": 10, "reverse": True}},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class DesignIntentValidationTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
sys.path.insert(0, str(ROOT / "backend" / "engine"))
|
||||
import cdsl_engine
|
||||
|
||||
cls.engine = cdsl_engine
|
||||
|
||||
def assert_plan_error(self, intent: dict, code: str = "INVALID_DESIGN_INTENT") -> None:
|
||||
with self.assertRaises(self.engine.DesignIntentError) as context:
|
||||
self.engine.validate_design_intent(intent, self.engine)
|
||||
self.assertEqual(context.exception.code, code)
|
||||
|
||||
def test_valid_mounting_plate_plan_and_cdsl_match(self) -> None:
|
||||
intent = mounting_plate_intent()
|
||||
validated = self.engine.validate_design_intent(intent, self.engine)
|
||||
|
||||
self.assertEqual(validated["feature_order"], ["base_plate", "mounting_holes", "adjustment_slot"])
|
||||
self.engine.validate_intent_cdsl(validated, mounting_plate_cdsl(), self.engine)
|
||||
|
||||
def test_revise_requires_current_base_revision(self) -> None:
|
||||
intent = mounting_plate_intent()
|
||||
intent["mode"] = "revise"
|
||||
intent["base_revision_id"] = ""
|
||||
self.assert_plan_error(intent)
|
||||
|
||||
intent["base_revision_id"] = "rev_001"
|
||||
self.engine.validate_design_intent(intent, self.engine, current_revision_id="rev_001")
|
||||
with self.assertRaises(self.engine.DesignIntentError) as context:
|
||||
self.engine.validate_design_intent(intent, self.engine, current_revision_id="rev_002")
|
||||
self.assertEqual(context.exception.code, "INVALID_DESIGN_INTENT")
|
||||
|
||||
def test_rejects_duplicate_ids_bad_order_and_unregistered_capabilities(self) -> None:
|
||||
duplicate = mounting_plate_intent()
|
||||
duplicate["structures"][1]["id"] = "base_plate"
|
||||
self.assert_plan_error(duplicate)
|
||||
|
||||
cyclic = mounting_plate_intent()
|
||||
cyclic["structures"][0]["depends_on"] = ["mounting_holes"]
|
||||
self.assert_plan_error(cyclic)
|
||||
|
||||
unsupported = mounting_plate_intent()
|
||||
unsupported["structures"][0]["cdsl_strategy"]["atomic_id"] = "thread_add"
|
||||
self.assert_plan_error(unsupported)
|
||||
|
||||
incomplete = mounting_plate_intent()
|
||||
incomplete["feature_order"].pop()
|
||||
self.assert_plan_error(incomplete)
|
||||
|
||||
no_profile = mounting_plate_intent()
|
||||
no_profile["structures"][0]["cdsl_strategy"].pop("profile_type")
|
||||
self.assert_plan_error(no_profile)
|
||||
|
||||
unknown_expectation = mounting_plate_intent()
|
||||
unknown_expectation["verification_expectations"][0]["feature_id"] = "missing"
|
||||
self.assert_plan_error(unknown_expectation)
|
||||
|
||||
def test_blockers_and_nonblocking_approximations_are_explicit(self) -> None:
|
||||
clarification = mounting_plate_intent()
|
||||
clarification["open_questions"] = [{"id": "thickness", "question": "What thickness is required?", "blocking": True}]
|
||||
clarification["status"] = "needs_clarification"
|
||||
self.engine.validate_design_intent(clarification, self.engine)
|
||||
with self.assertRaises(self.engine.DesignIntentError) as context:
|
||||
self.engine.validate_intent_cdsl(clarification, mounting_plate_cdsl(), self.engine)
|
||||
self.assertEqual(context.exception.code, "DESIGN_INTENT_BLOCKED")
|
||||
|
||||
approximation = mounting_plate_intent()
|
||||
approximation["capability_gaps"] = [{
|
||||
"code": "unsupported_thread_geometry",
|
||||
"structure_id": "mounting_holes",
|
||||
"message": "Only a cylindrical bore is available.",
|
||||
"blocking": False,
|
||||
}]
|
||||
self.assert_plan_error(approximation)
|
||||
approximation["assumptions"] = ["Thread geometry is approximated by cylindrical bores."]
|
||||
self.engine.validate_design_intent(approximation, self.engine)
|
||||
|
||||
def test_rejects_cdsl_feature_atomic_profile_and_selector_mismatches(self) -> None:
|
||||
missing_feature = mounting_plate_cdsl()
|
||||
missing_feature["features"].pop()
|
||||
with self.assertRaises(self.engine.DesignIntentError) as context:
|
||||
self.engine.validate_intent_cdsl(mounting_plate_intent(), missing_feature, self.engine)
|
||||
self.assertEqual(context.exception.code, "INTENT_CDSL_MISMATCH")
|
||||
|
||||
atomic = mounting_plate_cdsl()
|
||||
atomic["features"][1]["atomic_id"] = "extrude_add_blind"
|
||||
with self.assertRaises(self.engine.DesignIntentError):
|
||||
self.engine.validate_intent_cdsl(mounting_plate_intent(), atomic, self.engine)
|
||||
|
||||
profile = mounting_plate_cdsl()
|
||||
profile["geometry"]["sketches"][1]["profile"]["type"] = "circles"
|
||||
with self.assertRaises(self.engine.DesignIntentError):
|
||||
self.engine.validate_intent_cdsl(mounting_plate_intent(), profile, self.engine)
|
||||
|
||||
selector_plan = copy.deepcopy(mounting_plate_intent())
|
||||
selector_plan["structures"][2]["cdsl_strategy"]["selector_roles"] = ["host_face"]
|
||||
with self.assertRaises(self.engine.DesignIntentError):
|
||||
self.engine.validate_intent_cdsl(selector_plan, mounting_plate_cdsl(), self.engine)
|
||||
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "backend"))
|
||||
|
||||
from app.services.agent_service import AgentService # noqa: E402
|
||||
from app.services.engine_service import load_engine # noqa: E402
|
||||
from app.services.library import CdslLibrary # noqa: E402
|
||||
from app.services.part_skills import PartSkillLibrary # noqa: E402
|
||||
from app.services.storage import WorkspaceStore # noqa: E402
|
||||
from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402
|
||||
from tests.test_design_intent import mounting_plate_cdsl, mounting_plate_intent # noqa: E402
|
||||
|
||||
|
||||
BACKEND = ROOT / "backend"
|
||||
PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
|
||||
|
||||
|
||||
class DesignIntentFlowTests(unittest.TestCase):
|
||||
def settings(self, root: Path) -> Settings:
|
||||
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
||||
return Settings(
|
||||
task_root=root / "tasks",
|
||||
conversation_root=root / "conversations",
|
||||
library_root=BACKEND / "cdsl_library",
|
||||
engine_root=BACKEND / "engine" / "cdsl_engine",
|
||||
llm_base_url=provider.base_url,
|
||||
llm_api_key=provider.api_key,
|
||||
llm_model="test-model",
|
||||
llm_timeout_s=1,
|
||||
default_provider_id="test",
|
||||
providers=(provider,),
|
||||
)
|
||||
|
||||
def test_design_intent_is_required_before_library_or_cdsl(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self.settings(Path(directory))
|
||||
store = WorkspaceStore(settings)
|
||||
agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT))
|
||||
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "mounting plate"}, "", "mounting plate", [], intent_state=state))
|
||||
generated, _ = asyncio.run(agent._run_tool("generate_cdsl_model", {"design_intent_id": "intent_aaaaaaaaaaaa", "cdsl": {}, "summary": "x", "assumptions": []}, "", "mounting plate", [], intent_state=state))
|
||||
|
||||
self.assertEqual(searched["code"], "DESIGN_INTENT_REQUIRED")
|
||||
self.assertEqual(generated["code"], "DESIGN_INTENT_REQUIRED")
|
||||
self.assertEqual(list(settings.task_root.glob("cad_*")), [])
|
||||
|
||||
def test_ready_plan_persists_before_build_and_links_success_revision(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self.settings(Path(directory))
|
||||
store = WorkspaceStore(settings)
|
||||
skills = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
agent = AgentService(settings, store, CdslLibrary(settings), skills)
|
||||
request = "Create a mounting plate with four holes and a center slot"
|
||||
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
planned, _ = asyncio.run(agent._run_tool(
|
||||
"propose_design_intent",
|
||||
{"intent": mounting_plate_intent(), "summary": "plate plan", "assumptions": []},
|
||||
"", request, [], part_skill_selection=skills.select(request), intent_state=state,
|
||||
))
|
||||
|
||||
self.assertTrue(planned["ok"])
|
||||
task = store.read_task(planned["task_id"])
|
||||
self.assertEqual(task["current_revision"], "")
|
||||
planning_path = store.artifact_path(planned["task_id"], planned["design_intent_path"])
|
||||
self.assertTrue(planning_path.is_file())
|
||||
saved = json.loads(planning_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(saved["part_skill_ids"], skills.select(request)["skill_ids"])
|
||||
|
||||
searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "plate"}, planned["task_id"], request, [], intent_state=state))
|
||||
self.assertTrue(searched["ok"])
|
||||
result, built = asyncio.run(agent._run_tool(
|
||||
"generate_cdsl_model",
|
||||
{"design_intent_id": planned["design_intent_id"], "cdsl": mounting_plate_cdsl(), "summary": "plate", "assumptions": []},
|
||||
planned["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state,
|
||||
))
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertIsNotNone(built)
|
||||
revision = store.read_task(planned["task_id"])["revisions"][-1]
|
||||
self.assertEqual(revision["design_intent_id"], planned["design_intent_id"])
|
||||
self.assertEqual(revision["design_intent_path"], planned["design_intent_path"])
|
||||
audit = json.loads(store.artifact_path(planned["task_id"], revision["part_skills_path"]).read_text(encoding="utf-8"))
|
||||
self.assertEqual(audit["design_intent_id"], planned["design_intent_id"])
|
||||
self.assertEqual(audit["design_intent_assumptions"], [])
|
||||
|
||||
def test_blocked_plan_is_persisted_without_a_revision_and_new_plan_supersedes_it(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self.settings(Path(directory))
|
||||
store = WorkspaceStore(settings)
|
||||
skills = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
agent = AgentService(settings, store, CdslLibrary(settings), skills)
|
||||
request = "Create a mounting plate"
|
||||
state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
blocked_intent = mounting_plate_intent()
|
||||
blocked_intent["open_questions"] = [{"id": "thickness", "question": "Thickness?", "blocking": True}]
|
||||
blocked_intent["status"] = "needs_clarification"
|
||||
blocked, _ = asyncio.run(agent._run_tool(
|
||||
"propose_design_intent",
|
||||
{"intent": blocked_intent, "summary": "blocked", "assumptions": []},
|
||||
"", request, [], part_skill_selection=skills.select(request), intent_state=state,
|
||||
))
|
||||
self.assertEqual(blocked["code"], "DESIGN_INTENT_BLOCKED")
|
||||
task = store.read_task(blocked["task_id"])
|
||||
self.assertEqual(task["revisions"], [])
|
||||
self.assertEqual(task["design_intents"][0]["status"], "pending")
|
||||
|
||||
ready, _ = asyncio.run(agent._run_tool(
|
||||
"propose_design_intent",
|
||||
{"intent": mounting_plate_intent(), "summary": "ready", "assumptions": []},
|
||||
blocked["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state,
|
||||
))
|
||||
self.assertTrue(ready["ok"])
|
||||
task = store.read_task(blocked["task_id"])
|
||||
self.assertEqual(task["design_intents"][0]["status"], "superseded")
|
||||
self.assertEqual(task["design_intents"][-1]["status"], "accepted")
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
@@ -9,7 +11,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "json_to_cdsl"))
|
||||
|
||||
from evidence_v2_to_cdsl import StepInspector, convert_evidence
|
||||
from evidence_v2_to_cdsl import StepInspector, batch_convert, convert_evidence
|
||||
|
||||
|
||||
def _identity(stable_id: str, *, kind: str = "feature", geometry: dict | None = None) -> dict:
|
||||
@@ -59,6 +61,42 @@ FIXTURE = {
|
||||
|
||||
|
||||
class EvidenceV2ToCdslTests(unittest.TestCase):
|
||||
def test_batch_conversion_recurses_and_records_relative_source_path(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
evidence_dir = root / "evidence"
|
||||
source = evidence_dir / "category" / "part.solidworks_evidence_v2.json"
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text(json.dumps(FIXTURE), encoding="utf-8")
|
||||
|
||||
output_dir = root / "output"
|
||||
manifest = batch_convert(evidence_dir, output_dir)
|
||||
|
||||
self.assertEqual(manifest["input_count"], 1)
|
||||
self.assertEqual(manifest["converted_count"], 1)
|
||||
self.assertEqual(manifest["results"][0]["source"], "category/part.solidworks_evidence_v2.json")
|
||||
diagnostic = json.loads((output_dir / "part.diagnostic.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(diagnostic["source"], "category/part.solidworks_evidence_v2.json")
|
||||
|
||||
def test_batch_conversion_uses_full_filename_and_disambiguates_normalized_ids(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary_directory:
|
||||
root = Path(temporary_directory)
|
||||
evidence_dir = root / "evidence"
|
||||
sources = [
|
||||
evidence_dir / "first" / "14.Rod Nut.solidworks_evidence_v2.json",
|
||||
evidence_dir / "second" / "14 Rod Nut.solidworks_evidence_v2.json",
|
||||
]
|
||||
for source in sources:
|
||||
source.parent.mkdir(parents=True, exist_ok=True)
|
||||
source.write_text(json.dumps(FIXTURE), encoding="utf-8")
|
||||
|
||||
manifest = batch_convert(evidence_dir, root / "output")
|
||||
|
||||
self.assertEqual(manifest["converted_count"], 2)
|
||||
part_ids = {item["part_id"] for item in manifest["results"]}
|
||||
self.assertEqual(len(part_ids), 2)
|
||||
self.assertTrue(all(part_id.startswith("14-Rod-Nut-") for part_id in part_ids))
|
||||
|
||||
def test_converts_fixed_evidence_v2_fixture_to_semantic_cdsl(self) -> None:
|
||||
cdsl, diagnostic = convert_evidence(FIXTURE, source_name="fixture.solidworks_evidence_v2.json")
|
||||
atoms = {feature["atomic_id"] for feature in cdsl["features"]}
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "backend"))
|
||||
|
||||
from app.models.contracts import ChatMessage, MessagePart # noqa: E402
|
||||
from app.services.agent_service import AgentService, TOOL_SCHEMAS, system_prompt # noqa: E402
|
||||
from app.services.engine_service import build_revision, load_engine, validate_cdsl # noqa: E402
|
||||
from app.services.library import CdslLibrary # noqa: E402
|
||||
from app.services.part_skills import PartSkillLibrary # noqa: E402
|
||||
from app.services.storage import WorkspaceStore # noqa: E402
|
||||
from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402
|
||||
|
||||
|
||||
BACKEND = ROOT / "backend"
|
||||
PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills"
|
||||
|
||||
|
||||
def workplane(z: float = 0.0) -> dict[str, list[float]]:
|
||||
return {
|
||||
"origin_mm": [0.0, 0.0, z],
|
||||
"x_dir": [1.0, 0.0, 0.0],
|
||||
"y_dir": [0.0, 1.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
}
|
||||
|
||||
|
||||
def document(part_id: str, sketches: list[dict], features: list[dict]) -> dict:
|
||||
return {
|
||||
"schema": "cad.cdsl.llm.v1",
|
||||
"schema_version": "1.1.0",
|
||||
"kind": "part",
|
||||
"part_id": part_id,
|
||||
"meta": {"unit": "mm"},
|
||||
"geometry": {"sketches": sketches},
|
||||
"features": features,
|
||||
}
|
||||
|
||||
|
||||
def mounting_plate_fixture() -> dict:
|
||||
return document("golden-mounting-plate", [
|
||||
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 80, "height_mm": 60}},
|
||||
{"id": "grid", "workplane": workplane(10), "profile": {"type": "circle_grid", "radius_mm": 3, "count_x": 2, "count_y": 2, "spacing_x_mm": 50, "spacing_y_mm": 30, "center_mm": [0, 0]}},
|
||||
], [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 10}, "sketch_id": "base"},
|
||||
{"id": "mount_pattern", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 10, "reverse": True}, "sketch_id": "grid"},
|
||||
{"id": "counterbores", "atomic_id": "hole_counterbore", "depends_on": ["mount_pattern"], "params": {"diameter_mm": 6, "depth_mm": 10, "counterbore_diameter_mm": 12, "counterbore_depth_mm": 4, "positions": [{"mm": [-15, 0, 0]}, {"mm": [15, 0, 0]}], "host_face": {"frame": workplane(10)}}, "sketch_id": "base"},
|
||||
])
|
||||
|
||||
|
||||
def mounting_bracket_fixture() -> dict:
|
||||
web_plane = {"origin_mm": [0, -20, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1], "normal": [0, 1, 0]}
|
||||
return document("golden-mounting-bracket", [
|
||||
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 80, "height_mm": 40}},
|
||||
{"id": "web", "workplane": web_plane, "profile": {"type": "rectangle", "center": [0, -15], "width_mm": 50, "height_mm": 30}},
|
||||
{"id": "slot", "workplane": workplane(6), "profile": {"type": "obround", "center": [0, 0], "length_mm": 24, "width_mm": 8}},
|
||||
{"id": "symmetric_holes", "workplane": workplane(6), "profile": {"type": "circles", "items": [{"center": [-25, 0], "radius_mm": 3}, {"center": [25, 0], "radius_mm": 3}]}},
|
||||
], [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 6}, "sketch_id": "base"},
|
||||
{"id": "web_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "params": {"distance_mm": 6}, "sketch_id": "web"},
|
||||
{"id": "slot_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["web_add"], "params": {"distance_mm": 6, "reverse": True}, "sketch_id": "slot"},
|
||||
{"id": "symmetric_cut", "atomic_id": "extrude_cut_blind", "depends_on": ["slot_cut"], "params": {"distance_mm": 6, "reverse": True}, "sketch_id": "symmetric_holes"},
|
||||
])
|
||||
|
||||
|
||||
def flange_fixture() -> dict:
|
||||
return document("golden-flange", [
|
||||
{"id": "base", "workplane": workplane(), "profile": {"type": "annulus", "inner_radius_mm": 10, "outer_radius_mm": 40}},
|
||||
{"id": "bolt_circle", "workplane": workplane(12), "profile": {"type": "circles", "items": [{"center": [25, 0], "radius_mm": 3}, {"center": [0, 25], "radius_mm": 3}, {"center": [-25, 0], "radius_mm": 3}, {"center": [0, -25], "radius_mm": 3}]}},
|
||||
], [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 12}, "sketch_id": "base"},
|
||||
{"id": "bolt_holes", "atomic_id": "extrude_cut_blind", "depends_on": ["base_add"], "params": {"distance_mm": 12, "reverse": True}, "sketch_id": "bolt_circle"},
|
||||
])
|
||||
|
||||
|
||||
def shaft_fixture() -> dict:
|
||||
end_plane = {"origin_mm": [0, 35, 0], "x_dir": [1, 0, 0], "y_dir": [0, 0, -1], "normal": [0, 1, 0]}
|
||||
return document("golden-stepped-shaft", [
|
||||
{"id": "shaft_profile", "workplane": workplane(), "profile": {"type": "polygon", "vertices": [[0, 0], [10, 0], [10, 15], [7, 15], [7, 35], [0, 35]]}},
|
||||
], [
|
||||
{"id": "shaft_add", "atomic_id": "revolve_add", "depends_on": [], "params": {"angle_deg": 360, "axis": {"origin_mm": [0, 0, 0], "direction": [0, 1, 0]}}, "sketch_id": "shaft_profile"},
|
||||
{"id": "coaxial_bore", "atomic_id": "hole_blind", "depends_on": ["shaft_add"], "params": {"diameter_mm": 4, "depth_mm": 35, "positions": [{"mm": [0, 0, 0]}], "host_face": {"frame": end_plane}}, "sketch_id": "shaft_profile"},
|
||||
])
|
||||
|
||||
|
||||
def bearing_housing_fixture() -> dict:
|
||||
return document("golden-bearing-housing", [
|
||||
{"id": "base", "workplane": workplane(), "profile": {"type": "rectangle", "center": [0, 0], "width_mm": 100, "height_mm": 60}},
|
||||
{"id": "housing", "workplane": workplane(8), "profile": {"type": "circle", "radius_mm": 28}},
|
||||
{"id": "base_holes", "workplane": workplane(8), "profile": {"type": "circle_grid", "radius_mm": 4, "count_x": 2, "count_y": 2, "spacing_x_mm": 80, "spacing_y_mm": 40, "center_mm": [0, 0]}},
|
||||
], [
|
||||
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 8}, "sketch_id": "base"},
|
||||
{"id": "housing_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "params": {"distance_mm": 25}, "sketch_id": "housing"},
|
||||
{"id": "bearing_seat", "atomic_id": "hole_counterbore", "depends_on": ["housing_add"], "params": {"diameter_mm": 12, "depth_mm": 25, "counterbore_diameter_mm": 30, "counterbore_depth_mm": 10, "positions": [{"mm": [0, 0, 0]}], "host_face": {"frame": workplane(33)}}, "sketch_id": "housing"},
|
||||
{"id": "base_mount_holes", "atomic_id": "extrude_cut_blind", "depends_on": ["bearing_seat"], "params": {"distance_mm": 8, "reverse": True}, "sketch_id": "base_holes"},
|
||||
])
|
||||
|
||||
|
||||
def hex_nut_fixture() -> dict:
|
||||
return document("golden-hex-nut", [
|
||||
{"id": "hex", "workplane": workplane(), "profile": {"type": "polygon", "vertices": [[10, 0], [5, 8.660254], [-5, 8.660254], [-10, 0], [-5, -8.660254], [5, -8.660254]]}},
|
||||
{"id": "thread_bore", "workplane": workplane(10), "profile": {"type": "circle", "radius_mm": 3}},
|
||||
], [
|
||||
{"id": "nut_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 10}, "sketch_id": "hex"},
|
||||
{"id": "m6_bore", "atomic_id": "extrude_cut_blind", "depends_on": ["nut_add"], "params": {"distance_mm": 10, "reverse": True}, "sketch_id": "thread_bore"},
|
||||
])
|
||||
|
||||
|
||||
GOLDEN_CASES = (
|
||||
("带沉孔和孔阵列的安装底板", mounting_plate_fixture, {"planning/mounting-plate", "atomic/counterbored-hole-creation", "atomic/pattern-holes-from-datum"}, {"atomic/pattern-holes-from-datum": "expanded"}),
|
||||
("带槽和对称孔的安装支架", mounting_bracket_fixture, {"planning/mounting-bracket", "functional/slotted-adjustment-feature", "functional/symmetric-feature-layout"}, {"atomic/pattern-holes-from-datum": "expanded"}),
|
||||
("带螺栓圆的法兰", flange_fixture, {"planning/flange", "functional/flange-bolt-circle"}, {"functional/flange-bolt-circle": "expanded"}),
|
||||
("带回转体同轴孔的阶梯轴", shaft_fixture, {"planning/simple-shaft", "functional/axisymmetric-revolve-strategy", "atomic/coaxial-bore-rule"}, {}),
|
||||
("带轴承座孔和底座孔的轴承座", bearing_housing_fixture, {"planning/bearing-housing", "functional/bearing-bore-seat"}, {}),
|
||||
("M6 六角螺母", hex_nut_fixture, {"planning/hexagonal-nut", "atomic/threaded-hole-creation"}, {"atomic/threaded-hole-creation": "approximated"}),
|
||||
)
|
||||
|
||||
|
||||
class PartSkillLibraryTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.library = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
|
||||
def test_english_and_chinese_triggers(self) -> None:
|
||||
english = self.library.select("Create a mounting plate with counterbored hole pattern")
|
||||
chinese = self.library.select("带沉孔和孔阵列的安装底板")
|
||||
|
||||
self.assertIn("planning/mounting-plate", english["skill_ids"])
|
||||
self.assertIn("planning/mounting-plate", chinese["skill_ids"])
|
||||
self.assertIn("atomic/counterbored-hole-creation", chinese["skill_ids"])
|
||||
|
||||
def test_exclusions_and_selection_limits(self) -> None:
|
||||
selection = self.library.select("Create a flange nut with an M6 threaded hole")
|
||||
|
||||
self.assertNotIn("planning/flange", selection["skill_ids"])
|
||||
self.assertLessEqual(len(selection["planning_ids"]), 1)
|
||||
self.assertLessEqual(len(selection["support_ids"]), 3)
|
||||
self.assertEqual(len(selection["skill_ids"]), len(set(selection["skill_ids"])))
|
||||
|
||||
def test_inheritance_and_primary_family_conflict(self) -> None:
|
||||
inherited = ["planning/mounting-plate", "atomic/counterbored-hole-creation"]
|
||||
addition = self.library.select("Add one M6 threaded hole", inherited)
|
||||
conflict = self.library.select("Add a flange bolt circle", inherited)
|
||||
replacement = self.library.select("Replace the whole part with a flange bolt circle", inherited)
|
||||
|
||||
self.assertIn("planning/mounting-plate", addition["skill_ids"])
|
||||
self.assertIn("atomic/threaded-hole-creation", addition["skill_ids"])
|
||||
self.assertEqual(conflict["conflict"]["current"], "planning/mounting-plate")
|
||||
self.assertEqual(conflict["conflict"]["matched"], "planning/flange")
|
||||
self.assertEqual(replacement["planning_ids"], ["planning/flange"])
|
||||
self.assertIsNone(replacement["conflict"])
|
||||
|
||||
def test_catalog_and_bridge_files_are_complete(self) -> None:
|
||||
payload = json.loads((PART_SKILL_ROOT / "catalog.json").read_text(encoding="utf-8"))
|
||||
categories = {kind: 0 for kind in ("planning", "functional", "atomic")}
|
||||
for skill in payload["skills"]:
|
||||
categories[skill["kind"]] += 1
|
||||
self.assertTrue((PART_SKILL_ROOT / skill["bridge"]).is_file())
|
||||
self.assertTrue((PART_SKILL_ROOT / skill["source"]).is_file())
|
||||
self.assertTrue(skill["triggers"])
|
||||
self.assertTrue(skill["capability_translation_rules"])
|
||||
self.assertEqual(categories, {"planning": 6, "functional": 7, "atomic": 8})
|
||||
self.assertTrue((PART_SKILL_ROOT / "LICENSE").is_file())
|
||||
self.assertTrue((PART_SKILL_ROOT / "PROVENANCE.md").is_file())
|
||||
|
||||
def test_audit_records_exact_omitted_and_blocked_capability_states(self) -> None:
|
||||
fixture = mounting_plate_fixture()
|
||||
exact = self.library.audit(self.library.select("安装底板"), fixture)
|
||||
finishing = self.library.audit(self.library.select("edge treatment"), fixture)
|
||||
blocked = self.library.audit(self.library.select("flange bolt circle"), mounting_plate_fixture())
|
||||
|
||||
exact_statuses = {item["skill_id"]: item["status"] for item in exact["capability_translations"]}
|
||||
finishing_statuses = {item["skill_id"]: item for item in finishing["capability_translations"]}
|
||||
blocked_statuses = {item["skill_id"]: item["status"] for item in blocked["capability_translations"]}
|
||||
self.assertEqual(exact_statuses["planning/mounting-plate"], "exact")
|
||||
self.assertEqual(finishing_statuses["atomic/fillet-chamfer-last"]["status"], "omitted")
|
||||
self.assertEqual(finishing_statuses["atomic/fillet-chamfer-last"]["translation"], "selector_unavailable")
|
||||
self.assertEqual(blocked_statuses["functional/flange-bolt-circle"], "blocked")
|
||||
|
||||
|
||||
class AgentPartSkillTests(unittest.TestCase):
|
||||
def test_agent_stream_selects_and_injects_part_skill_before_first_model_call(self) -> None:
|
||||
class CaptureAgent(AgentService):
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.first_messages: list[dict[str, object]] = []
|
||||
|
||||
async def _complete(self, messages: list[dict[str, object]], *args: object, **kwargs: object) -> dict[str, object]:
|
||||
self.first_messages = [dict(message) for message in messages]
|
||||
return {"choices": [{"message": {"role": "assistant", "content": "已分析", "tool_calls": []}}]}
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self._settings(Path(directory))
|
||||
library = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
agent = CaptureAgent(settings, WorkspaceStore(settings), CdslLibrary(settings), library)
|
||||
message = ChatMessage(id="user_1", role="user", parts=[MessagePart(type="text", text="生成一个法兰螺栓圆")])
|
||||
|
||||
async def consume() -> None:
|
||||
async for _ in agent.stream([message], None, None):
|
||||
pass
|
||||
|
||||
asyncio.run(consume())
|
||||
self.assertIn("[planning/flange]", str(agent.first_messages[0]["content"]))
|
||||
self.assertIn("[functional/flange-bolt-circle]", str(agent.first_messages[0]["content"]))
|
||||
|
||||
def test_prompt_contains_bridge_without_adding_tools(self) -> None:
|
||||
library = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
selection = library.select("Create a flange with a bolt circle")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self._settings(Path(directory))
|
||||
prompt = system_prompt(settings, "Create a flange with a bolt circle", part_skill_context=library.render_context(selection))
|
||||
|
||||
self.assertIn("[planning/flange]", prompt)
|
||||
self.assertIn("circular-pattern atomic", prompt)
|
||||
self.assertEqual([tool["function"]["name"] for tool in TOOL_SCHEMAS], ["search_cdsl_library", "read_cdsl_reference", "propose_design_intent", "read_current_cdsl", "generate_cdsl_model"])
|
||||
|
||||
def test_generation_persists_assumptions_and_selected_skills(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = self._settings(Path(directory))
|
||||
store = WorkspaceStore(settings)
|
||||
library = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
agent = AgentService(settings, store, CdslLibrary(settings), library)
|
||||
request = "M6 六角螺母"
|
||||
selection = library.select(request)
|
||||
engine = load_engine(settings)
|
||||
intent = engine.design_intent_from_cdsl(hex_nut_fixture(), request=request)
|
||||
intent_state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""}
|
||||
planned, _ = asyncio.run(agent._run_tool(
|
||||
"propose_design_intent",
|
||||
{"intent": intent, "summary": "M6 nut plan", "assumptions": []},
|
||||
"",
|
||||
request,
|
||||
[],
|
||||
part_skill_selection=selection,
|
||||
intent_state=intent_state,
|
||||
))
|
||||
self.assertTrue(planned["ok"])
|
||||
result, generated = asyncio.run(agent._run_tool(
|
||||
"generate_cdsl_model",
|
||||
{"design_intent_id": planned["design_intent_id"], "cdsl": hex_nut_fixture(), "summary": "M6 nut", "assumptions": ["M6 thread is represented as a cylindrical bore"]},
|
||||
planned["task_id"],
|
||||
request,
|
||||
["reference-fixture"],
|
||||
part_skill_selection=selection,
|
||||
intent_state=intent_state,
|
||||
))
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertIsNotNone(generated)
|
||||
task = store.read_task(str(generated["task_id"]))
|
||||
revision = task["revisions"][0]
|
||||
audit = json.loads(store.artifact_path(task["task_id"], revision["part_skills_path"]).read_text(encoding="utf-8"))
|
||||
report = json.loads(store.artifact_path(task["task_id"], revision["report_path"]).read_text(encoding="utf-8"))
|
||||
self.assertIn("M6 thread is represented as a cylindrical bore", audit["assumptions"])
|
||||
self.assertIn("atomic/threaded-hole-creation", revision["part_skill_ids"])
|
||||
self.assertEqual(report["generation_context"]["cdsl_reference_ids"], ["reference-fixture"])
|
||||
|
||||
@staticmethod
|
||||
def _settings(root: Path) -> Settings:
|
||||
provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),))
|
||||
return Settings(
|
||||
task_root=root / "tasks",
|
||||
conversation_root=root / "conversations",
|
||||
library_root=BACKEND / "cdsl_library",
|
||||
engine_root=BACKEND / "engine" / "cdsl_engine",
|
||||
llm_base_url=provider.base_url,
|
||||
llm_api_key=provider.api_key,
|
||||
llm_model="test-model",
|
||||
llm_timeout_s=1,
|
||||
default_provider_id="test",
|
||||
providers=(provider,),
|
||||
)
|
||||
|
||||
|
||||
class PartSkillBuildAndGoldenTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.settings = AgentPartSkillTests._settings(Path(tempfile.mkdtemp()))
|
||||
cls.engine = load_engine(cls.settings)
|
||||
cls.library = PartSkillLibrary(PART_SKILL_ROOT)
|
||||
|
||||
def test_success_and_failure_revisions_always_write_part_skill_audit(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
settings = AgentPartSkillTests._settings(Path(directory))
|
||||
store = WorkspaceStore(settings)
|
||||
selection = self.library.select("带螺栓圆的法兰")
|
||||
audit = self.library.audit(selection, flange_fixture(), ["Explicit circle expansion"])
|
||||
success = build_revision(
|
||||
settings=settings, store=store, task_id=None, request="带螺栓圆的法兰", cdsl=flange_fixture(),
|
||||
reference_ids=["flange-reference"], summary="flange", part_skills=audit,
|
||||
generation_assumptions=["Explicit circle expansion"],
|
||||
)
|
||||
success_audit = json.loads(store.artifact_path(success["task_id"], success["part_skills_path"]).read_text(encoding="utf-8"))
|
||||
success_report = json.loads(store.artifact_path(success["task_id"], success["report_path"]).read_text(encoding="utf-8"))
|
||||
self.assertEqual(success_audit["skill_ids"], audit["skill_ids"])
|
||||
self.assertIn("generation_context", success_report)
|
||||
|
||||
invalid = copy.deepcopy(flange_fixture())
|
||||
invalid["features"][0]["atomic_id"] = "not_supported"
|
||||
failed_audit = self.library.audit(selection, invalid, [])
|
||||
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
|
||||
build_revision(
|
||||
settings=settings, store=store, task_id=success["task_id"], request="bad flange", cdsl=invalid,
|
||||
reference_ids=[], summary="bad", part_skills=failed_audit, generation_assumptions=[],
|
||||
)
|
||||
task = store.read_task(success["task_id"])
|
||||
failed = task["revisions"][-1]
|
||||
self.assertEqual(failed["status"], "failed")
|
||||
self.assertTrue(store.artifact_path(task["task_id"], failed["part_skills_path"]).is_file())
|
||||
report = json.loads(store.artifact_path(task["task_id"], failed["report_path"]).read_text(encoding="utf-8"))
|
||||
self.assertEqual(report["generation_context"]["part_skill_ids"], audit["skill_ids"])
|
||||
legacy_report = {"engine_result": {"engine": "cdsl_only"}}
|
||||
self.assertEqual(legacy_report.get("generation_context", {}), {})
|
||||
|
||||
def test_golden_part_families_validate_preflight_rebuild_and_audit(self) -> None:
|
||||
for request, factory, expected_skills, expected_statuses in GOLDEN_CASES:
|
||||
with self.subTest(request=request):
|
||||
cdsl = factory()
|
||||
selection = self.library.select(request)
|
||||
self.assertTrue(expected_skills.issubset(selection["skill_ids"]))
|
||||
validate_cdsl(cdsl, self.engine)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
step_path = Path(directory) / "model.step"
|
||||
result = self.engine.run_cdsl_only(copy.deepcopy(cdsl), step_path)
|
||||
self.assertEqual(result["engine"], "cdsl_only")
|
||||
self.assertTrue(step_path.is_file())
|
||||
self.assertGreater(step_path.stat().st_size, 0)
|
||||
audit = self.library.audit(selection, cdsl, ["golden fixture"])
|
||||
statuses = {item["skill_id"]: item["status"] for item in audit["capability_translations"]}
|
||||
for skill_id, status in expected_statuses.items():
|
||||
self.assertEqual(statuses[skill_id], status)
|
||||
self.assertTrue(audit["evidence"]["features"])
|
||||
self.assertTrue(audit["evidence"]["profiles"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user