diff --git a/docs/superpowers/specs/2026-05-27-assembly-add-mate-design.md b/docs/superpowers/specs/2026-05-27-assembly-add-mate-design.md
index b0b8ea3..1646c7a 100644
--- a/docs/superpowers/specs/2026-05-27-assembly-add-mate-design.md
+++ b/docs/superpowers/specs/2026-05-27-assembly-add-mate-design.md
@@ -1,7 +1,20 @@
# Дизайн: сопряжение компонентов сборки (assembly_add_mate) через API7
**Дата:** 2026-05-27
-**Статус:** спайк зелёный; ревью Codex учтено (8 замечаний); реализуется
+**Статус:** реализовано и проверено (2 интеграционных + 14 unit, всего 154); ревью Codex спека (8) и реализации (9) учтено
+
+## Правки по ревью реализации Codex (9 замечаний)
+
+- **#1 `Valid==false` → ошибка** (а не «успех с valid=false»): после Update+Rebuild проверяем
+ `mate.Valid`, иначе бросаем (срабатывает откат). `AddedMate.Valid` теперь всегда true при успехе.
+ Это же косвенно ловит грани одного компонента (#5) — мат выродится.
+- **#6/#7 distance `value` > 0** (не ≥0): нулевой зазор вырождается в coincidence →
+ `RequireMateValue` требует строго `> 0`; unit-кейс `value=0` бросает.
+- **#8 негативный интеграционный тест**: точка в пустоте → грань не найдена → ошибка.
+- **#2 откат best-effort** оставлен намеренно (исходное исключение важнее ошибки отката; паттерн
+ `HoleService`). **#3** `mates.Add ?? throw` до try корректно (null = ничего не добавлено).
+ **#4** «первый IFace» оставлен (консистентно с HoleService/move_face). **#9** точечный release
+ RCW отклонён (конвенция v2-2).
## Решения по ревью Codex (важные правки к дизайну ниже)
diff --git a/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs b/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs
index 197855b..278e9cb 100644
--- a/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs
+++ b/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs
@@ -123,8 +123,12 @@ public sealed class AssemblyService
"Update сопряжения вернул FALSE — связь не наложена (несовместимые грани/значение?).");
if (!top.RebuildModel(true))
throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после сопряжения.");
+ if (!mate.Valid)
+ throw new InvalidOperationException(
+ "Сопряжение построено, но вырождено (Valid=false) — вероятно, грани несовместимы " +
+ "или принадлежат одному компоненту. Проверьте выбор граней.");
- return new AddedMate { Type = type, Valid = mate.Valid };
+ return new AddedMate { Type = type, Valid = true };
}
catch
{
diff --git a/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs b/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs
index 68c44c7..c6bfd8c 100644
--- a/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs
+++ b/src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs
@@ -38,13 +38,15 @@ public static class AssemblyValidation
///
/// Проверить числовое значение сопряжения. Если значение требуется (,
- /// напр. расстояние) — оно должно быть конечным и неотрицательным.
+ /// напр. расстояние) — оно должно быть конечным и строго положительным (зазор 0 вырождается в
+ /// совпадение — для него есть отдельный тип).
///
public static void RequireMateValue(bool requiresValue, double value)
{
if (!requiresValue) return; // для типов без параметра значение игнорируется
- if (!double.IsFinite(value) || value < 0)
+ if (!double.IsFinite(value) || value <= 0)
throw new ArgumentOutOfRangeException(nameof(value),
- "Значение сопряжения (расстояние) должно быть конечным и неотрицательным.");
+ "Значение сопряжения (расстояние) должно быть конечным и > 0 " +
+ "(для нулевого зазора используйте тип coincidence).");
}
}
diff --git a/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs b/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs
index b646961..2e09238 100644
--- a/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs
+++ b/tests/Kompas.Mcp.Tests/AssemblyValidationTests.cs
@@ -77,10 +77,11 @@ public sealed class AssemblyValidationTests
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(-1)]
+ [InlineData(0)] // нулевой зазор вырождается в coincidence → ошибка
public void RequireMateValue_throws_on_bad_required_value(double value)
=> Assert.Throws(() => AssemblyValidation.RequireMateValue(true, value));
[Fact]
- public void RequireMateValue_accepts_nonnegative_required_value()
+ public void RequireMateValue_accepts_positive_required_value()
=> Assert.Null(Record.Exception(() => AssemblyValidation.RequireMateValue(true, 30)));
}
diff --git a/tests/Kompas.Mcp.Tests/Integration/AssemblyMateTests.cs b/tests/Kompas.Mcp.Tests/Integration/AssemblyMateTests.cs
index d80a89f..174b57f 100644
--- a/tests/Kompas.Mcp.Tests/Integration/AssemblyMateTests.cs
+++ b/tests/Kompas.Mcp.Tests/Integration/AssemblyMateTests.cs
@@ -95,4 +95,17 @@ public sealed class AssemblyMateTests : IntegrationTestBase
}
finally { await _docs.CloseAsync(save: false); }
}
+
+ [Fact]
+ public async Task Mate_with_point_off_any_face_throws()
+ {
+ await SetupTwoCubesAsync();
+ try
+ {
+ // Вторая точка — в пустоте (далеко от обоих кубов): грань не найдена → понятная ошибка.
+ await Assert.ThrowsAsync(
+ () => _assembly.AddMateAsync(MateType.Coincidence, 20, 10, 10, 1000, 1000, 1000, value: 0));
+ }
+ finally { await _docs.CloseAsync(save: false); }
+ }
}