fix: correctness, cross-backend parity & enforced quality gate (audit Sprints 2-4)
Audit-driven fixes across the dual backend, the premium workflow, and the engineering layer. 318 tests, ruff-clean. Quality gate (the moat): - drawing_finalize now runs the premium ISO-128 critique in addition to the 8-step validator; leftover construction, non-ISO lineweights, untrimmed corners, duplicates and dim overlap block the gate (strict_critique fails on warnings too). - dim_overlap critique no longer a no-op (dim reference point + bbox fallback); dim/construction layers resolve from the active layer set (iso13567 too). Stability / correctness: - view_screenshot & drawing_export_pdf render headless via Agg (no more worker-thread SIGSEGV). - gear tooth profile no longer self-overlaps at high tooth counts; section view no longer duplicates bore lines; validator keyway heuristic fixed. - ezdxf aligned/angular dim API, full-circle polar array duplicate already covered by Sprint 1; this bundle adds save-format-from-extension, entity_offset side selection, ARC length, properties parity (bounding_box / rotation / char_height), mtext rotation. COM robustness (mocked-COM tested): - CoUninitialize on teardown; transaction commit/rollback try/finally + UNDO via the CMDACTIVE guard; system_run_lisp guard + USERS1 value capture; set_variable type coercion; HWND via Application.HWND; BlockInfo.description. Security: - HTTP remote-bind guard fires on every launch path (incl. `fastmcp run`); StaticTokenVerifier wired into FastMCP; AutoLISP allowlist bypass-vector tests. Hygiene: linetype-on-demand in _apply_attrs, honest view_zoom returns, snap-on-arc sweep handling, point_tangent acos clamp, dynamic tool_groups / tool count, word-boundary SPUR/HELICAL, undo-stack locking. Deferred: R22 (undo/transaction stack isolation) lands with the I1 refiner (1.2), which depends on it.
This commit is contained in:
@@ -22,6 +22,14 @@ DIMENSION_LIKE_TEXT = re.compile(
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# R27 — match SPUR / HELICAL as whole words (not substrings of e.g.
|
||||
# "SPURIOUS") when checking title/helix consistency.
|
||||
_SPUR_RE = re.compile(r"\bSPUR\b", re.IGNORECASE)
|
||||
_HELICAL_RE = re.compile(r"\bHELICAL\b", re.IGNORECASE)
|
||||
|
||||
# Layers that hold title-block text (scoped scan target for step 5).
|
||||
_TITLE_LAYERS = {"TITLE", "TITLEBLOCK"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationFinding:
|
||||
@@ -141,38 +149,54 @@ class DrawingValidator:
|
||||
"bore_missing",
|
||||
"Expected bore but no circle on GEOMETRY layer found",
|
||||
))
|
||||
# draw_keyed_bore emits the keyway notch as an OPEN 4-point polyline
|
||||
# on GEOMETRY — requiring `closed` here made this a permanent
|
||||
# false-negative. Match a small (3-6 point) polyline, open or closed.
|
||||
keyway_polys = [
|
||||
e for e in ents
|
||||
if e.type in ("LWPOLYLINE", "POLYLINE")
|
||||
and e.layer == "GEOMETRY"
|
||||
and len(e.properties.get("points", [])) <= 6
|
||||
and e.properties.get("closed")
|
||||
and 3 <= len(e.properties.get("points", [])) <= 6
|
||||
]
|
||||
if expected.get("must_have_keyway") and len(keyway_polys) < 1:
|
||||
findings.append(ValidationFinding(
|
||||
"warning",
|
||||
"keyway_section_unverified",
|
||||
"Could not confirm keyway in section view via heuristic "
|
||||
"(small closed polyline on GEOMETRY)",
|
||||
"Could not confirm keyway via heuristic "
|
||||
"(small 3-6 point polyline on GEOMETRY)",
|
||||
))
|
||||
|
||||
# 5 — title consistency
|
||||
helix = expected.get("helix_angle")
|
||||
if helix:
|
||||
title_texts = [
|
||||
(e.properties.get("text", "") or "").upper()
|
||||
# R27 — scope the scan to title-block text where possible (TITLE /
|
||||
# TITLEBLOCK layer); fall back to all TEXT-layer text if no title
|
||||
# layer carries any text.
|
||||
title_block_texts = [
|
||||
e.properties.get("text", "") or ""
|
||||
for e in ents
|
||||
if e.type in ("TEXT", "MTEXT") and e.layer == "TEXT"
|
||||
if e.type in ("TEXT", "MTEXT") and e.layer in _TITLE_LAYERS
|
||||
]
|
||||
if title_block_texts:
|
||||
title_texts = title_block_texts
|
||||
else:
|
||||
title_texts = [
|
||||
e.properties.get("text", "") or ""
|
||||
for e in ents
|
||||
if e.type in ("TEXT", "MTEXT") and e.layer == "TEXT"
|
||||
]
|
||||
joined = " | ".join(title_texts)
|
||||
if "HELICAL SPUR" in joined:
|
||||
# R27 — whole-word matching so "SPURIOUS" no longer trips SPUR.
|
||||
has_spur = bool(_SPUR_RE.search(joined))
|
||||
has_helical = bool(_HELICAL_RE.search(joined))
|
||||
if has_spur and has_helical:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"title_helical_spur",
|
||||
"Title contains 'HELICAL SPUR GEAR' — invalid; use "
|
||||
"'HELICAL GEAR' or 'SPUR GEAR'",
|
||||
))
|
||||
elif "SPUR" in joined and "HELICAL" not in joined:
|
||||
elif has_spur:
|
||||
findings.append(ValidationFinding(
|
||||
"error",
|
||||
"title_says_spur_for_helical",
|
||||
|
||||
Reference in New Issue
Block a user