bench: matrix v6 adds arch_roundtrip; four architectural correctness checks; the live arch smoke

The evidence for track F, in the shape tracks A, E and B + G used.

- benchmarks/tasks_v6.py: 19 tasks, DEFAULT_MATRIX = "v6"; v5 to v2 stay
  addressable. arch_roundtrip draws the example plan with
  arch_plan_from_spec and reads it back without the spec: rooms_detect must
  find each labelled room within 0.1 % of its label and of the net floor
  computed by hand (4825 x 5750 and 3825 x 5750), the schedules must list
  D1, D2, W1, W2, and the plan is gated on a zero-issue critique and a
  finalize score of at least 90. A test removes one room label and the gate
  fails naming arch_room_unlabelled.
- Four correctness checks, every number worked out by hand in the docstring:
  L / T / X junction corners and wall areas, a 4 x 5 m room labelled 20.00 m2
  rather than its 21.84 m2 axis area, a door interrupting both faces, and a
  plan of plain lines read as 20 and 15 m2 at confidence 0.6. The published
  A/B report is re-run over 43 checks: 26 / 43 -> 43 / 43, zero regressed.
- scripts/smoke_arch_com.py runs the whole plan once on a live seat and
  closes every document it opens; the CHANGELOG records what it returned.
- The token suite measures a TOOL_PACKS=core,arch row; check_doc_numbers
  derives the task count from the v6 matrix; CLAUDE.md, both READMEs and
  the spec status line say what shipped.
- fix(com): the first live smoke exited 1 - the room reader found 0 wall
  segments. Measured on AutoCAD 2026 (2026-09-23): with a makepy cache,
  ModelSpace.Item(i) returns the gen_py IAcadEntity wrapper
  (type(item).__name__ == "IAcadEntity") and item.StartPoint on an
  AcDbLine raises AttributeError, so every entity_list row lost its
  type-specific properties (28 wall LINEs, 0 with start/end). _entity_info
  now re-dispatches exactly that wrapper through _com_unnarrow; after the
  fix 38 of 53 rows carry start/end and the smoke exits 0 with both rooms
  at 27 743 750 / 21 993 750 mm2 and 0 critique issues. Re-dispatch
  measured at ~1.7 ms per entity (dynamic vs gencache alike).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
