feat(mech): six mech_* critique focuses, the mech pack, lean 60, aliases and the counts

The four track-B/G branches are merged; this is the integration that makes the
25 new tools one surface instead of four.

- engineering/mech/critique.py: the six focuses of spec 13, dispatched the way
  the P&ID ones are. One index per critique run, shared through run_critique's
  context, so six focuses cost one read of the drawing; a drawing with no
  ACADMCP_MECH payload yields nothing at all, which is what keeps focus=None
  cheap for a P&ID sheet or a plain DXF. Each focus has a positive and a
  negative fixture.
  Two narrowings are in the module docstring rather than hidden:
  mech_view_misaligned checks the projection axis and not the quadrant,
  because the pinned payload carries no projection angle to check it against,
  and mech_duplicate_dimension catches two dimensions in the same place rather
  than a semantic restatement, because no engine exposes what a dimension
  measured.
- The index reads what the tools actually write, not only the plan's fixture
  shape. mech_part_draw anchors its part payload on a POINT (layer MECH) and
  lists every drawn view under "views"; the plan's index read INSERT/CIRCLE
  only and one "view" per payload, so it was silent on every part the server
  draws. It now reads POINT too, expands "views" into one entry per drawn view
  (the section check looks in the section's own WCS box, the alignment check
  compares frame_bbox centres), and pages entity_list instead of one call.
  Test: a part drawn by draw_part + add_view is read and passes all six; the
  same part with its hatch erased is reported.
- Live type names, measured on AutoCAD 2026 (2026-09-23, scratch document,
  closed): ObjectName AcDbBlockReference -> BLOCKREFERENCE, AcDbRotatedDimension
  -> ROTATEDDIMENSION, AcDbAlignedDimension -> ALIGNEDDIMENSION, AcDbPoint ->
  POINT with GetBoundingBox = the point. The index reads BLOCKREFERENCE as an
  INSERT and any *DIMENSION as a dimension; a wrapper test reports the ezdxf
  entities under exactly those measured names.
- mech_thread_unrepresented uses the view engine's own MINOR_RATIO, and checks
  the thin line ISO 6410 draws: the minor diameter of an external thread, the
  major of an internal one.
- mech_bom_balloon_mismatch: identical parts share one balloon (ISO 6433
  balloons a parts-list row), so a second M12x60 is covered by the first one's
  balloon instead of reported.
- TOOL_PACK_NAMES has "mech" (the 14 tools of SECTIONS 21-23). SECTION 24
  stays core: a frame, a title block, a parts list and an xref are universal
  drafting, not a vertical. Lean gains five and reaches 60 (54 with
  TOOL_PACKS=core).
- _GROUP_TAG_PRIORITY gains sheet directly after mech; the mech group label
  becomes "mechanical". SECTION 24's last four tools (xref_attach,
  xref_manage, image_attach, drawing_export_dwg) now carry the sheet tag, so
  group, pack and SECTION are one list: 229 tools in 25 groups, and
  engineering/blocks/entity_creation/drawing return to their pre-SECTION-24
  sizes.
- Ten golden tuning cases, two of them Turkish (with the diacritic spellings
  added to the alias records next to the ASCII-folded ones). The shared
  AutoCAD command set needed no change: the merge had already resolved TABLE,
  REVCLOUD, SECTIONPLANE, INSERT, ARRAY and SAVEAS, and nothing is stale.
