Commit Graph

55 Commits

Author SHA1 Message Date
ispyisail 173346c712 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
2026-10-03 22:26:42 +13:00
ispyisail b899d46453 Merge master (#1264, #1265) into feature/qet-mcp-layout-ops
README and tests: both PRs added text at the same places; kept both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
2026-10-03 15:56:00 +13:00
ispyisail 372e541a00 qet-mcp: ops that line symbols up so wires come out straight
A wire is straight only when its two terminals are exactly in line, and
qet_edit placed a symbol only by its origin, so an assistant had to guess
where the terminals would land -- and usually missed by a few pixels.

- place_element adds a symbol with one of its terminals in line with
  another symbol's, gap pixels away on the side that terminal faces, and
  notes when the new symbol's terminal faces the wrong way to meet it.
- align_terminal moves a placed symbol across so two terminals line up.
- align_elements lines up symbols' boxes (left/center/right/top/middle/
  bottom), as Edit > Align does.
- distribute_elements spaces symbols' origins evenly, or a pitch apart,
  keeping symbols on the grid on it.

place_element and align_terminal need qet.terminalPosition(); on a build
without it the edit stops before running, naming the missing call, and
every other op still works there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FPuYPS4T7QuEwjNu22rXD
2026-10-03 13:37:00 +13:00
ispyisail 5f2d31bff7 qet-mcp: add qet_layout_check, a score and fixes for how a drawing reads
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
2026-10-03 13:17:09 +13:00
Laurent Trinques e1f14e4b2f Merge pull request #1258 from ispyisail/fix/route-close-symbols
Fix wire routing looping between close symbols (#1178)
2026-10-02 23:15:33 +02:00
Laurent Trinques d8add2b8a0 Merge pull request #1257 from ispyisail/feature/pdf-reproducible
Add repeatable PDF export when SOURCE_DATE_EPOCH is set
2026-10-02 23:15:05 +02:00
Laurent Trinques ff64fdcb00 Merge pull request #1256 from ispyisail/fix/route-review-followups
Fix the follow-ups from the review of wire routing (#1245)
2026-10-02 23:14:51 +02:00
Laurent Trinques 7d61acebd9 Merge pull request #1260 from ispyisail/fix/live-mode-polish
Fix live mode's English text, settings path and screenshot (#1178)
2026-10-02 23:02:54 +02:00
ispyisail 5859bc3353 Live mode: English text, the right settings tab, folio-only screenshot
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>
2026-10-03 09:14:57 +13:00
ispyisail 84cda1f0b0 Routing: no loops between close symbols, and route inside frames
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>
2026-10-03 08:43:27 +13:00
ispyisail b0d73080ce Routing: follow up scorpio810's review of #1245
- 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>
2026-10-03 08:13:31 +13:00
ispyisail 8bbdafe2e4 qet-mcp README: the preset note says frame, not PDF page
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>
2026-10-03 07:51:44 +13:00
ispyisail 51d20abca4 Write the same PDF for the same project when SOURCE_DATE_EPOCH is set
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>
2026-10-03 07:50:28 +13:00
ispyisail 580cbb1cb1 Merge remote-tracking branch 'upstream/master' into feature/mcp-folio-presets
# Conflicts:
#	misc/qet-mcp/README.md
#	misc/qet-mcp/qet_mcp.py
2026-10-03 07:44:54 +13:00
Laurent Trinques 8fc43aa30f Merge pull request #1245 from ispyisail/feature/mcp-route-around
Add wire routing around symbols to the scripting API and MCP
2026-10-02 14:05:13 +02:00
ispyisail 0b4fc8beb0 qet-mcp: size a folio for a sheet of paper (set_folio_border preset)
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>
2026-10-02 23:00:11 +13:00
ispyisail e345d4e5d0 Give each end of the wiring export its terminal index and uuid
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>
2026-10-02 22:30:49 +13:00
ispyisail 5e28e015e0 Scripting API and MCP: route a conductor around the symbols in its way
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>
2026-10-02 22:28:11 +13:00
ispyisail 51b1229935 Merge pull request #1228 from ispyisail/feature/mcp-macro-recorder
Add MCP tools to read macro recordings and check scripts against them
2026-10-02 15:14:16 +13:00
ispyisail f6867a0f4e Merge pull request #1225 from ispyisail/feature/mcp-live-tools
Add MCP live tools to act on the project open in QElectroTech
2026-10-02 15:13:29 +13:00
ispyisail 0f909e1246 Merge pull request #1224 from ispyisail/feature/mcp-script-tools
Add MCP tools to write, test and store script buttons
2026-10-02 15:13:08 +13:00
ispyisail f4660f19ec qet-mcp: macro recordings -- read what a person did, check a script against it
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>
2026-10-02 14:06:51 +13:00
ispyisail a99c58487d qet-mcp: find the live channel in qet-assistant.json
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>
2026-10-02 13:03:33 +13:00
ispyisail 15c114edd5 qet-mcp: read qet-assistant.json; qet_about; a note at first contact
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>
2026-10-02 13:02:26 +13:00
ispyisail ae06377f1a qet-mcp: live commands, folios, undo of the assistant's own step, screenshot
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>
2026-10-02 10:40:29 +13:00
ispyisail bf74468c40 qet-mcp: live mode tools, to act on the project open in QElectroTech
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>
2026-10-02 10:27:25 +13:00
ispyisail 55eac848df qet-mcp: tools to write, test and store scripts that QElectroTech shows as buttons
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>
2026-10-02 09:13:05 +13:00
ispyisail 7df9052a47 qet-mcp: set a folio's conductor defaults (set_conductor_default)
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>
2026-10-02 07:16:44 +13:00
ispyisail 65a1080cd3 Keep headless runs' settings in a folder of their own (QET_SETTINGS_DIR)
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>
2026-10-01 08:00:55 +13:00
ispyisail 9778dc0a86 Merge branch 'feature/qet-mcp-install' into feature/qet-mcp-windows-python 2026-09-29 14:37:53 +13:00
ispyisail c4b1e02810 Merge remote-tracking branch 'upstream/master' into feature/qet-mcp-install 2026-09-29 14:37:43 +13:00
ispyisail 0392d74e15 Merge pull request #1131 from ispyisail/fix/qet-mcp-folio-ref-uuid
Fix qet_edit's folio "$id" naming the wrong folio after an insert
2026-09-29 14:22:54 +13:00
ispyisail 6b9c5189b1 Offer Python with the Windows installer, for the MCP server
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>
2026-09-29 13:00:30 +13:00
ispyisail ac952d3df6 Install the MCP server with QElectroTech
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>
2026-09-29 12:55:10 +13:00
ispyisail 124b7e4f6e qet-mcp: stop a tool call choosing the program the server runs
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>
2026-09-29 12:11:03 +13:00
ispyisail 94b0d8d06d qet-mcp: a folio's "$id" keeps naming it after a later insert or removal
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>
2026-09-29 11:46:18 +13:00
ispyisail f530b7636e Merge pull request #1128 from ispyisail/feature/qet-mcp-call
Add a one-shot --call to qet-mcp and document desktop app setup
2026-09-29 11:37:14 +13:00
ispyisail 03fc51d0fe qet-mcp: add --call, and document using it from the Claude desktop app
A web chat in a browser cannot start a local program, so it cannot run
this server. Two ways round that, documented in the README:

- the Claude desktop app runs stdio servers; a step-by-step setup;
- a chat that can execute Python can upload qet_mcp.py and run
  `--call <tool> '<json>'` (or `-` to read the arguments from stdin).

--call goes through the same dispatcher as the stdio server, so the
workspace policy applies unchanged. Exit status 0 success, 1 the tool
reported an error, 2 the call was malformed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:15:26 +13:00
ispyisail f58bf7502e qet-mcp: qet_edit names a terminal by its uuid
"terminal", "from_terminal" and "to_terminal" take the terminal's uuid
(as qet_element_info lists it) in place of its index. It names a
terminal of the op's own element -- for add_conductor, of that end's
element, "$name" references included -- and is turned at run time into
the index the call takes by qet.terminalIndex(); if the element has no
such terminal the op fails with a note. Unlike the index, a sort by
position, it is defined between two terminals at the same point. The
lookup is required only when a uuid is used, so index-only edits still
run on older builds.

README: the terminal uuids; wires of older projects now get a lasting
uuid (#1107) instead of staying unnamed.

Tests: script generation for every terminal-taking op, "$name" and folio
uuid resolution, the index still passed through; through the binary,
bobine_ka_a_remanence (file order A2, A1; index order A1, A2) wired A2 to
A1 by uuid lands on exactly those ends, and an unknown uuid stops the run
with its note. 255/255 against a build with qet.terminalIndex(); a build
without it is reported as missing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:00:16 +13:00
Laurent Trinques 4bf02cd936 Merge branch 'master' into feat/qet-mcp-edit-folio-uuid 2026-09-28 20:53:51 +02:00
ispyisail c7e5c0c3b3 qet-mcp: qet_edit names a folio by its uuid
qet_edit takes a folio as its index, and the index shifts when an earlier
op in the same run adds, inserts or removes a folio: remove folio 0, then
edit "folio 2", and the edit lands on the folio that was 3. Every other
item qet_edit addresses (elements, texts, shapes, pictures, tables, symbol
text fields) can already be named by uuid; folios could not.

- "folio" and "to_folio" take the folio's uuid as well as its index,
  turned into the current index at run time by qet.folioIndex(). The
  lookup is required only when a uuid is used, so an index-only edit still
  runs on a build without it.
- qet_project_info lists each folio's uuid. A folio saved without one
  (132 of the 133 in the shipped examples) shows it empty until the
  project is saved once, and an empty "folio" says so.

Tests: the generated script for a uuid folio, on its own and inside a
table lookup and link_elements' to_folio; and a real run that removes
folio 0 and then retitles the old folio 2 by uuid, on a file saved without
folio uuids. The run fails against a build without qet.folioIndex().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:00:27 +13:00
ispyisail e7745afd18 qet-mcp: answer review on naming a conductor by uuid
- An end conductorEnds() reports as "?" is never picked.
- An empty "conductor" says why: an older project's conductors have no
  saved uuid, so qet_conductors reports it empty.
- The op description and README say set_conductor still changes the
  whole potential when given a uuid, and that older projects' conductors
  are named by element + terminal until #1103.
- test_conductor_by_uuid covers move_conductor_segment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:25:05 +13:00
ispyisail 9b967707a0 qet-mcp: qet_edit names a conductor by its uuid
set_conductor, move_conductor_segment and delete_conductor named a
conductor by one of its terminals, which had to carry exactly one
conductor: where two meet at a terminal, neither could be named from it.

Each now also takes "conductor": "{uuid}" (as qet_conductors reports it)
in place of element + terminal. The generated script asks
qet.conductorEnds() for the conductor's two ends and passes the one whose
terminal carries only that conductor. Where both ends are shared, or no
conductor has that uuid, the op fails and its "note" says which. The
lookup is required only when a uuid is used; giving both forms is an
error.

Tests: the script generated for each form and the argument errors; on a
folio where one terminal carries two conductors, deleting either by uuid
leaves exactly the other; an unknown uuid is reported. With the end chosen
without checking its terminal carries only that conductor, the terminal
test fails. 248/248 with a build carrying qet.conductorEnds().

Stacked on the conductorUuids()/conductorEnds() scripting change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:12:55 +13:00
ispyisail 83da997c83 Merge pull request #1100 from ispyisail/feat/qet-mcp-items
Add qet_items: list drawn items with their uuids
2026-09-28 16:15:03 +13:00
ispyisail 279c98f10a qet-mcp: add qet_items, listing drawn items with their uuids
qet_edit addresses free texts, shapes, pictures and tables by uuid, and
qet_diff reports them by uuid, but no tool listed them: the only way to
learn an item's uuid was to read the .qet. qet_items lists every free
text, shape, picture, table and symbol text field per folio (counted from
1, as qet_elements), with its uuid and main fields; filter by folio and
kind; default limit 500.

Also a test that every tool argument holding a data path is in the
workspace policy (_DATA_PATHS). Nothing checked that direction: a new tool
left out of the policy would have read or written anywhere with every test
passing, as removing qet_items' entry showed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:15:21 +13:00
ispyisail a73e5c462d qet-mcp: qet_continuity no longer reports a folio that does not exist as clean
qet.checkContinuity() answers a folio index it has no folio for with an
empty list, so qet_continuity returned "0 findings" for it -- the same
answer as a clean folio. The index counts from 0 while qet_elements numbers
folios from 1, so passing the last folio's number checked nothing and said
so cleanly; any other folio's number checked the next folio instead.

An index with no folio is now refused before QElectroTech is launched, with
the valid range and the counting rule. Each finding also carries
folio_number (counted from 1) beside the existing folio index. The
qet_continuity and qet_conductors descriptions and the README say how each
tool counts. Nothing changes for a valid index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:58:05 +13:00
ispyisail 5da0b92001 qet-mcp: say which folio index to use when qet_edit is given the wrong one
qet_elements and qet_project_info number folios from 1, as the application
does; qet_edit passes "folio" straight to the scripting API, which counts
from 0. Using the number qet_elements showed addresses the next folio, and
the op fails with nothing but "returned False".

Nothing changes for a call that works. When an op fails and the element it
names is in the project on another folio, the hint now says which index to
use. The qet_edit description and the README say how folios are counted.
Counting from 1 instead was not done: it would silently move every existing
caller's edits to another folio.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 11:20:04 +13:00
ispyisail 4278ef864e Add DXF export from the command line (--export-dxf)
qelectrotech --export-dxf <project.qet> <output_dir> [--show-terminals]
writes one DXF per folio, named <NN>_<title>.dxf like the PNG and SVG
exports. Discussion #1072.

The DXF code moves out of ExportDialog into DxfExport, unchanged except
that its options come from an ExportProperties argument instead of the
dialog. The dialog and the command line both call it, and the command
line uses the dialog's default options (the preferences' export
settings), so both write the same entities.

- Createdxf answers a file it cannot open with a message box and
  exit(0); the command line checks the file first and fails with a
  message and exit code 1 instead.
- A note is printed when pictures become outline boxes, as the dialog
  warns.
- Scripting: qet.exportDxf(outDir, showTerminals). MCP: format "dxf".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
2026-09-27 21:54:36 +13:00
ispyisail 76eea09192 qet-mcp: address texts, shapes and images by uuid; part uuids in symbols
- qet_edit: every op taking a text, shape or image "index" also takes its
  uuid, resolved at run time with textIndex()/shapeIndex()/imageIndex().
  Those are only required when a uuid is used.
- qet_element_build writes a uuid on every part (the caller's, or a new
  one) and returns them in part_uuids; qet_element_info lists part and
  terminal uuids.
- README and tests: drawing_item_view, uuid addressing, part uuids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 11:16:37 +13:00
ispyisail a9ad6e2f34 Give shapes, independent texts and images a persisted uuid
Elements, conductors, terminals and tables already carry a uuid. The
drawing furniture beside them did not, so a script or the MCP server could
only name a line, a box or a note by its index in a position-sorted list,
which shifts whenever one is added or removed.

- QetShapeItem, IndependentTextItem and DiagramImageItem get uuid(),
  newUuid() and setUuid(), read from and written to a "uuid" attribute.
- A folio loaded from a file written before this (or carrying a duplicate
  uuid) derives one from the folio uuid, the item kind and its order in the
  file, so the same file gives the same uuids on every load and a re-save
  is stable -- the #754 lesson for conductors.
- Paste and folio duplication renew them, as they already do for elements
  and conductors.
- Scripting: texts(), shapes() and images() end each line with the uuid;
  textIndex(), shapeIndex() and imageIndex() turn one back into an index.
- misc/qet-mcp: qet_diff keys texts, shapes and images on uuid when both
  sides have one, so a move or edit reads as a change to that item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:54:54 +13:00