Umut CAN
2026-09-23 23:35:46 +03:00
co-authored by Claude Opus 5.5
parent 6e004cc658
commit 0abbe55041
20 changed files with 952 additions and 53 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ def test_correctness_suite_has_the_settings_checks():
):
assert CHECKS[name][1] == "Settings"
# v4 left the suite at 32; tracks B + G grew it to 39 (tests/test_benchmark_v5.py).
assert len(CHECKS) == 39
assert len(CHECKS) == 43
@pytest.mark.asyncio # asyncio_mode is strict; the four tests above are sync
+1 -1
View File
@@ -48,7 +48,7 @@ def test_the_suite_carries_the_mechanical_and_sheet_checks():
assert CHECKS[name][1] == "Mechanical"
assert CHECKS["sheet_frame_iso5457_a3"][1] == "Sheet"
assert CHECKS["bom_balloon_link"][1] == "Sheet"
assert len(CHECKS) == 39
assert len(CHECKS) == 43
@pytest.mark.asyncio
+119
View File
@@ -0,0 +1,119 @@
"""The v6 task matrix: v5 plus what track F added.
``arch_roundtrip`` is track F's evidence - a two-room plan drawn from one
spec, read back off the drawing by the room reader that never sees the spec,
and gated on the numbers the design spec fixed in advance: every room's
detected area within 0.1 % of its label's and of the hand-computed net floor,
the schedules listing the drawn tags, ``drawing_critique(focus=None)`` at
zero and the finalize score at least 90. The published competitor reports
were never asked it and the chart shows them ``not_run``.
"""
from __future__ import annotations
import pytest
from benchmarks.tasks_v5 import TASKS_V5
from benchmarks.tasks_v6 import DEFAULT_MATRIX, MATRICES, NEW_TASKS_V6, TASKS_V6, task_by_id
def test_v6_extends_v5_without_touching_it():
assert TASKS_V6[: len(TASKS_V5)] == TASKS_V5
assert [t.task_id for t in NEW_TASKS_V6] == ["arch_roundtrip"]
assert len(TASKS_V6) == 19
assert MATRICES["v5"] == TASKS_V5 and MATRICES["v6"] == TASKS_V6
assert DEFAULT_MATRIX == "v6"
assert task_by_id("arch_roundtrip").category == "arch"
def test_the_adapter_implements_every_v6_task():
from benchmarks.adapters.autocad_mcp_pro import AutoCADMCPProAdapter
for task in TASKS_V6:
assert hasattr(AutoCADMCPProAdapter, f"_task_{task.task_id}"), task.task_id
def test_the_hand_areas_are_the_net_floor_not_the_axis_area():
from benchmarks.adapters.autocad_mcp_pro import ROUNDTRIP_AREAS_MM2
assert ROUNDTRIP_AREAS_MM2 == {"01": 27_743_750.0, "02": 21_993_750.0}
# the axis areas the rooms would report if the wall thickness were ignored
assert 5000.0 * 6000.0 not in ROUNDTRIP_AREAS_MM2.values()
assert 4000.0 * 6000.0 not in ROUNDTRIP_AREAS_MM2.values()
def test_the_suite_carries_the_four_architectural_checks():
from benchmarks.correctness_suite import CHECKS
for name in (
"arch_junction_l_t_x",
"arch_room_area_net",
"arch_opening_cuts_wall",
"arch_rooms_detect_foreign",
):
assert CHECKS[name][1] == "Architecture"
assert len(CHECKS) == 43
@pytest.mark.asyncio
async def test_arch_roundtrip_passes_headlessly(tmp_path):
"""The benchmark task itself, run once here so a regression in the wall
engine, the room reader, the schedules or the critique fails the suite and
not only the matrix."""
from benchmarks.adapters.autocad_mcp_pro import AutoCADMCPProAdapter
adapter = AutoCADMCPProAdapter(backend="ezdxf")
await adapter.setup(tmp_path)
try:
await adapter._reset()
passed, metrics, _artifacts = await adapter._task_arch_roundtrip()
finally:
await adapter.cleanup()
assert passed, metrics
assert metrics["critique_issues"] == 0, metrics["critique_focuses"]
assert metrics["score"] >= 90.0
assert metrics["door_tags"] == ["D1", "D2"] and metrics["window_tags"] == ["W1", "W2"]
for number in ("01", "02"):
assert metrics["rooms"][number]["agrees"], metrics["rooms"]
@pytest.mark.asyncio
async def test_arch_roundtrip_fails_loudly_when_a_room_label_is_removed(tmp_path, monkeypatch):
"""The gate has to be able to fail. One room left without its label is
enough: the reader still finds the face, the critique names
arch_room_unlabelled, and the room is missing from the area comparison."""
from benchmarks.adapters.autocad_mcp_pro import AutoCADMCPProAdapter
from engineering.arch import rooms
real = rooms.label_room
async def skip_the_hall(backend, room_dict, **kw):
if room_dict.get("number") == "02":
return {"ok": True, "skipped": True}
return await real(backend, room_dict, **kw)
monkeypatch.setattr(rooms, "label_room", skip_the_hall)
adapter = AutoCADMCPProAdapter(backend="ezdxf")
await adapter.setup(tmp_path)
try:
await adapter._reset()
passed, metrics, _artifacts = await adapter._task_arch_roundtrip()
finally:
await adapter.cleanup()
assert passed is False
assert "arch_room_unlabelled" in metrics["critique_focuses"]
assert "02" not in metrics["rooms"]
@pytest.mark.asyncio
async def test_the_net_area_check_measures_the_label():
from benchmarks import correctness_suite
assert await correctness_suite.arch_room_area_net() is True
@pytest.mark.asyncio
async def test_the_foreign_plan_check_passes():
from benchmarks import correctness_suite
assert await correctness_suite.arch_rooms_detect_foreign() is True
+53
View File
@@ -568,6 +568,59 @@ def test_a_dimension_reports_where_its_text_stands():
assert props["text_position"] == [181.0, 230.0]
class IAcadEntity:
"""What pywin32 hands back for ``ModelSpace.Item(i)`` once a makepy cache
for the AutoCAD type library exists: acax25enu.tlb declares the result
``IAcadEntity*``, so the wrapper is the gen_py class of that name and it
exposes only the entity base's members. Measured on AutoCAD 2026
(2026-09-23, scripts/smoke_arch_com.py): ``type(item).__name__`` is
``"IAcadEntity"`` and ``item.StartPoint`` on an AcDbLine raises
``AttributeError: '<win32com.gen_py.AutoCAD 2025 Type Library.IAcadEntity
instance>' object has no attribute 'StartPoint'``. The concrete object is
reachable only through ``_oleobj_``."""
BASE_MEMBERS = frozenset(
{"ObjectName", "Handle", "Layer", "color", "Linetype", "Visible", "GetBoundingBox"}
)
def __init__(self, raw):
object.__setattr__(self, "_oleobj_", raw)
def __getattr__(self, key):
if key in self.BASE_MEMBERS:
return getattr(self._oleobj_, key)
raise AttributeError(
"'<win32com.gen_py.AutoCAD 2025 Type Library.IAcadEntity instance>' "
f"object has no attribute {key!r}"
)
def test_a_line_listed_through_a_narrowed_wrapper_keeps_its_endpoints():
"""``entity_list`` reads every entity through ``ModelSpace.Item``. With a
makepy cache the LINE rows lost ``start``/``end`` (only the bounding box
survived), so on the live seat the room reader found 0 wall segments and
``arch_plan_from_spec`` refused its first room label."""
from backends.com_backend import _entity_info
raw = _Entity(
"AcDbLine",
GetBoundingBox=lambda: ((125.0, 125.0, 0.0), (1000.0, 125.0, 0.0)),
StartPoint=(125.0, 125.0, 0.0),
EndPoint=(1000.0, 125.0, 0.0),
Length=875.0,
Handle="2B0",
Layer="A-WALL-E-N",
color=256,
Linetype="ByLayer",
Visible=True,
)
info = _entity_info(IAcadEntity(raw))
assert info.type == "LINE" and info.layer == "A-WALL-E-N"
assert info.properties["start"] == [125.0, 125.0]
assert info.properties["end"] == [1000.0, 125.0]
assert info.properties["length"] == 875.0
def test_com_geometry_bbox_is_the_drawn_body_without_the_attribute():
from backends.com_backend import _entity_info
+23 -12
View File
@@ -161,25 +161,36 @@ def test_the_cache_arithmetic_follows_the_published_multipliers():
# -- lane 1: idle cost ------------------------------------------------------
def test_the_idle_lane_covers_the_four_advertised_surfaces(report):
"""Default, lean, search - and TOOL_PACKS=core,mech, the mechanical client
that never opens a P&ID, a live seat or a floor plan: the default surface
minus exactly the pid, settings and arch packs (track F added `arch`)."""
def test_the_idle_lane_covers_the_five_advertised_surfaces(report):
"""Default, lean, search - and the two single-vertical clients:
TOOL_PACKS=core,mech (the default surface minus exactly the pid and settings
packs, and the arch pack) and TOOL_PACKS=core,arch (minus pid, settings and
mech)."""
variants = {row["variant"]: row for row in report["idle"]}
assert set(variants) == {"default", "lean", "packs_core_mech", "search"}
assert set(variants) == {"default", "lean", "packs_core_mech", "packs_core_arch", "search"}
assert variants["default"]["advertised_tools"] > 100
assert variants["lean"]["advertised_tools"] == len(server.LEAN_TOOL_NAMES)
assert variants["search"]["advertised_tools"] == 2
dropped = sum(len(server.PACK_TOOL_NAMES[pack]) for pack in ("pid", "settings", "arch"))
packs = server.PACK_TOOL_NAMES
assert variants["packs_core_mech"]["advertised_tools"] == (
variants["default"]["advertised_tools"] - dropped
variants["default"]["advertised_tools"]
- len(packs["pid"])
- len(packs["settings"])
- len(packs["arch"])
)
assert (
variants["default"]["chars"]
> variants["packs_core_mech"]["chars"]
> variants["lean"]["chars"]
> variants["search"]["chars"]
assert variants["packs_core_arch"]["advertised_tools"] == (
variants["default"]["advertised_tools"]
- len(packs["pid"])
- len(packs["settings"])
- len(packs["mech"])
)
for narrowed in ("packs_core_mech", "packs_core_arch"):
assert (
variants["default"]["chars"]
> variants[narrowed]["chars"]
> variants["lean"]["chars"]
> variants["search"]["chars"]
)
def test_the_idle_lane_separates_the_wire_payload_from_what_the_model_pays(report):