Навыки и README по итогам регресс-прогона

Снято два неверных утверждения kompas-3d: ширина линейна по widthFactor (замер даёт
+0.55 % коэффициента → +0.07 % ширины) и проекции #Спереди/#Сверху у model_snapshot,
которых у инструмента нет. Добавлено: габарит глифов вместо ячейки, миттер на острых
терминалах (t/sin(θ/2)), трекинг кратными пробелами, замер в черновом документе,
требование называть каждую операцию.

kompas-mcp-dev: правила формы (name у операции, ответ несёт данные для следующего шага),
проверенные COM-цепочки, делегирование субагенту cad-engineer. В бэклог CLAUDE.md —
отсутствие feature_delete/sketch_update и фильтра подмножества рёбер.
This commit is contained in:
2026-07-31 16:36:13 +03:00
parent 31b933e1cc
commit 5ef452ceb7
5 changed files with 86 additions and 16 deletions
+15
View File
@@ -53,6 +53,13 @@ description: Внутренняя методика доработки самог
текстом от `ToolErrorText.Describe` (Core/Startup: разворачивает `AggregateException`, склеивает
до трёх вложенных причин через «←»). Пишешь новое сообщение об ошибке — помни, что до агента
дойдёт именно оно.
- **У мутирующей операции есть `name` — имя узла в дереве построения.** Прокидывается до создания
объекта (`PartModeler.NewEntity` для API5, `IModelObject.Name` до `Update()` для API7-путей),
нормализуется в `FeatureName`. Заводишь новую операцию — добавь параметр сразу: дерево из
«Эскиз:1…Эскиз:9» нечитаемо ни человеку, ни агенту, вернувшемуся к модели.
- **Ответ операции несёт то, что нужно для СЛЕДУЮЩЕГО шага.** Надпись отдаёт габарит глифов
(`TextMetrics`), а не только ширину ячейки; примитив проверяется по объёму. Иначе агент строит
вслепую и пересобирает документ ради замера — цикл, который стоил регресс-прогону четырёх кругов.
- **Мутирующая операция сама дописывает итог проверки** через `AutoValidation.AnnotateAsync`
(`Core/Validation`). Переключатель — `set_auto_validate` и `KOMPAS_MCP_AUTOVALIDATE`; проверка
никогда не превращает удачную операцию в ошибку (сбой самой проверки уходит в примечание).
@@ -102,6 +109,14 @@ CI гоняет только `Category=Unit&Requires!=Windows` на Linux-ран
## Делегирование
**Прогон задачи целиком** — субагент **`cad-engineer`** (`.claude/agents/`, наследует модель; из
инструментов — только навыки, чтение и MCP `kompas`, ничего пишущего). Отладочный цикл навыка и
каталога: правишь `plugin/skills/*` или сервер → натравливаешь субагента на ту же задачу с нуля →
читаешь его протокол (вызовы, затыки, цитаты из навыка, чего не хватило в инструментах). Смысл именно
в чистом контексте: если задача решается только с твоими подсказками по ходу — значит, недоработан
навык, а не субагент. Правок он не делает принципиально, поэтому нехватка инструмента возвращается
находкой, а не самодельным обходом. В плагин не входит.
**Поиск по справке SDK** (сигнатуры, константы, перечисления, цепочки COM-вызовов) — субагент
**`kompas-sdk-research`** (Haiku, read-only). Справка живёт **не в репозитории**, а в RAG-базе
`kompas-sdk` (MCP-сервер, репозиторий `kompas-sdk-docs`) — грепать по проекту бесполезно, доступ к
@@ -219,6 +219,20 @@
`ksGetTextLengthFromReference(ref)` (advance width in mm, ≈ 10 % wider than the glyph bbox) and place the
text yourself; the dynamic arrays live in КОМПАС, so `ksDeleteArray()` them in a `finally`; an unknown
font name is silently substituted rather than failing; `height` sizes the cap+ascender, not the bbox.
- **Glyph bounding box (verified live)**: `ksConvertTextToCurve(ref)` **returns a new reference — the
curves**, and only that one gives the real box. `ksDocument2D.ksGetObjGabaritRect(reference, ksRectParam)`
(`ko_RectParam`; `GetpBot()`/`GetpTop()` → `ksMathPointParam.x/y`, **millimetres of the sketch**, not
sheet centimetres) on the *text* reference returns the cell (23.75 mm for Arial h=10 «HH») while the
same call on the *converted* reference returns the glyphs (19.97 mm, matching the extruded solid to
0.05 mm). Returns 0 rather than throwing when the object has no box — treat as «unknown», don't fail
the primitive. Test: `SketchTextTests.Reported_glyph_box_matches_the_extruded_geometry`.
- **Feature names in the tree (`name` on every mutating tool, verified live)**: API5 `ksEntity.name` is
read/write and must be assigned **before `Create()`** — `PartModeler.NewEntity(part, type, name)` does it
for every entity, sketches included. API7 objects created outside `ksPart` (`IElementaryBody` from
`ElementaryBodies.Add`, `IHole3D` from `Holes3D.Add`) take it via `IModelObject.Name` set before
`Update()`. Reading back is asymmetric: `ksFeature.name` (what `list_features`/`describe_model` walk) is
**read-only** — the tree shows the assigned name, but you cannot rename through `ksFeature`.
Test: `FeatureNamingTests`.
- **Elementary bodies / primitives (`primitive`, verified live)**: `(IPart7 as IModelContainer)
.ElementaryBodies.Add(ksObj3dTypeEnum)` → cast to `IBlockBySizes` / `ICylinder` / `ISphere` /
`IConeByHeight` → set sizes → `Update()`. Types: `o3d_BlockBySizes=668`, `o3d_Cylinder=663`,
+15 -1
View File
@@ -65,6 +65,13 @@ a *general* tool with tests, and describe the method in the `kompas-3d` skill.
arc/angular/rough dimensions (needs its own spike — `ILineDimension` has no `BaseObject`).
- **Assembly**: mate types beyond `coincidence`/`distance``parallel`, `perpendicular`, `concentric`,
`angle`, `tangency` (enum values exist in `MateType.cs`, not verified live).
- **Editing the tree** (raised by the `cad-engineer` regression run): there is no `feature_delete` /
`sketch_update`, so any fit-up of a sketch means `document_close(all)` + rebuilding from scratch —
four full rounds in that run. `IFeature7.Delete()` exists and `ksDocument3D.DeleteObject` is the API5
counterpart; neither is verified live yet.
- **Addressing a subset of edges/faces**: on a body with hundreds of edges `list_edges` is unusable by
answer size and there is no filter (by operation, by bounding window). Points now work in a batch
(`fillet_edge(points=[…])`), but discovery still has no cheap path.
- **Known caveat:** boss/cut direction on a *selected face* depends on the face-normal orientation —
`forward` may need flipping. Pick the direction from the `list_faces(index=…)` normal or a snapshot
before building.
@@ -152,7 +159,14 @@ junction by the `ReparsePoint` attribute rather than by resolving the target, wh
still works once the target is gone. No `post-checkout` hook on purpose — it fires after checkout already
broke, too late to help.
## Delegation subagent
## Delegation subagents
**`cad-engineer`** (`.claude/agents/`, model inherit — Skill/Read/Glob/Grep + the `kompas` MCP server,
no write access): runs a whole CAD task end-to-end from a clean context, building **only** through MCP
tools. It is a **debugging harness**, not a shortcut: this session fixes the skills and the server, then
re-runs the same task through the subagent and reads its protocol — calls made, dead ends, complaints
against the skill (quoted), complaints against the catalog. It can't patch anything, so a missing tool
comes back as a finding instead of a hand-made workaround. Project-local, never shipped in the plugin.
**`kompas-sdk-research`** (`.claude/agents/`, model **Haiku**, read-only — MCP `kompas-sdk` tools only):
finds an interface/method/enum/constant signature in the SDK knowledge base and returns a compressed
+5 -3
View File
@@ -82,8 +82,10 @@ kompas-mcp.exe --version # версия бинаря, КОМПАС для эт
Инструменты сгруппированы по смыслу операции, а не по способу вызова: тип задаётся параметром
(`extrude(mode=boss|cut)`, `hole(type=simple|counterbore|countersink|conic)`,
`pattern(kind=linear|circular)`), а объект — либо индексом из `list_faces`/`list_edges`,
либо точкой в мировых координатах. Эскиз строится списком примитивов за один вызов:
`sketch_create(plane|faceIndex, entities[])`.
либо точкой в мировых координатах (рёбра под скругление/фаску — и списком точек `points[]`,
чтобы N углов давали одну операцию). Эскиз строится списком примитивов за один вызов:
`sketch_create(plane|faceIndex, entities[])`. У каждой операции есть `name` — имя узла в дереве
построения; без него дерево состоит из безликих «Эскиз:1», «Элемент выдавливания:2».
Формообразование идёт двумя путями, и их сочетают: `primitive(kind=block|cylinder|sphere|cone,
result=new|union|subtract|intersect)` строит тело по размерам без эскиза (в том числе вычитает
@@ -93,7 +95,7 @@ result=new|union|subtract|intersect)` строит тело по размера
Примитивы эскиза: `line`, `circle`, `rectangle`, `arc`, `arc3points`, `ellipse`, `polyline`,
`polygon`, `spline`, `point`, `axis`, `text`. Надпись (`type=text` с `text`, `height`, `fontName`)
сразу переводится в кривые, поэтому её можно выдавливать — так делаются логотипы и гравировка;
в ответе возвращается фактическая длина строки, по ней надпись выравнивают. Параметры
в ответе возвращается габарит глифов (и отдельно ширина ячейки строки), по нему надпись выравнивают. Параметры
`extrude(thinThickness, thinSide)` превращают контур в стенку заданной толщины: `outward`
наращивает её наружу и даёт ободок-эквидистанту вокруг контура (например, подложку под надпись).
+37 -12
View File
@@ -49,7 +49,7 @@ MCP-сервер даёт **общие** операции КОМПАС (эски
- *Система/документы:* `kompas_connect`, `kompas_set_visible`, `kompas_status`, `set_auto_validate`; `document_create|open|save|close|active` (`document_save(path)` — сохранить новый документ или копию, без `path` — на прежнее место); `set_part_info(name, marking)` — наименование и обозначение детали.
- *Эскиз (три инструмента вместо шестнадцати):* `sketch_create` — основание задаётся `plane` (+`offset`) ЛИБО `faceIndex` ЛИБО точкой `x,y,z`, а геометрия сразу списком `entities[{type: line|circle|rectangle|arc|arc3points|ellipse|polyline|polygon|spline|point|axis|text, …}]`; `sketch_add` — дополнить открытый эскиз; `sketch_close`.
- *Формообразующие:* `primitive(kind=block|cylinder|sphere|cone, result=new|union|subtract|intersect)` — тело по размерам БЕЗ эскиза, `extrude(mode=boss|cut)`, `revolve(mode=boss|cut)`, `fillet_edge`/`chamfer_edge` (список `edgeIndices`одной операцией на все рёбра — либо точка), `shell`, `rib`, `sweep`, `loft`, `draft`, `rebuild`.
- *Формообразующие:* `primitive(kind=block|cylinder|sphere|cone, result=new|union|subtract|intersect)` — тело по размерам БЕЗ эскиза, `extrude(mode=boss|cut)`, `revolve(mode=boss|cut)`, `fillet_edge`/`chamfer_edge` (одной операцией на все рёбра: список `edgeIndices` ЛИБО список `points[{x,y,z}]`, либо одна точка), `shell`, `rib`, `sweep`, `loft`, `draft`, `rebuild`. У всех — `name`: имя операции в дереве построения.
- *Отверстия:* `hole(type=simple|counterbore|countersink|conic)` — грань по `faceIndex` (центр грани) либо точкой.
- *Массивы и зеркало:* `pattern(kind=linear|circular)`, `mirror` (без `featureIds` — всё тело, с ними — только эти операции).
- *Прямое редактирование (без дерева, в т.ч. импортированная B-rep):* `move_face` (сдвинуть грань на N мм по нормали; грань — `faceIndex` или точка), `split_solid_by_plane` (рассечь тело плоскостью), `move_body` (сдвинуть тело на вектор), `boolean_union` (объединить тела).
@@ -114,9 +114,16 @@ MCP-сервер; если инструмента под задачу нет, с
стоит безликая «Деталь», и она же уедет в штамп чертежа и в спецификацию; `describe_model` специально
отмечает деталь без наименования. Имя файла при сохранении этого НЕ заменяет — это разные свойства.
**Называй каждую операцию.** У `sketch_create`, `extrude`, `primitive`, `fillet_edge`, `hole`,
`pattern`, `shell` и остальных есть `name` — имя узла в дереве построения. Без него дерево выглядит
как «Эскиз:1, Эскиз:2, Элемент выдавливания:3», и по нему нельзя понять, что чем построено, —
ни человеку, ни тебе самому через сотню вызовов. Имя пиши по смыслу: «Контур плашки», «Рельеф букв
OldMan», «Карман под площадку», «Скругления углов плашки».
**Не разбивай построение на лишние вызовы.** Прямоугольник с четырьмя отверстиями — это ОДИН
`sketch_create` со списком из пяти примитивов, а не шесть вызовов. Скругление восьми рёбер — один
`fillet_edge(radius, edgeIndices=[…])`, а не восемь. Каждый лишний вызов — лишний шанс сбиться.
`fillet_edge(radius, edgeIndices=[…])` или `fillet_edge(radius, points=[…])`, а не восемь.
Каждый лишний вызов — лишний шанс сбиться.
## Надписи, логотипы и рельеф
@@ -125,16 +132,28 @@ MCP-сервер; если инструмента под задачу нет, с
`widthFactor` — сужение, `angle` — наклон строки). Она **сразу переводится в кривые**, поэтому
выдавливается как обычный контур: гравировка — `extrude(mode="cut")`, выпуклые буквы — `mode="boss"`.
- **Позиционирование — в два прохода, длина строки лишь для прикидки.** Ответ содержит длину
надписи в мм («надпись «OldMan» — длина 79.05 мм»), но это ширина С боковыми просветами, и
насколько она больше самих глифов — зависит от шрифта: замерено 1–2 % у наборных (Zilla Slab,
Bevan) и 79 % у скриптовых (Lobster, Pacifico). Поэтому: вставь надпись, выдави, прочитай
**фактический** габарит `describe_model(sections="box")` и уже по нему пересчитай точку вставки.
- **Ответ даёт ГАБАРИТ ГЛИФОВ — по нему и позиционируй.** «надпись «OldMan» — глифы X 1.00…87.93,
Y 12.14…28.94 (86.93 × 16.80 мм), ширина ячейки 94.57 мм». Первые числа — прямоугольник, реально
занятый буквами (совпадает с тем, что получится после выдавливания); *ширина ячейки* — шаг строки
с боковыми просветами, она больше на 1–2 % у наборных шрифтов (Zilla Slab, Bevan) и на 79 % у
скриптовых (Lobster, Pacifico). Вписываешь надпись в поле — считай по габариту, не по ячейке.
- **Мерить дешевле в черновом документе.** Пока деталь пуста, габарит надписи — это габарит всей
модели, и подбор кегля идёт без пересборки: `document_create part` → эскиз с текстом → прочитать
метрики → закрыть без сохранения. В собранном теле (сотни граней) выделить рельеф уже нечем.
- **`height` — это высота ПРОПИСНОЙ (cap height), а не габарит строки.** Замер на Zilla Slab при
`height=10`: «HH» → 9.97 мм, «hd» → 10.71 (восходящие выше прописной), «Hy» → 13.24 мм
(нижний выносной уходит на −3.27). Считаешь компоновку — закладывай выносные отдельно.
- **Ширина линейна** и по `height`, и по `widthFactor`: вписать строку в заданную ширину можно
одним пересчётом, без подбора вслепую.
- **Ширина линейна по `height`** (проверено до 0.01 мм) — им и подгоняй точный размер.
**По `widthFactor` линейности НЕТ:** замер даёт +0.55 % коэффициента → +0.07 % ширины
(КОМПАС квантует кегль). Поэтому `widthFactor` — грубая пропорция начертания, а не масштаб:
выставил его один раз, дальше набираешь ширину высотой.
- **Толщина каймы на острых терминалах больше, чем `thinThickness`.** Стенка `outward` обходит
контур с миттером: на остром окончании глифа (скриптовые «n», «l») габарит растёт на
`t / sin(θ/2)`, а не на `t` — замерено 2.11 мм при `t`=1.0 у Lobster. Считать «глиф + 2t» нельзя,
габарит после операции надо перечитывать.
- **Разрядки (трекинга) у примитива нет — её набирают пробелами.** Ширину пробела для конкретного
шрифта и кегля вычисляют двумя пробами: «EDITION» → 38.38 мм, «E D I T I O N» → 47.11 мм,
значит пробел 1.455 мм (Bevan, cap 5.5).
- **Подложка под надпись — тонкой стенкой, а не вторым эскизом.** `extrude(..., thinThickness=1.2,
thinSide="outward")` по контуру букв даёт ободок-эквидистанту вокруг них; выдавь ТОТ ЖЕ эскиз
дважды — сплошным на высоту подложки и стенкой наружу — и получишь силуэт надписи с равномерной
@@ -151,6 +170,10 @@ MCP-сервер; если инструмента под задачу нет, с
Zilla Slab «AB» (203 ребра): одиночное ребро скругляется R0.1 и R0.2, а `fillet_edge` сразу по
всем рёбрам отказывает при обоих радиусах (соседние скругления конфликтуют). Адресно выбрать
«только верхние» рёбра пока нечем — `list_edges` не отдаёт ни координат, ни принадлежности грани.
- **Углы вокруг надписи — одним `fillet_edge(points=[…])`.** На теле с сотнями рёбер список из
`list_edges` неподъёмен, но точки углов известны из построения: передай их списком, и все рёбра
уйдут в ОДНУ операцию дерева. Если ребра в части точек нет (типичный случай — угол «съеден»
каймой букв), ошибка перечислит ВСЕ такие точки сразу: убери их и повтори одним вызовом.
- **Шрифт обязан быть установлен в системе**, где работает КОМПАС: неизвестное имя молча
подменяется другим шрифтом, и надпись «поедет» по ширине без единого сообщения об ошибке.
@@ -166,9 +189,10 @@ MCP-сервер; если инструмента под задачу нет, с
компоненты пишутся как отдельные `.m3d` рядом; открывать деталь самостоятельным документом
(`OpenSourceDocument`). Гашение видимости компонента на снимок **не влияет** — изоляция так не делается.
4. **Осмотр детали — структурно:** `describe_model` (габарит по осям → какая ось «высота», МЦХ,
тела, топология). Грани/рёбра под операцию — `list_faces`/`list_faces(index=N)`, `measure`. `model_snapshot`
(или стандартные проекции `#Спереди/#Сверху/#Слева/#Справа/#Изометрия`) — **только** если нужен
визуально-пространственный контроль формы.
тела, топология). Грани/рёбра под операцию — `list_faces`/`list_faces(index=N)`, `measure`.
`model_snapshot` — **только** если нужен визуально-пространственный контроль формы; выбора вида
у него нет (снимок идёт с текущей ориентации окна, параметры — только разрешение и оттенки серого),
так что фронтальный контроль плоской детали им не сделать: сверяй числа.
5. **Модификация** на «тупой» импортированной B-rep (итог проверки читаешь в ответе каждой операции):
- *Простой случай* — `move_face`: сдвинуть плоскую грань на +N мм (наружу) или −N (внутрь);
грань бери по `faceIndex` из `list_faces`. ⚠ На сложном торце (с отверстием/пазом) FaceMover
@@ -198,6 +222,7 @@ MCP-сервер; если инструмента под задачу нет, с
версия из этой же папки), `document_save(path)` по этому пути не пройдёт — КОМПАС не перезаписывает
занятый файл. Освободить имя: `document_close(all=true)`. Проверяй, что файл на диске реально
обновился (время изменения), особенно когда перестраиваешь деталь поверх прежней.
Несуществующий каталог в пути — не помеха: `document_save` и `export_step` создают его сами.
- **Единицы — мм** (геометрия) и кг (масса). Локальные координаты эскиза ≠ мировые координаты модели.
## Открытые вопросы / границы