From 173346c712341b2d4bda13c3cea6361e73208d11 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sat, 3 Oct 2026 22:26:42 +1300 Subject: [PATCH] qet-mcp: qet_check finds crowded terminals and folio reports with several wires Discussion #1158 asks for a limit on the wires per terminal. Before any limit, two read-only checks let anyone count their own projects: - crowded_terminals: terminals with more than four wires (two double ferrules, one either side of the screw), folio reports left out; - reports_with_several_wires: folio report arrows with more than one wire, the rule plc-user proposes for reports. Both are info, not errors: cable, busbar and single-line symbols carry more wires on purpose, and a third of the shipped examples' reports have several. Over the 24 examples: 23 crowded terminals in 10 projects (the worst, 19, is a cable symbol in affuteuse_250h.qet) and 130 reports in 4 projects. Plain SQL over the conductor table, so they work on any current QElectroTech. test_qet_mcp: the checks are read-only, the exact-answer test lists them, and a coil wired to five others is reported once with its count (checked to fail with the threshold raised). 398 tests pass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD --- misc/qet-mcp/README.md | 2 +- misc/qet-mcp/qet_mcp.py | 39 ++++++++++++++++++++++++++++++++++++ misc/qet-mcp/test_qet_mcp.py | 24 ++++++++++++++++++++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index 36bd4969e..0852d13a4 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -39,7 +39,7 @@ here read the model. | `qet_element_build` | **author a `.elmt`** — draw a new symbol, with terminals to wire it by | | `qet_project_new` | **start from nothing** — an empty project with a title and folios | | `qet_element_search` | **find a symbol** in a collection by name (any language), type or terminal count | -| `qet_check` | **design-rule checks** — duplicate labels, unlabelled masters, unnumbered conductors, empty folios | +| `qet_check` | **design-rule checks** — duplicate labels, unlabelled masters, unnumbered conductors, empty folios, terminals with more than four wires, folio reports with several wires | | `qet_layout_check` | **does the drawing read well?** — a 0–100 score; wires that jog because two symbols are a few pixels out of line, symbols off the grid, wires through symbols, overlaps, crossings; and the moves that fix them, ready for `qet_edit` | | `qet_query` | **ask the project database** — read-only SQL over the views and tables | | `qet_about` | **start here** — where QElectroTech keeps things, what is switched on, the stored scripts, the calls a script can make (from `qet-assistant.json`) | diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index c5ea6af52..969e8a501 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -2535,6 +2535,45 @@ CHECKS = { "WHERE NOT EXISTS (SELECT 1 FROM element e WHERE e.diagram_uuid = d.uuid) " "ORDER BY p.pos", }, + "crowded_terminals": { + "severity": "info", + "note": "Terminals with more than four wires: two double ferrules, one " + "either side of the screw, is already a lot for a real terminal " + "(discussion #1158). Cable, busbar and single-line symbols carry " + "more on purpose; element_type and label tell them apart.", + "sql": "SELECT d.pos AS folio, e.pos AS diagram_position, " + "COALESCE(ei.label,'') AS label, e.type AS element_type, " + "COALESCE(t.name,'') AS terminal, w.n AS wires FROM (" + "SELECT tu, eu, COUNT(*) AS n FROM (" + "SELECT terminal1_uuid AS tu, terminal1_element_uuid AS eu FROM conductor " + "UNION ALL SELECT terminal2_uuid, terminal2_element_uuid FROM conductor" + ") GROUP BY tu, eu HAVING n > 4) AS w " + "JOIN element e ON e.uuid = w.eu " + "LEFT JOIN diagram d ON d.uuid = e.diagram_uuid " + "LEFT JOIN element_info ei ON ei.element_uuid = e.uuid " + "LEFT JOIN terminal t ON t.uuid = w.tu AND t.element_uuid = w.eu " + "WHERE e.type NOT IN ('next_report', 'previous_report') " + "ORDER BY w.n DESC, folio, diagram_position", + }, + "reports_with_several_wires": { + "severity": "info", + "note": "Folio report arrows with more than one wire. A report is a " + "virtual point: one wire, continued on the other folio, says " + "where the wire really runs (discussion #1158). A third of the " + "shipped examples' reports have several, so this is a style, " + "not an error.", + "sql": "SELECT d.pos AS folio, e.pos AS diagram_position, " + "COALESCE(ei.label,'') AS label, w.n AS wires FROM (" + "SELECT tu, eu, COUNT(*) AS n FROM (" + "SELECT terminal1_uuid AS tu, terminal1_element_uuid AS eu FROM conductor " + "UNION ALL SELECT terminal2_uuid, terminal2_element_uuid FROM conductor" + ") GROUP BY tu, eu HAVING n > 1) AS w " + "JOIN element e ON e.uuid = w.eu " + "LEFT JOIN diagram d ON d.uuid = e.diagram_uuid " + "LEFT JOIN element_info ei ON ei.element_uuid = e.uuid " + "WHERE e.type IN ('next_report', 'previous_report') " + "ORDER BY w.n DESC, folio, diagram_position", + }, "masters_without_manufacturer_reference": { "severity": "info", "note": "Masters that will show a blank article number in the BOM.", diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 0ece9ec19..f20351f7f 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -1207,6 +1207,8 @@ class CheckAndContinuityAnswers(unittest.TestCase): {"kind": "check", "name": "unnumbered_conductors", "rows": [{"n": 1}, {"n": 2}], "error": ""}, {"kind": "check", "name": "duplicate_simple_labels", "rows": [], "error": ""}, {"kind": "check", "name": "empty_folios", "rows": None, "error": "bad SQL"}, + {"kind": "check", "name": "crowded_terminals", "rows": [], "error": ""}, + {"kind": "check", "name": "reports_with_several_wires", "rows": [], "error": ""}, {"kind": "other", "name": "masters_without_manufacturer_reference", "rows": [1]}, ] with self.stub(lines): @@ -1214,7 +1216,7 @@ class CheckAndContinuityAnswers(unittest.TestCase): C = m.CHECKS self.assertEqual(r, { "ok": False, - "summary": {"errors": 1, "warnings": 1, "info": 1, "passed": 1, "check_failures": 2}, + "summary": {"errors": 1, "warnings": 1, "info": 1, "passed": 3, "check_failures": 2}, "findings": [ {"check": "duplicate_master_labels", "severity": "error", "count": 12, "note": C["duplicate_master_labels"]["note"], "rows": rows[:10]}, @@ -1222,7 +1224,7 @@ class CheckAndContinuityAnswers(unittest.TestCase): "note": C["unlabelled_masters"]["note"], "rows": [{"x": 1}]}, {"check": "unnumbered_conductors", "severity": "info", "count": 2, "note": C["unnumbered_conductors"]["note"], "rows": [{"n": 1}, {"n": 2}]}], - "passed": ["duplicate_simple_labels"], + "passed": ["duplicate_simple_labels", "crowded_terminals", "reports_with_several_wires"], "check_failures": [ {"check": "empty_folios", "error": "bad SQL"}, {"check": "masters_without_manufacturer_reference", "error": "no result came back"}]}) @@ -5477,6 +5479,24 @@ class Integration(unittest.TestCase): self.assertEqual(found["masters_without_manufacturer_reference"]["count"], 2) self.assertFalse(c["ok"]) + def test_check_finds_a_crowded_terminal(self): + """Five wires on one terminal of a coil: crowded_terminals reports + that terminal once, with its count; four would pass.""" + base = self.sb.new() + ids = "habcde" + r = self.ok(self.sb.edit(base, [ + *[{"op": "add_element", "id": x, "folio": 0, "path": COIL, + "x": 100 + i * 120, "y": 100 + i * 90} for i, x in enumerate(ids)], + *[{"op": "add_conductor", "folio": 0, "from": "$h", "from_terminal": 0, + "to": f"${x}", "to_terminal": 0} for x in ids[1:]]])) + c = m.tool_check(BINARY, r["output"], checks=["crowded_terminals", + "reports_with_several_wires"]) + found = {f["check"]: f for f in c["findings"]} + self.assertEqual(found["crowded_terminals"]["count"], 1, c) + self.assertEqual(found["crowded_terminals"]["rows"][0]["wires"], 5) + self.assertEqual(found["crowded_terminals"]["rows"][0]["element_type"], "master") + self.assertIn("reports_with_several_wires", c["passed"]) + def test_check_passes_a_clean_project(self): base = self.sb.new() r = self.ok(self.sb.edit(base, [