A wire is straight only when its two terminals are exactly in line, and a
symbol is placed by its origin with its terminals at an offset from it, so
symbols an assistant places "under each other" by eye land a few pixels
apart and the wire jogs. Nothing told it so.
qet_layout_check reads every symbol and every wire's drawn path through
the scripting API (the default path is not saved in the file) and reports
a 0-100 score with: wires that jog where moving one symbol would make them
straight, extra bends, wires through symbols, overlaps, symbols off the
grid and crossings. "fixes" is one move_element per symbol, planned
together so they can be applied in one qet_edit call: a straight wire pins
its symbols, a move never lands a symbol on another or across a wire, and
symbols lined up with each other go onto the grid together. Styles: iec
(columns), nfpa (rungs) or auto. Read-only.
Over the 23 shipped examples, applying the fixes never lowers a score and
a second check proposes no further moves on 21 of them; a sloppy
assistant-style drawing goes from 40 to 100 in one round, in both styles.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
Reported on #1178 after a first try of live mode on macOS:
- The English interface showed the live-mode setting in French. The
strings were in lang/qet_en.ts but untranslated. This fills in English
for live mode, the script buttons, the macro recorder and the Connect
an AI assistant dialog (101 entries); other features' untranslated
English entries are left for their own change.
- The setting is on Configurer QElectroTech > Général > Projets, but the
start-up warning, the MCP server's error and its README all said
"Configurer > Général". All three now name the Projects tab, and the
README and the server name the English menus and buttons.
- qet_live_screenshot grabbed the whole viewport, so a tall window around
a landscape folio came back mostly empty. It now crops to the folio's
frame and title block where they are on screen (the whole viewport if
the folio is scrolled out of sight), and says so in cropped_to_folio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reported on #1178: with "route": "avoid", two contacts one above the
other, wired bottom terminal to top terminal 30 px apart or closer, got
a five-segment loop (at 20 px: v 80, h -20, v -140, h 20, v 80) that ran
down through the lower contact and back up past the upper one.
The cause was exitPoint(): it walked out of a terminal until clear of
every obstacle, so with another symbol in front it walked through that
symbol, and the search then had to come back. The same walk made a wire
between two symbols inside a frame (a cabinet drawn as one element)
leave through the frame's side, go round, and cross back in.
The router now knows each terminal's own symbol (Request::start_symbol,
end_symbol; applyRoute() fills them):
- two terminals facing each other on one line with nothing between
them are joined straight, however close;
- the exit walks through the margin around other symbols but never
through one; a terminal pointing straight into another symbol gets
"no-route" instead of a route through it;
- an obstacle drawn around either end's own symbol is left out.
Without the symbols (the old Request), routes are as before.
Rerouting every wire of four shipped examples (perceuse, affuteuse_250h,
Polonez, industrial; 1331 wires): master routes 121 of them through
another symbol, this none (10 pass through a second symbol lying
exactly on an end symbol's rectangle, which no route can avoid). 99
wires that master routed through a symbol now get "no-route" and keep
their path. The 1232 wires both route are 6 % shorter in total
(436,144 -> 410,608 units) with 11 % fewer bends (2256 -> 2010).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rename the `near` lambda in Conductor::setPathPoints() to `isNear`:
`near` and `far` are empty macros in the Windows headers (minwindef.h).
- Reword the log when setPathPoints() refuses a route. "does not join the
two terminals" named only one of its reasons; the router always returns
the two terminals and at least one exit point, so a refusal means the
route is not a run of horizontal and vertical segments between them.
- README: running along another wire is penalised, not forbidden, so two
wires can end up drawn on top of each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note the op returns reads "...; frame 1223.25 x 791.25 pt"; the README
quoted it as "PDF page".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two exports of one project differed in the creation and modification
dates (the time of the export, in local time) and in the document id
(random). With SOURCE_DATE_EPOCH set (reproducible-builds.org),
--export-pdf now uses the time it names, in UTC, and a document id
derived from the project: version 5 of the project uuid with the
SHA-256 of the project file, so it is the same for the same file and
changes when the file does. Qt has no setter for the dates, so
PdfLinks::setDocumentDate() rewrites them in the document information
and the XMP metadata after the file is written, fixing the XMP /Length
and shifting the xref table. Without the variable nothing changes.
Only --export-pdf reads it; the print window's PDF export does not, as
a person exporting by hand wants the real date.
qet_export (misc/qet-mcp) gains "reproducible" and "source_date_epoch".
It reports whether the build honoured the variable, since an older one
ignores it.
tst_pdfreproducible exports examples/741.qet four times and requires
identical bytes and the epoch's date; it fails with either this change
or the hash seed fix reverted. Projects with cross-reference links also
need their links in a fixed order, which is a separate fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, Qt names the settings subfolder after the organization domain
when one is set, so QElectroTech read <folder>/qelectrotech.org/ while
main.cpp, tst_settingsdir and qet_mcp.py all use <folder>/QElectroTech/.
Clear the domain when QET_SETTINGS_DIR is set, on macOS only. Nothing in
QElectroTech reads it; SingleApplication hashes it into its key, so these
runs no longer hand off to an open QElectroTech.
test_qet_mcp.py: find true on PATH (/usr/bin/true on macOS), and compare
the found binary with samefile() on case-insensitive file systems.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setFolioBorder(folio, "preset", "tabloid-landscape") picks the column and
row counts and whole-number sizes that fill the sheet best without going
over it, as one ChangeBorderCommand. The title block and headers are
measured from the folio, so it holds for any template on either edge.
Sizes stay whole because the folio properties panel edits them in whole
pixels. Among fills within 1.5 pt of the sheet, the size nearest the
current one wins: from a new folio tabloid landscape is 23 x 70 by 12 x 82,
not 9 x 179 by 5 x 197. A0-A5, letter, legal, tabloid/ledger, each
-portrait or -landscape; folioPresets() lists them, and folioBorder() reads
"width" and "height" of the frame and title block.
Every preset exports on its sheet; landscape ones rely on the previous
commit, which turns a wide page before matching it to a sheet.
The MCP server checks the name up front, probes for folioPresets(), and
the op's note says what was chosen and the frame's size in points.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Most shipped symbols leave their terminals unnamed, so --export-wiring
wrote both terminal columns empty and a reader had to work each end out
from the .qet: terminal uuid, then its place in the definition, then the
index. The terminal table now keeps the index the scripting API's
addConductor() takes, from both fills of the database (the document and
the folios give the same value: Element::parseTerminal()'s sort, top to
bottom then left to right), and wiring_list_view and the CSV gain
from/to_terminal_index and from/to_terminal_uuid as their last columns.
The index is left empty for two terminals at one point, where the sort
is not stable and the index is not defined; the uuid tells them apart.
The wiring list dialog reads named columns and is unaffected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A new conductor gets the default two or three straight segments, which
run through whatever symbol or wire lies between its terminals; scripts
then fix it segment by segment with moveConductorSegment().
ConductorRouter finds an orthogonal path on the folio grid that leaves
and enters each terminal in its own direction, keeps clear of every
element's rectangle, stays inside the border, and charges for bends and
for running along or crossing other wires. qet.routeConductor() and
qet.routeConductorBetween() apply it through Conductor::setPathPoints(),
which pushes the same ChangeConductorCommand a handle drag does, so the
path is saved, survives a reload, and one undo restores the default.
Where no route exists the wire keeps its path and the call says so.
qet-mcp: add_conductor takes "route": "avoid", and a route_conductor op
reroutes an existing conductor (by terminal or by uuid). The router
methods are required only by an edit that routes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
qet_recording_list and qet_recording_read fetch what QElectroTech's macro
recorder saved: each step from the undo history as structured changes
(consecutive folio states diffed with qet_diff), and the overall change
from before.qet to after.qet. qet_recording_check runs a script on a copy
of before.qet from where the person started -- the folio on screen (a
proxy answers qet.currentFolio(), which a Qt object will not let a script
replace) and the selection -- and says whether the result matches
after.qet, or what differs. qet_recording_remove deletes one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
QElectroTech now advertises its live channel in the "live" part of
qet-assistant.json instead of a live-session.json of its own. When nothing
is listening, the error now tells apart QElectroTech not running, live
mode switched off, and the start-up warning not accepted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
QElectroTech now writes qet-assistant.json with its folders, features,
script calls and stored scripts. The server reads it to find the scripts
folder -- right even when QElectroTech runs with --data-dir, where the
per-platform guess was wrong -- and qet_about shows it, never the live
token. The initialize reply now carries instructions: what QElectroTech
is, headless and live, the usual order of tools, start with qet_about.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
qet_live_screenshot returns an MCP image the assistant can look at;
--call prints an image part as a data: URI instead of failing on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_live_status, qet_live_run_script and qet_live_run_stored talk to a
running QElectroTech through the channel its LiveServer opens, only when
live mode is switched on in its settings and accepted at this start. They
find it through live-session.json in QElectroTech's data folder and send
its token on every request; with no session, the error says which of the
three switches is missing. Running needs QET_ENABLE_SCRIPTING=1 as editing
does; asking for the status does not.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checked against a real Windows package under Wine: QElectroTech keeps its
data in %APPDATA%\QElectroTech\QElectroTech, next to which the scripts
folder goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_script_api lists every qet.* call, asked of the QElectroTech that will
run it (qet.apiSignatures() where the build has it, names otherwise), with
the header format that makes a script a button.
qet_script_test runs a script's text on a copy of a project and returns the
qet_diff of what it changed, its log and its errors with their line.
qet_script_install stores a script, and optionally an SVG icon, in the
scripts folder QElectroTech reads; with test_project it tests first and
stores nothing if the test fails. qet_script_list, qet_script_read and
qet_script_remove complete the set.
The folder is the server's choice, never the call's: ids are checked as
file names, and storing or removing needs QET_ENABLE_SCRIPTING=1 like an
edit, because a stored script runs when the user clicks it. A header is
checked with the same rules as QElectroTech's own, so nothing is stored
that would get no button.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "one text per potential" switch (onetextperfolio) lives in a folio's
conductor defaults, which no scripting call or qet_edit op could reach, so
callers patched the saved .qet afterwards (#1178).
qet.setConductorDefault(folio, property, value) sets onetextperfolio or any
setConductorProperty name on a folio's defaults, or with folio -1 on the
project's defaults that new folios copy. A change to onetextperfolio
re-shows or hides the conductor texts at once, as the Folio properties
dialog does. Not on the undo stack, like both dialogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server gives each QElectroTech run a private HOME and writes the
element collection path for it (elements_dir) into
~/.config/QElectroTech/QElectroTech.conf there. Only Linux reads that
file: Qt keeps settings in the registry on Windows and in the system
preferences on macOS. So on those systems elements_dir did nothing, and
every run read the user's own settings (#1178).
When QET_SETTINGS_DIR names a folder, QElectroTech now keeps its
settings in <folder>/QElectroTech/QElectroTech.ini on every system. It is
set in main() before the first setting is read. Without the variable
nothing changes.
The server sets it for each run and writes the collection path to the
.ini as well as the .conf, so an older QElectroTech keeps working on
Linux. The path is written with forward slashes: Qt reads a backslash in
these files as an escape. The README notes that elements_dir needs this
on Windows and macOS, and that long arguments go on stdin with "-"
(Windows refuses a command line over 32,767 characters).
tst_settingsdir: a script places a symbol that only the folder's settings
can resolve, while the usual settings file points at an empty collection.
It fails on master and passes here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seven Edit > Align commands (Snap to grid and the six align
commands, discussion #1069) had no icons: the menu, the context menu,
command search and the shortcut bar showed them as bare text.
They are pixel-grid SVGs in ico/scalable/ drawn like the folio and
Add PDF icons: 24 pixel canvas, art on the inner 22 pixel grid, one
currentColor ink so misc/make_icon_themes.py writes the dark copies.
Each align icon is a guide line with two boxes of different lengths
against it; the centre ones show the guide only between the boxes.
Snap to grid is a faint grid with one box sitting exactly on a cell.
The boxes have a solid outline and a 35 % tinted inside, which keeps
the guide the strongest mark at 16 pixels.
Names follow the freedesktop icon names (align-horizontal-left, ...).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Windows portable ZIP ships "Lancer QET_qt6.bat", but the script only
looked for "Lancer QET.bat", so it stopped at its first check and never
registered anything. It now tries the ZIP's name first and falls back to
the installer's.
It also wrote to HKEY_CLASSES_ROOT, which a standard (non-admin) Windows
account cannot create keys in. It now writes to
HKEY_CURRENT_USER\Software\Classes: per-user, no elevation needed, and
Windows merges it into HKEY_CLASSES_ROOT for that user.
qet_uninstall_file_associations.reg removes the HKCU keys first, then the
old HKEY_CLASSES_ROOT ones as before.
Tested under Wine 10 on the git10275 nightly ZIP: the old script aborts
("Lancer QET.bat ... n'a pas ete trouve"), the new one registers .qet,
.elmt and .titleblock under HKCU with nothing under HKLM, the uninstall
file removes them, and the "Lancer QET.bat" fallback works. Wine does not
enforce admin rights, so the non-admin case itself is not proven there.
Reported in #1148.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Windows every tool that starts QElectroTech (qet_export, qet_edit,
qet_query, qet_check, qet_continuity, qet_project_new) failed, for two
reasons:
- The server ran a copy of the executable from a temporary folder, to get
its own SingleApplication key. A Windows program loads its DLLs from its
own folder, so the copy died before main() with 0xC0000135 (DLL not
found). Every flag the server passes is a CLI export flag or --run, and
main.cpp handles both before it constructs SingleApplication, so on
Windows the original is now run. The copy stays elsewhere.
- It set QT_QPA_PLATFORM=offscreen. The Windows packages ship only the
qwindows platform plugin, so Qt found none and stopped at a message box
nobody could close: every call hung until its timeout. Windows now keeps
its default platform; the export flags and --run open no window.
Checked under Wine (qet-wine-smoke) on the fork's CI Windows build, run
through python.org's embeddable Python: before, qet_export ended with
exit 3221225781; after, a PDF export, a qet_query (98 elements, as on
Linux) and a qet_edit placing a common:// element all succeed. The hang
was isolated by launching the same export from bash (works) and from
Python with one change at a time: only dropping QT_QPA_PLATFORM made it
work. Four unit tests pin both choices per platform; each fails with its
fix removed. Suite 262/262 none skipped on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation audit of the functions no audit had covered (tests needing no
QElectroTech): these 13 caught 153 of 337 planted bugs (45 %). With these
tests, 322 (96 %). qet_check was at 26/59 and qet_continuity at 24/37
even with the binary tests, since those look at a finding or two.
- qet_check / qet_continuity: _run_qet replaced by a stub returning chosen
log lines, so the whole answer is compared -- summary counts, "ok",
passed, check_failures, each finding's count, note and sampled rows,
sorting by severity, folio_number, the launch hint carried through,
the folio argument in the script, folio bounds, and lines that only
look like ours.
- qet_element_build / qet_element_info: the written header, names, kind
information and terminals, and the reported result, key for key; the
refusals; geometry worked out by hand; every part kind's extent and
written attributes; number formatting.
- qet_element_search and its index: the index entry key for key, the
cache, ranking (exact name, then first word, then length), the default
and given limits.
The 15 left are equivalent: timeouts and output limits, a ranking
constant that only has to exceed 0, and a containment check the 5-unit
margin keeps from ever failing. Tests only; 282/282.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most Windows users have no Python, which the MCP server needs. The
installer now offers "Python for the AI assistant", unticked by default:
the official embeddable package from python.org (3.14.7, ~12 MB, no
registry, removed with QElectroTech), in <folder>\mcp\python. The
portable folder and the MSI, which pack all of files/, carry it.
The workflow downloads it pinned by version and by python.org's own
sha256 for the file (checked against python.org's download API), and
fails the build on a mismatch. The script stays plain text beside it.
Checked under Wine (64-bit, qet-wine-smoke image), on an installer built
with makensis 3.10 (0 warnings, strings in all 29 languages):
- a silent default install puts mcp\qet_mcp.py in place and no Python;
the same installer with the section ticked by default installs it, so
the check tells the two apart;
- the installed Python runs the installed server: 15 tools listed,
qet_project_info on an example answers as on Linux, and the server
finds bin\QElectroTech.exe and elements\ by itself.
Not checked: a real Windows machine; the CI download step (fork run).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server (misc/qet-mcp) was only reachable from a source checkout.
It now ships with every package, next to the program, and finds that
QElectroTech and its element collection from where it sits:
- make install (Linux distributions, snap, flatpak):
<prefix>/share/qelectrotech/mcp/qet_mcp.py, executable, with its README.
- Windows installer: a new "AI assistant (MCP)" component (on by default,
~200 KB), installed to <folder>\mcp. The workflow stages files/mcp, so
the portable folder and the MSI, which packs all of files/, carry it too.
The server learns the Windows layout (<root>/mcp beside <root>/bin, whose
program is QElectroTech.exe, and <root>/elements), in addition to
<prefix>/share/qelectrotech/mcp. A copy saved anywhere else, even beside
some bin/ folder, is not taken for an installation.
The installer strings are given in all 29 installer languages: French
translated, the others in English until translated, which is what NSIS
would fall back to anyway but without its warning 6040 per language.
Checked: make install into a scratch prefix, then from the installed
script with nothing configured and no qelectrotech on PATH, an SVG export
and a qet_edit placing a common:// element both succeeded. makensis 3.10
compiles the installer with 0 warnings before and after (removing the
French description gives warning 6040), and the installer contains
mcp/qet_mcp.py. Suite 274/274 none skipped; each layout check was
removed in turn and a test failed. Snap/flatpak: installed by the same
CMake rule; launching their QElectroTech from outside is untested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qet_export, qet_edit, qet_query, qet_continuity, qet_check and
qet_project_new took the QElectroTech executable as a per-call argument
and ran whatever executable file it named, with the call's own paths as
arguments. The workspace policy exempted it as configuration, but it is
chosen by the model on every call, so text inside a project could steer
an assistant into starting another program.
The server now finds QElectroTech itself: QET_BINARY, then the install it
ships in (<prefix>/share/qelectrotech/mcp/), then PATH. "binary" becomes
optional; when given it must be that same file (after resolving
symlinks) or one listed in QET_MCP_BINARIES. QET_MCP_ALLOW_ANY_BINARY=1
restores the old behaviour, as QET_MCP_ALLOW_ANY_PATH does for paths.
"elements_dir" defaults to the installed collection and, when given, must
be in the workspace, that collection, or QET_MCP_ELEMENTS.
Both checks live in enforce_path_policy(), the one place tool arguments
enter. test_configuration_paths_are_exempt asserted the old exemption and
is replaced by BinaryPolicy (13 tests) and a stdio test of the original
reproduction. Each new check was removed in turn and the tests failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/qet-mcp-audit/mutate.py over the code changed since #1095
(_build_script, the diff helpers, _parse_script_output, tool_items and
the wire-end keys of #1118), tests needing no QElectroTech only: master
f530b7636's suite noticed 531 of 578 planted bugs (92 %). With these
tests, 568 (98 %). The 10 left are equivalent: rsplit("/", 1) vs 2, a
1e-6 tolerance compared with < or <=, a one-letter orientation sliced
[:1] or [:2], branches that only touch parts without terminals, and the
placeholder terminal 0 a conductor uuid overwrites.
- Every argument kind refuses what it cannot take, before any launch:
malformed indices, bool, points and nodes, search_and_replace with an
unknown kind or conductor field or an empty element_info field,
"conductor" given with "element" or "terminal" alone, an op that is not
an object -- and the same kinds accept what they should.
- _parse_script_output reports capabilities (none logged = unknown, not
"nothing missing"), notes on the right op, save and stopped_early;
ignores a line without the marker even when it would parse, and a line
of a kind it does not know.
- A uuid wire end in a column of terminals (same x) keys like the
numbered one: the match is on x, y and orientation together.
Tests only; 264/264.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "$id" of an add_folio or insert_folio held the index the folio had
when it was made, so a later insert_folio or remove_folio in the same
qet_edit run shifted it, and ops naming "$id" edited the wrong folio --
the case #1115 fixed for folios given by uuid, left open for these.
The script now also keeps such a folio's uuid (qet.folioUuid()) and
resolves "$id", where an op takes a folio, through qet.folioIndex() at
the moment it is used. On a build without folioUuid() it falls back to
the stored index, as before, and nothing new is required of the binary.
The op's reported result is still the index.
Test: add_folio "$f" (index 1), insert_folio at 0, set_folio_title
"$f": the title lands on the third folio -- on the second without this
change. Two script-generation tests updated for the new expression.
255/255.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>