mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-29 05:44:14 +02:00
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>
This commit is contained in:
@@ -359,6 +359,10 @@ Python, plus the hang guard on `addConductor` and the database refresh in
|
||||
an index would shift. A folio saved without a uuid shows it empty:
|
||||
QElectroTech gives it one on load and writes it on the next save, so it
|
||||
appears after a first `qet_edit`. Needs `qet.folioIndex()` in the build.
|
||||
The `"$id"` of an `add_folio` or `insert_folio` works the same way: it
|
||||
keeps naming that folio after a later `insert_folio` or `remove_folio` in
|
||||
the same run (on a build without `qet.folioUuid()`, it is the index the
|
||||
folio had when it was made, as before).
|
||||
- **A conductor can be named by its uuid** (`qet_conductors` reports it):
|
||||
`set_conductor`, `move_conductor_segment` and `delete_conductor` take
|
||||
`"conductor": "{uuid}"` in place of `element` + `terminal`, which works
|
||||
|
||||
+19
-1
@@ -1430,6 +1430,10 @@ SEARCH_REPLACE_KINDS = ["element_info", "conductor", "text"]
|
||||
FOLIO_PROPERTIES = ["title", "author", "filename", "plant", "locmach",
|
||||
"indexrev", "folio", "template"]
|
||||
|
||||
# The ops that make a folio: their "$id" is an index the next insert_folio
|
||||
# or remove_folio can shift, so the script also keeps the folio's uuid.
|
||||
FOLIO_MAKING_OPS = ("add_folio", "insert_folio")
|
||||
|
||||
# The ops that address one conductor by element + terminal, and so also
|
||||
# take "conductor": "{uuid}" (qet_conductors reports each one's uuid).
|
||||
CONDUCTOR_UUID_OPS = ("set_conductor", "move_conductor_segment", "delete_conductor")
|
||||
@@ -1469,6 +1473,9 @@ def _build_script(operations: list, output: str) -> str:
|
||||
JavaScript exception.
|
||||
"""
|
||||
refs: set[str] = set()
|
||||
# "$name"s made by an op that creates a folio: held as the folio's uuid
|
||||
# too, since a later insert_folio or remove_folio shifts its index.
|
||||
folio_refs: set[str] = set()
|
||||
# Resolvers a uuid reference needs; required only when one is used, so
|
||||
# an index-only edit still runs on a build that predates them.
|
||||
uuid_methods: set[str] = set()
|
||||
@@ -1478,6 +1485,7 @@ def _build_script(operations: list, output: str) -> str:
|
||||
lines = [
|
||||
"// generated by qet-mcp; do not edit",
|
||||
"var R = {};", # $name -> value from an earlier op
|
||||
"var F = {};", # $name -> uuid of a folio an op made
|
||||
"var missing = [];",
|
||||
"var need = @NEED@;",
|
||||
"for (var i = 0; i < need.length; i++) {",
|
||||
@@ -1550,6 +1558,11 @@ def _build_script(operations: list, output: str) -> str:
|
||||
raise ValueError(
|
||||
f"operation {op_index} refers to {value!r}, which no earlier "
|
||||
f"operation defined (set \"id\": {name!r} on the op that creates it)")
|
||||
if kind == "folio" and name in folio_refs:
|
||||
# the folio's index now, not when it was made; the stored
|
||||
# index on a build that cannot report folio uuids
|
||||
ref = _js(name)
|
||||
return f"(F[{ref}] ? qet.folioIndex(F[{ref}]) : R[{ref}])"
|
||||
return f"R[{_js(name)}]"
|
||||
if kind == "num":
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
@@ -1752,6 +1765,10 @@ def _build_script(operations: list, output: str) -> str:
|
||||
if ident is not None:
|
||||
lines.append(f" R[{_js(ident)}] = v{i};")
|
||||
refs.add(ident)
|
||||
if name in FOLIO_MAKING_OPS:
|
||||
lines.append(f" F[{_js(ident)}] = (typeof qet.folioUuid === 'function' "
|
||||
f"&& v{i} >= 0) ? qet.folioUuid(v{i}) : '';")
|
||||
folio_refs.add(ident)
|
||||
lines.append(
|
||||
f" qet.log({_js(_MARKER)} + JSON.stringify("
|
||||
f"{{kind: 'op', index: {i}, op: {_js(name)}, "
|
||||
@@ -2669,7 +2686,8 @@ TOOLS = [
|
||||
"qet_project_info call 1 is \"folio\": 0 here. A folio can "
|
||||
"be given as its uuid instead (qet_project_info lists them), "
|
||||
"which still names the same folio after an earlier op adds or "
|
||||
"removes one. A terminal (\"terminal\", \"from_terminal\", "
|
||||
"removes one; so does the \"$id\" of an add_folio or "
|
||||
"insert_folio. A terminal (\"terminal\", \"from_terminal\", "
|
||||
"\"to_terminal\") is given by its index -- qet_element_info "
|
||||
"lists terminals in index order, top to bottom then left to "
|
||||
"right -- or by its uuid, which qet_element_info also lists and "
|
||||
|
||||
@@ -184,7 +184,8 @@ class EditValidation(unittest.TestCase):
|
||||
s = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "add_element", "id": "k", "folio": "$f", "path": "p", "x": 0, "y": 0},
|
||||
{"op": "delete_element_text", "folio": "$f", "element": "$k", "index": U}])
|
||||
self.assertIn(f'qet.deleteElementText(R["f"], R["k"], qet.elementTextIndex(R["f"], R["k"], "{U}"))', s)
|
||||
f = '(F["f"] ? qet.folioIndex(F["f"]) : R["f"])' # a folio "$name": see test_folio_ref_follows_the_folio
|
||||
self.assertIn(f'qet.deleteElementText({f}, R["k"], qet.elementTextIndex({f}, R["k"], "{U}"))', s)
|
||||
# the lookups are required only when a uuid is used
|
||||
self.assertIn('"tableIndex"', self.build([{"op": "delete_table", "folio": 0, "table": U}]))
|
||||
self.assertNotIn('"tableIndex"', self.build([{"op": "delete_table", "folio": 0, "table": 0}]))
|
||||
@@ -214,6 +215,27 @@ class EditValidation(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "folio index, its uuid"):
|
||||
self.build([{"op": "set_folio", "folio": "first", "property": "author", "value": "a"}])
|
||||
|
||||
def test_folio_ref_follows_the_folio(self):
|
||||
"""A "$name" made by add_folio or insert_folio is looked up by the
|
||||
folio's uuid when used as a folio, since a later insert or removal
|
||||
shifts its index; the stored index is the fallback on a build that
|
||||
cannot report folio uuids. Used as anything else, it is unchanged."""
|
||||
s = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "insert_folio", "id": "g", "position": 0},
|
||||
{"op": "set_folio_title", "folio": "$f", "title": "t"},
|
||||
{"op": "set_folio_title", "folio": "$g", "title": "u"}])
|
||||
self.assertIn("F[\"f\"] = (typeof qet.folioUuid === 'function' && v0 >= 0) "
|
||||
"? qet.folioUuid(v0) : '';", s)
|
||||
self.assertIn('qet.setFolioTitle((F["f"] ? qet.folioIndex(F["f"]) : R["f"]), "t")', s)
|
||||
self.assertIn('qet.setFolioTitle((F["g"] ? qet.folioIndex(F["g"]) : R["g"]), "u")', s)
|
||||
# not required: an edit still runs on a build without folio uuids
|
||||
self.assertNotIn('"folioUuid"', s)
|
||||
# a "$name" from any other op is untouched
|
||||
s = self.build([{"op": "add_text", "id": "t", "folio": 0, "text": "x", "x": 0, "y": 0},
|
||||
{"op": "delete_text", "folio": 0, "index": "$t"}])
|
||||
self.assertIn('R["t"]', s)
|
||||
self.assertNotIn("F[", s.split("if (missing.length === 0) {")[1])
|
||||
|
||||
def test_conductor_by_uuid(self):
|
||||
"""A conductor named by uuid is turned into one of its ends at run
|
||||
time; the lookup is required only then, and "conductor" cannot be
|
||||
@@ -267,7 +289,8 @@ class EditValidation(unittest.TestCase):
|
||||
s = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "add_element", "id": "a", "folio": "$f", "path": "x.elmt", "x": 0, "y": 0},
|
||||
{"op": "delete_conductor", "folio": "$f", "element": "$a", "terminal": T}])
|
||||
self.assertIn(f'qet.deleteConductor(R["f"], R["a"], qetMcpTerminal(2, "terminal", R["f"], R["a"], "{T}"))', s)
|
||||
f = '(F["f"] ? qet.folioIndex(F["f"]) : R["f"])' # a folio "$name": see test_folio_ref_follows_the_folio
|
||||
self.assertIn(f'qet.deleteConductor({f}, R["a"], qetMcpTerminal(2, "terminal", {f}, R["a"], "{T}"))', s)
|
||||
s = self.build([{"op": "delete_conductor", "folio": V, "element": E, "terminal": T}])
|
||||
self.assertIn(f'qetMcpTerminal(0, "terminal", qet.folioIndex("{V}"), "{E}", "{T}")', s)
|
||||
# an index is unchanged and needs no lookup
|
||||
@@ -521,7 +544,8 @@ class EditValidation(unittest.TestCase):
|
||||
script = self.build([{"op": "add_folio", "id": "f"},
|
||||
{"op": "set_folio_title", "folio": "$f", "title": nasty}])
|
||||
# the literal must be valid JSON, so JavaScript reads exactly what was sent
|
||||
literal = re.search(r'setFolioTitle\(R\["f"\], (".*?")\)', script, re.S).group(1)
|
||||
literal = re.search(r'setFolioTitle\(\(F\["f"\] \? qet\.folioIndex\(F\["f"\]\) : R\["f"\]\), (".*?")\)',
|
||||
script, re.S).group(1)
|
||||
self.assertEqual(json.loads(literal), nasty)
|
||||
|
||||
def test_edit_refuses_in_place_and_empty(self):
|
||||
@@ -2355,6 +2379,19 @@ class Integration(unittest.TestCase):
|
||||
note = r["operations"][1].get("note", "")
|
||||
self.assertIn("from_terminal: no terminal", note)
|
||||
self.assertIn("to_terminal: no terminal", note)
|
||||
def test_folio_ref_survives_an_insert_before_it(self):
|
||||
"""add_folio names a folio "$f"; inserting another at position 0
|
||||
moves it from index 1 to 2. "$f" must still name it."""
|
||||
base = self.sb.new()
|
||||
r = self.ok(self.sb.edit(base, [
|
||||
{"op": "add_folio", "id": "f"},
|
||||
{"op": "insert_folio", "position": 0},
|
||||
{"op": "set_folio_title", "folio": "$f", "title": "MINE"}]))
|
||||
self.assertEqual(r["operations"][0]["result"], 1)
|
||||
titles = [f["title"] for f in m.tool_project_info(r["output"])["folios"]]
|
||||
self.assertEqual(len(titles), 3)
|
||||
self.assertEqual(titles[2], "MINE", titles)
|
||||
self.assertNotIn("MINE", titles[:2])
|
||||
|
||||
def test_noop_edit_has_no_conductor_churn(self):
|
||||
"""Re-saving renumbers the file's terminal ids; the diff must not
|
||||
|
||||
Reference in New Issue
Block a user