- Inventory regenerated, the frozen group-size snapshot retaken from the live
  registry, and the README release-snapshot / pack / lean figures taken from
  scripts/check_doc_numbers.py rather than typed.

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 12:48:44 +03:00
co-authored by Claude Opus 5.5
parent a6c9c0d927
commit 3b033033f9
11 changed files with 1202 additions and 62 deletions
+32 -10
View File
@@ -691,6 +691,7 @@ async def _registered_tool_count() -> int | None:
_GROUP_TAG_PRIORITY = (
"pid",
"mech",
"sheet",
"style",
"pagesetup",
"environment",
@@ -719,7 +720,8 @@ _GROUP_TAG_PRIORITY = (
# Map the winning tag to a human-readable group label for the breakdown.
_GROUP_TAG_LABELS = {
"pid": "pid",
"mech": "mech",
"mech": "mechanical",
"sheet": "sheet",
"style": "styles",
"pagesetup": "page_setup",
"environment": "environment",
@@ -949,6 +951,17 @@ LEAN_TOOL_NAMES = frozenset(
# them on the lean surface; with S's three above, lean is 55.
"page_setup_apply",
"batch_plot",
# Mechanical + sheet (tracks B/G) — the five a lean client needs to
# draw a part and put it on a sheet. The catalogue, the annotation
# symbols and the delivery tools stay full-profile: a lean client
# draws the part and leaves the paperwork to a full one. The first
# three are `mech`-pack tools, so TOOL_PACKS=core hides them from a
# lean surface too; sheet_frame and titleblock_apply are core.
"mech_part_draw",
"mech_view_add",
"mech_dimension_part",
"sheet_frame",
"titleblock_apply",
}
)
@@ -1004,8 +1017,10 @@ PACK_TOOL_NAMES: dict[str, frozenset[str]] = {
"system_prompt_message",
}
),
# SECTIONS 21-23 - the mechanical part model, standard parts and
# mechanical annotation (tracks B+G).
# SECTIONS 21-23 — the mechanical part drawer, the standard-parts catalogue
# and the ISO annotation symbols (spec §11). SECTION 24 (the sheet, the
# parts list, xrefs and DWG) stays in `core`: a frame, a title block, a
# parts list and an xref are universal drafting, not a vertical.
"mech": frozenset(
{
"mech_part_draw",
@@ -5692,7 +5707,10 @@ async def drawing_finalize(
(iso128, layer_color, dim_overlap, untrimmed_corner, duplicate_entities, construction_left,
gdt, plus the P&ID focuses pid_dangling_line, pid_duplicate_tag,
pid_incompatible_connection, pid_untagged_instrument, pid_illegal_tag,
pid_unconnected_equipment — silent on a sheet with no P&ID symbols),
pid_unconnected_equipment — silent on a sheet with no P&ID symbols, plus the
mechanical focuses mech_missing_centreline, mech_unhatched_section,
mech_view_misaligned, mech_duplicate_dimension, mech_thread_unrepresented,
mech_bom_balloon_mismatch — silent on a sheet with no mechanical part),
then saves to disk, exports a screenshot, and returns the DWG path.
Raises ToolError if any validator 'error' finding is present, or if critique reports an
@@ -5947,10 +5965,14 @@ async def drawing_critique(
Field(
default=None,
description="Subset of: iso128, layer_color, dim_overlap, untrimmed_corner, "
"duplicate_entities, construction_left, gdt, and the P&ID focuses "
"duplicate_entities, construction_left, gdt, the P&ID focuses "
"pid_dangling_line, pid_duplicate_tag, pid_incompatible_connection, "
"pid_untagged_instrument, pid_illegal_tag, pid_unconnected_equipment "
"(silent on a drawing with no P&ID symbols). None = run all.",
"(silent on a drawing with no P&ID symbols), and the mechanical focuses "
"mech_missing_centreline, mech_unhatched_section, mech_view_misaligned, "
"mech_duplicate_dimension, mech_thread_unrepresented, "
"mech_bom_balloon_mismatch (silent on a drawing with no mechanical part). "
"None = run all.",
),
] = None,
ctx: Context = None,
@@ -9576,7 +9598,7 @@ async def balloon_add(
)
@mcp.tool(
annotations={"title": "Sheet: Attach Xref", "destructiveHint": False},
tags={"drawing", "block"},
tags={"sheet", "drawing", "block"},
)
async def xref_attach(
path: Annotated[str, Field(description="Full path of the drawing to reference.")],
@@ -9612,7 +9634,7 @@ async def xref_attach(
@cad_tool(summary="List, reload, bind, detach or repath an external reference.", cost="mutate")
@mcp.tool(
annotations={"title": "Sheet: Manage Xrefs", "destructiveHint": True},
tags={"drawing", "block"},
tags={"sheet", "drawing", "block"},
)
async def xref_manage(
action: Annotated[
@@ -9646,7 +9668,7 @@ async def xref_manage(
@cad_tool(summary="Attach a raster image (PNG/JPG) as an underlay.", cost="mutate")
@mcp.tool(
annotations={"title": "Sheet: Attach Image", "destructiveHint": False},
tags={"drawing", "create"},
tags={"sheet", "drawing", "create"},
)
async def image_attach(
path: Annotated[str, Field(description="Full path of the raster image.")],
@@ -9720,7 +9742,7 @@ async def data_extract(
@cad_tool(summary="Write the drawing as a real DWG at a chosen AutoCAD version.", cost="safe")
@mcp.tool(
annotations={"title": "Drawing: Export DWG", "destructiveHint": False},
tags={"drawing"},
tags={"sheet", "drawing"},
)
async def drawing_export_dwg(
path: Annotated[str, Field(description="Full path of the .dwg to write.")],