@h2loop/autosar 1.5.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agents/autosar-integrator.md +1 -0
- package/package.json +1 -1
- package/plugins/index.js +51 -49
- package/python/ecuc_engine/add_like.py +471 -0
- package/python/ecuc_engine/add_signal.py +8 -2
- package/python/ecuc_engine/arxml_edit.py +219 -16
- package/python/ecuc_engine/cli.py +57 -5
- package/skills/autosar/SKILL.md +15 -0
|
@@ -31,6 +31,10 @@ from .model import Project
|
|
|
31
31
|
from .xmltree import Node, escape, escape_attr, line_indent, parse
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
_PRED = re.compile(r"""^([A-Za-z][\w-]*)\[\s*(?:@?[\w-]*\s*=\s*)?['"]?([^'"\]]*)['"]?\s*\]$""")
|
|
35
|
+
_SHORT = re.compile(r"[A-Za-z][A-Za-z0-9_]*\Z")
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
class ArxmlEditError(Exception):
|
|
35
39
|
pass
|
|
36
40
|
|
|
@@ -102,6 +106,10 @@ class ArxmlEditor:
|
|
|
102
106
|
self.added_tags: dict = {} # path -> tag of elements added in this call
|
|
103
107
|
self.created_packages: list = []
|
|
104
108
|
self.new_files: list = []
|
|
109
|
+
self.notes: list = []
|
|
110
|
+
self._tags: Optional[set] = None
|
|
111
|
+
self._typed: Optional[str] = None
|
|
112
|
+
self._idtags: set = set()
|
|
105
113
|
|
|
106
114
|
# ---------- files / lookup ----------
|
|
107
115
|
def tree_for(self, path: str) -> tuple:
|
|
@@ -144,15 +152,144 @@ class ArxmlEditor:
|
|
|
144
152
|
raise ArxmlEditError("a new top-level package needs \"file\" (which ARXML file to put it in); one of: "
|
|
145
153
|
+ ", ".join(self.p.rel(c) for c in cands[:20]))
|
|
146
154
|
|
|
155
|
+
# ---------- path dialects ----------
|
|
156
|
+
# Agents write XML/XPath-style paths ("/AR-PACKAGE/AR-PACKAGE[2]/APPLICATION-SW-COMPONENT-TYPE[SHORT-NAME='X']/
|
|
157
|
+
# PORTS", "/PT/Swc/TqLim/PORTS", ".../I-SIGNAL-TO-I-PDU-MAPPING/m/TRANSFER-PROPERTY"). 124 arxml_edit errors in 30
|
|
158
|
+
# held-out Qwen runs were mostly these. An AUTOSAR path is SHORT-NAMEs only: tag segments are dropped, a
|
|
159
|
+
# [SHORT-NAME='x'] predicate gives x, and a trailing collection tag is kept as the collection to add into.
|
|
160
|
+
def known_tags(self) -> set:
|
|
161
|
+
if self._tags is None:
|
|
162
|
+
tags = {"AUTOSAR", "ELEMENT"}
|
|
163
|
+
for f in self.p.sources.input + self.p.sources.config:
|
|
164
|
+
try:
|
|
165
|
+
ft = self.load(f)
|
|
166
|
+
except Exception: # noqa: BLE001
|
|
167
|
+
continue
|
|
168
|
+
for n in ft.root.iter():
|
|
169
|
+
tags.add(n.tag.upper())
|
|
170
|
+
if n.short_name is not None:
|
|
171
|
+
self._idtags.add(n.tag.upper())
|
|
172
|
+
self._tags = tags
|
|
173
|
+
return self._tags
|
|
174
|
+
|
|
175
|
+
def normalize(self, raw: str) -> tuple:
|
|
176
|
+
"""(SHORT-NAME path, trailing collection tag or None, changed?) for an XML/XPath-style path."""
|
|
177
|
+
out, trailing, changed = [], None, False
|
|
178
|
+
self._typed = None
|
|
179
|
+
tags = self.known_tags()
|
|
180
|
+
for seg in [x for x in raw.strip().strip("/").split("/") if x]:
|
|
181
|
+
m = _PRED.match(seg)
|
|
182
|
+
if m:
|
|
183
|
+
inner = m.group(2).strip()
|
|
184
|
+
changed = True
|
|
185
|
+
if not inner or inner.isdigit():
|
|
186
|
+
raise ArxmlEditError(f"path segment {seg!r} selects by position; name the element by its SHORT-NAME "
|
|
187
|
+
"instead (e.g. /PT/Swc/CruiseCtl)")
|
|
188
|
+
out.append(inner)
|
|
189
|
+
trailing = None
|
|
190
|
+
continue
|
|
191
|
+
if seg.upper() in tags or "-" in seg:
|
|
192
|
+
changed = True
|
|
193
|
+
if seg.upper() in self._idtags: # an element type (APPLICATION-SW-COMPONENT-TYPE): not a collection
|
|
194
|
+
self._typed, trailing = seg.upper(), None
|
|
195
|
+
else: # a collection (PORTS) or wrapper: add into it, if it fits
|
|
196
|
+
trailing = seg.upper() if (out or self._typed) else None
|
|
197
|
+
continue
|
|
198
|
+
out.append(seg)
|
|
199
|
+
trailing = None
|
|
200
|
+
return "/" + "/".join(out), trailing, changed
|
|
201
|
+
|
|
202
|
+
def suggest(self, path: str, k: int = 5) -> list:
|
|
203
|
+
names = [x for x in path.strip("/").split("/") if x and _SHORT.match(x)]
|
|
204
|
+
if not names:
|
|
205
|
+
return []
|
|
206
|
+
known = set(self.p.identifiables) | {q for ft in self.files.values() for q in ft.by_path}
|
|
207
|
+
last = names[-1]
|
|
208
|
+
hits = [q for q in known if q.rsplit("/", 1)[-1] == last] or \
|
|
209
|
+
[q for q in known if q.rsplit("/", 1)[-1].lower() == last.lower()]
|
|
210
|
+
hits.sort(key=lambda q: (-sum(n in q.split("/") for n in names), len(q)))
|
|
211
|
+
return hits[:k]
|
|
212
|
+
|
|
213
|
+
def not_found(self, raw: str, norm: Optional[str]) -> str:
|
|
214
|
+
msg = f"no ARXML element at {raw}"
|
|
215
|
+
if norm and norm != raw:
|
|
216
|
+
msg += f" (read as {norm}, which does not exist either)"
|
|
217
|
+
msg += (". An AUTOSAR path is the SHORT-NAMEs joined by '/', e.g. /PT/Swc/TqLim: no XML tags (AR-PACKAGE, "
|
|
218
|
+
"ELEMENTS, PORTS, APPLICATION-SW-COMPONENT-TYPE), no [SHORT-NAME='x'] predicates.")
|
|
219
|
+
sugg = self.suggest(norm or raw)
|
|
220
|
+
if sugg:
|
|
221
|
+
msg += " Did you mean: " + ", ".join(sugg)
|
|
222
|
+
return msg
|
|
223
|
+
|
|
224
|
+
def by_type(self, tag: str, file: Optional[str]) -> list:
|
|
225
|
+
trees = []
|
|
226
|
+
if file:
|
|
227
|
+
f = os.path.normpath(os.path.join(self.p.root, file))
|
|
228
|
+
for cand in self.p.sources.input + self.p.sources.config + list(self.files):
|
|
229
|
+
if os.path.realpath(cand) == os.path.realpath(f):
|
|
230
|
+
trees = [self.load(cand)]
|
|
231
|
+
break
|
|
232
|
+
else:
|
|
233
|
+
trees = [self.load(f) for f in self.p.sources.input]
|
|
234
|
+
return sorted({q for ft in trees for q, n in ft.by_path.items() if n.tag == tag})
|
|
235
|
+
|
|
236
|
+
def resolve(self, raw: str, what: str, file: Optional[str] = None) -> tuple:
|
|
237
|
+
"""(FileTree, node, SHORT-NAME path, collection hint) for a path in any of the dialects above."""
|
|
238
|
+
p = raw.rstrip("/")
|
|
239
|
+
try:
|
|
240
|
+
ft, node = self.tree_for(p)
|
|
241
|
+
return ft, node, p, None
|
|
242
|
+
except ArxmlEditError:
|
|
243
|
+
pass
|
|
244
|
+
norm, trailing, changed = self.normalize(p)
|
|
245
|
+
if changed and norm != "/":
|
|
246
|
+
try:
|
|
247
|
+
ft, node = self.tree_for(norm)
|
|
248
|
+
self.notes.append(f"{what} {raw!r} read as {norm!r}" + (f" (collection {trailing})" if trailing else ""))
|
|
249
|
+
return ft, node, norm, trailing
|
|
250
|
+
except ArxmlEditError:
|
|
251
|
+
pass
|
|
252
|
+
# only tags ("/AR-PACKAGES/AR-PACKAGE/ELEMENTS/APPLICATION-SW-COMPONENT-TYPE/PORTS"): the one element of that type
|
|
253
|
+
if changed and norm == "/" and self._typed:
|
|
254
|
+
hits = self.by_type(self._typed, file)
|
|
255
|
+
if len(hits) == 1:
|
|
256
|
+
ft, node = self.tree_for(hits[0])
|
|
257
|
+
self.notes.append(f"{what} {raw!r} read as {hits[0]!r} (the only {self._typed}"
|
|
258
|
+
+ (f" in {file}" if file else "") + ")" + (f" (collection {trailing})" if trailing else ""))
|
|
259
|
+
return ft, node, hits[0], trailing
|
|
260
|
+
if hits:
|
|
261
|
+
raise ArxmlEditError(f"{raw} names no element, and there are several {self._typed}: {', '.join(hits[:8])}; "
|
|
262
|
+
"give the SHORT-NAME path of the one you mean")
|
|
263
|
+
# package names left out ("/ExtLightCtrl/PpLightStatus"): accept the one element whose path ends that way
|
|
264
|
+
tail = "/" + norm.strip("/") if changed else "/" + p.strip("/")
|
|
265
|
+
if tail != "/":
|
|
266
|
+
known = set(self.p.identifiables) | {q for ft in self.files.values() for q in ft.by_path}
|
|
267
|
+
hits = sorted(q for q in known if q == tail or q.endswith(tail))
|
|
268
|
+
if len(hits) == 1:
|
|
269
|
+
ft, node = self.tree_for(hits[0])
|
|
270
|
+
self.notes.append(f"{what} {raw!r} read as {hits[0]!r} (the only element whose path ends with {tail})"
|
|
271
|
+
+ (f" (collection {trailing})" if trailing else ""))
|
|
272
|
+
return ft, node, hits[0], trailing
|
|
273
|
+
if len(hits) > 1:
|
|
274
|
+
raise ArxmlEditError(f"{raw} is ambiguous: several elements end with {tail}: {', '.join(hits[:8])}; give the full path")
|
|
275
|
+
raise ArxmlEditError(self.not_found(raw, norm if changed else None))
|
|
276
|
+
|
|
147
277
|
def parent_for(self, parent_path: str, file: Optional[str]) -> tuple:
|
|
148
|
-
"""(FileTree, node) of parent_path
|
|
278
|
+
"""(FileTree, node, path, collection hint) of parent_path; creates missing AR-PACKAGEs below the deepest
|
|
279
|
+
existing package, but only for a plain SHORT-NAME path (never from a tag-style or mistyped path)."""
|
|
149
280
|
pp = parent_path.rstrip("/")
|
|
281
|
+
if pp and pp != "/" and self.normalize(pp)[0] == "/" and not self._typed: # "/AUTOSAR/AR-PACKAGES": the root
|
|
282
|
+
self.notes.append(f"parent_path {parent_path!r} read as the file's top level")
|
|
283
|
+
pp = ""
|
|
150
284
|
if pp == "":
|
|
151
|
-
|
|
285
|
+
ft, node = self.root_of(file)
|
|
286
|
+
return ft, node, "", None
|
|
152
287
|
try:
|
|
153
|
-
return self.
|
|
154
|
-
except ArxmlEditError:
|
|
155
|
-
|
|
288
|
+
return self.resolve(pp, "parent_path", file)
|
|
289
|
+
except ArxmlEditError as miss:
|
|
290
|
+
segs = [x for x in pp.split("/") if x]
|
|
291
|
+
if not segs or not all(_SHORT.match(x) for x in segs) or segs[0].upper() == "AUTOSAR":
|
|
292
|
+
raise miss
|
|
156
293
|
segs = [s for s in pp.split("/") if s]
|
|
157
294
|
anc, k = "", 0
|
|
158
295
|
for i in range(len(segs) - 1, 0, -1):
|
|
@@ -170,7 +307,8 @@ class ArxmlEditor:
|
|
|
170
307
|
self.op_add({"parent_path": anc or "/", "element": "AR-PACKAGE", "short_name": s, "file": file})
|
|
171
308
|
anc = f"{anc}/{s}"
|
|
172
309
|
self.created_packages.append(anc)
|
|
173
|
-
|
|
310
|
+
ft, node = self.tree_for(pp)
|
|
311
|
+
return ft, node, pp, None
|
|
174
312
|
|
|
175
313
|
def load(self, f: str) -> FileTree:
|
|
176
314
|
if f not in self.files:
|
|
@@ -218,8 +356,14 @@ class ArxmlEditor:
|
|
|
218
356
|
for k in keys:
|
|
219
357
|
v = values[k]
|
|
220
358
|
for one in (v if isinstance(v, list) else [v]):
|
|
221
|
-
|
|
222
|
-
|
|
359
|
+
try:
|
|
360
|
+
sub = etype.find_sub_element(k, version) if etype is not None else None
|
|
361
|
+
except Exception: # noqa: BLE001 -- autosar-data rejects a name that is no AUTOSAR tag at all
|
|
362
|
+
raise ArxmlEditError(f"{k!r} is not an AUTOSAR element name (sub-elements of {tag}: "
|
|
363
|
+
f"{', '.join(o for o in order if o != 'SHORT-NAME')[:500]})") from None
|
|
364
|
+
is_ref = k.endswith(("-REF", "-TREF", "-IREF")) and isinstance(one, dict) and \
|
|
365
|
+
any(x in one for x in ("ref", "$ref", "REF", "DEST", "dest"))
|
|
366
|
+
if isinstance(one, dict) and not is_ref:
|
|
223
367
|
sn = one.get("SHORT-NAME")
|
|
224
368
|
lines.extend(self.render(k, sn, one, sub, version, depth + 1))
|
|
225
369
|
else:
|
|
@@ -228,6 +372,10 @@ class ArxmlEditor:
|
|
|
228
372
|
return lines
|
|
229
373
|
|
|
230
374
|
def leaf(self, tag: str, v) -> str:
|
|
375
|
+
if isinstance(v, dict) and not ("ref" in v) and any(k in v for k in ("$ref", "REF", "path", "#text", "value")) \
|
|
376
|
+
and (tag.endswith("-REF") or tag.endswith("-TREF") or tag.endswith("-IREF")):
|
|
377
|
+
ref = v.get("$ref") or v.get("REF") or v.get("path") or v.get("#text") or v.get("value")
|
|
378
|
+
v = {"ref": str(ref).lstrip("->").strip(), "dest": v.get("dest") or v.get("DEST")}
|
|
231
379
|
if isinstance(v, dict) and "ref" in v:
|
|
232
380
|
target, dest = str(v["ref"]), v.get("dest") or self.dest_of(str(v["ref"]))
|
|
233
381
|
if not dest:
|
|
@@ -267,10 +415,22 @@ class ArxmlEditor:
|
|
|
267
415
|
if not parent_path or not tag:
|
|
268
416
|
raise ArxmlEditError("add needs parent_path (the package or element to add under) and element (the XML "
|
|
269
417
|
"tag), e.g. " + ADD_EXAMPLE)
|
|
270
|
-
|
|
418
|
+
if tag.upper() == "SHORT-NAME":
|
|
419
|
+
raise ArxmlEditError("SHORT-NAME is not an element to add: give short_name on the add of the element itself; "
|
|
420
|
+
"to rename an element use {op:'set', path:<element path>, element:'SHORT-NAME', value:<new>}")
|
|
421
|
+
ft, parent, parent_path, coll_hint = self.parent_for(parent_path, op.get("file"))
|
|
422
|
+
if tag.upper().startswith("ECUC-") or parent.tag.startswith("ECUC-"):
|
|
423
|
+
raise ArxmlEditError(
|
|
424
|
+
"this is ECUC configuration: use ecuc_edit, not arxml_edit, e.g. {op:'add', path:'" + (parent_path or "/<Pkg>/<Module>")
|
|
425
|
+
+ "', container:'<ContainerDef>:<ShortName>', values:{<ParamDef>: <value>}} (ecuc_read schema=true lists "
|
|
426
|
+
"the containers and params that may be added there)")
|
|
271
427
|
version = _version(ft.data)
|
|
272
428
|
ptype = _etype(ft.chain(parent), version)
|
|
273
429
|
coll = op.get("collection")
|
|
430
|
+
if coll is None and coll_hint and ptype is not None:
|
|
431
|
+
hold = ptype.find_sub_element(coll_hint, version)
|
|
432
|
+
if hold is not None and hold.find_sub_element(tag, version) is not None:
|
|
433
|
+
coll = coll_hint
|
|
274
434
|
if coll is None and ptype is not None:
|
|
275
435
|
if ptype.find_sub_element(tag, version) is None:
|
|
276
436
|
for c in _order(ptype):
|
|
@@ -278,6 +438,21 @@ class ArxmlEditor:
|
|
|
278
438
|
if ct is not None and ct.find_sub_element(tag, version) is not None:
|
|
279
439
|
coll = c
|
|
280
440
|
break
|
|
441
|
+
if coll is None and parent_path:
|
|
442
|
+
holders = []
|
|
443
|
+
for q, n in ft.by_path.items():
|
|
444
|
+
if not q.startswith(parent_path + "/") or n is parent:
|
|
445
|
+
continue
|
|
446
|
+
nt = _etype(ft.chain(n), version)
|
|
447
|
+
if nt is None:
|
|
448
|
+
continue
|
|
449
|
+
if nt.find_sub_element(tag, version) is not None or any(
|
|
450
|
+
(ct := nt.find_sub_element(c, version)) is not None and ct.find_sub_element(tag, version) is not None
|
|
451
|
+
for c in _order(nt)):
|
|
452
|
+
holders.append(q)
|
|
453
|
+
if len(holders) == 1:
|
|
454
|
+
self.notes.append(f"{tag} cannot go directly under {parent_path}; added under {holders[0]}")
|
|
455
|
+
return self.op_add(dict(op, parent_path=holders[0], collection=None))
|
|
281
456
|
if coll is None:
|
|
282
457
|
raise ArxmlEditError(f"{tag} cannot be placed under {parent.tag} {parent_path} in this schema; "
|
|
283
458
|
f"{parent.tag} takes: {', '.join(_placeable(ptype, version))[:600]}")
|
|
@@ -360,8 +535,9 @@ class ArxmlEditor:
|
|
|
360
535
|
def op_set(self, op: dict):
|
|
361
536
|
path, tag = op.get("path"), op.get("element")
|
|
362
537
|
if not path or not tag or "value" not in op:
|
|
363
|
-
raise ArxmlEditError("set needs path, element
|
|
364
|
-
|
|
538
|
+
raise ArxmlEditError("set needs path (the element that holds the value), element (the XML tag of the value) "
|
|
539
|
+
"and value, e.g. {op:'set', path:'/PT/Com/ISignals/ENG_TEMP', element:'LENGTH', value:8}")
|
|
540
|
+
ft, node, _, _ = self.resolve(path, "path")
|
|
365
541
|
leaf = self.own_child(node, tag)
|
|
366
542
|
text = self.leaf(tag, op["value"]).encode("utf-8")
|
|
367
543
|
if leaf is None:
|
|
@@ -385,7 +561,9 @@ class ArxmlEditor:
|
|
|
385
561
|
|
|
386
562
|
def op_remove(self, op: dict):
|
|
387
563
|
path = (op.get("path") or "").rstrip("/")
|
|
388
|
-
|
|
564
|
+
if not path:
|
|
565
|
+
raise ArxmlEditError("remove needs path (the element to remove)")
|
|
566
|
+
ft, node, _, _ = self.resolve(path, "path")
|
|
389
567
|
start = node.start
|
|
390
568
|
if line_indent(ft.data, start) is not None:
|
|
391
569
|
nl = ft.data.rfind(b"\n", 0, start)
|
|
@@ -444,16 +622,28 @@ def _placeable(ptype, version) -> list:
|
|
|
444
622
|
return out
|
|
445
623
|
|
|
446
624
|
|
|
625
|
+
_TAG_ANYCASE = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+\Z|[A-Za-z]+\Z")
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _upper_keys(v):
|
|
629
|
+
"""Upper-case element-name keys of a values tree ("Init-Value" -> INIT-VALUE); '@'/'$' keys and ref dicts stay."""
|
|
630
|
+
if isinstance(v, list):
|
|
631
|
+
return [_upper_keys(x) for x in v]
|
|
632
|
+
if not isinstance(v, dict) or "ref" in v or "$ref" in v:
|
|
633
|
+
return v
|
|
634
|
+
return {(k.upper() if _TAG_ANYCASE.match(k) and k not in ("ref", "dest") else k): _upper_keys(x) for k, x in v.items()}
|
|
635
|
+
|
|
636
|
+
|
|
447
637
|
def normalize(op: dict, notes: list, i: int) -> dict:
|
|
448
|
-
"""Accept the shapes agents mix up (ecuc_edit's path/container, name/tag
|
|
449
|
-
if op.get("op")
|
|
638
|
+
"""Accept the shapes agents mix up (ecuc_edit's path/container/param, name/tag, any-case tags); note each rewrite."""
|
|
639
|
+
if op.get("op") not in ("add", "set"):
|
|
450
640
|
return op
|
|
451
641
|
op = dict(op)
|
|
452
|
-
if not op.get("parent_path") and op.get("path"):
|
|
642
|
+
if op.get("op") == "add" and not op.get("parent_path") and op.get("path"):
|
|
453
643
|
op["parent_path"] = op.pop("path")
|
|
454
644
|
notes.append(f"op {i}: 'path' read as parent_path")
|
|
455
645
|
cont = op.get("container")
|
|
456
|
-
if not op.get("element") and isinstance(cont, str):
|
|
646
|
+
if op.get("op") == "add" and not op.get("element") and isinstance(cont, str):
|
|
457
647
|
tag, _, sn = cont.partition(":")
|
|
458
648
|
if _TAG.match(tag):
|
|
459
649
|
op.pop("container")
|
|
@@ -465,6 +655,15 @@ def normalize(op: dict, notes: list, i: int) -> dict:
|
|
|
465
655
|
if alias in op and not op.get(key):
|
|
466
656
|
op[key] = op.pop(alias)
|
|
467
657
|
notes.append(f"op {i}: {alias!r} read as {key!r}")
|
|
658
|
+
if op.get("op") == "set" and not op.get("element") and op.get("param"):
|
|
659
|
+
op["element"] = op.pop("param")
|
|
660
|
+
notes.append(f"op {i}: 'param' read as 'element'")
|
|
661
|
+
el = op.get("element")
|
|
662
|
+
if isinstance(el, str) and _TAG_ANYCASE.match(el) and el != el.upper():
|
|
663
|
+
op["element"] = el.upper()
|
|
664
|
+
notes.append(f"op {i}: element {el!r} read as {el.upper()!r} (AUTOSAR tags are upper case)")
|
|
665
|
+
if isinstance(op.get("values"), dict):
|
|
666
|
+
op["values"] = _upper_keys(op["values"])
|
|
468
667
|
vals = op.get("values")
|
|
469
668
|
if isinstance(vals, dict) and "SHORT-NAME" in vals and not op.get("short_name"):
|
|
470
669
|
vals = dict(vals)
|
|
@@ -483,6 +682,7 @@ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
|
483
682
|
created = []
|
|
484
683
|
notes: list = []
|
|
485
684
|
ops = [normalize(o, notes, i) for i, o in enumerate(ops)]
|
|
685
|
+
ed.notes = notes
|
|
486
686
|
try:
|
|
487
687
|
for i, op in enumerate(ops):
|
|
488
688
|
kind = op.get("op")
|
|
@@ -499,6 +699,9 @@ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
|
|
|
499
699
|
raise ArxmlEditError(f"unknown op {kind!r} (new_file/add/set/remove)")
|
|
500
700
|
except ArxmlEditError as e:
|
|
501
701
|
raise ArxmlEditError(f"op {i} ({kind} {op.get('path') or op.get('parent_path') or op.get('file')}): {e}") from None
|
|
702
|
+
except Exception as e: # noqa: BLE001 -- never let a library error escape as a bare engine failure
|
|
703
|
+
raise ArxmlEditError(f"op {i} ({kind} {op.get('path') or op.get('parent_path') or op.get('file')}): "
|
|
704
|
+
f"{type(e).__name__}: {e}") from None
|
|
502
705
|
except ArxmlEditError as e:
|
|
503
706
|
return {"ok": False, "error": str(e), "written": False, "notes": notes}
|
|
504
707
|
problems = ed.check()
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
|
+
import re
|
|
11
12
|
import os
|
|
12
13
|
import sys
|
|
13
14
|
import time
|
|
@@ -15,16 +16,44 @@ import time
|
|
|
15
16
|
from . import ENGINE_VERSION
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
REPAIRED: list = []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _loads(text: str):
|
|
23
|
+
"""json.loads, then the slips models make in long hand-written JSON: trailing commas, missing commas between
|
|
24
|
+
objects/values, Python literals (True/None, single quotes). The first error is re-raised if nothing parses."""
|
|
25
|
+
try:
|
|
26
|
+
return json.loads(text)
|
|
27
|
+
except json.JSONDecodeError as first:
|
|
28
|
+
fixed = re.sub(r",\s*([}\]])", r"\1", text)
|
|
29
|
+
fixed = re.sub(r"([}\]\"])\s*\n?\s*([{\"])", lambda m: m.group(1) + ("," if m.group(2) in "{\"" else "") + m.group(2), fixed) \
|
|
30
|
+
if fixed.count('"') % 2 == 0 else fixed
|
|
31
|
+
for cand in (fixed, ):
|
|
32
|
+
try:
|
|
33
|
+
v = json.loads(cand)
|
|
34
|
+
REPAIRED.append(f"ops JSON had a syntax error ({first.msg} at char {first.pos}) and was repaired; check the result")
|
|
35
|
+
return v
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
pass
|
|
38
|
+
try:
|
|
39
|
+
import ast
|
|
40
|
+
v = ast.literal_eval(text.replace("true", "True").replace("false", "False").replace("null", "None"))
|
|
41
|
+
REPAIRED.append(f"ops were not strict JSON ({first.msg} at char {first.pos}) and were read as a literal")
|
|
42
|
+
return v
|
|
43
|
+
except Exception: # noqa: BLE001
|
|
44
|
+
raise first from None
|
|
45
|
+
|
|
46
|
+
|
|
18
47
|
def _ops(arg: str):
|
|
19
48
|
if arg.startswith("@"):
|
|
20
49
|
with open(arg[1:]) as f:
|
|
21
50
|
arg = f.read()
|
|
22
51
|
if arg == "-":
|
|
23
52
|
arg = sys.stdin.read()
|
|
24
|
-
ops =
|
|
53
|
+
ops = _loads(arg)
|
|
25
54
|
for _ in range(3): # tolerate JSON that was encoded more than once ("[{...}]" as a string)
|
|
26
55
|
if isinstance(ops, str):
|
|
27
|
-
ops =
|
|
56
|
+
ops = _loads(ops)
|
|
28
57
|
if isinstance(ops, dict) and isinstance(ops.get("ops"), (list, str)):
|
|
29
58
|
return _ops(json.dumps(ops["ops"]))
|
|
30
59
|
return ops if isinstance(ops, list) else [ops]
|
|
@@ -78,6 +107,14 @@ def main(argv=None) -> int:
|
|
|
78
107
|
an.add_argument("--dry-run", action="store_true")
|
|
79
108
|
ac = sub.add_parser("arxml-check", help="verify non-ECUC ARXML: strict schema + every reference and its DEST")
|
|
80
109
|
ac.add_argument("--file", action="append", default=[], help="limit to files whose path contains this")
|
|
110
|
+
al = sub.add_parser("add-like", help="add a new X like an existing Y: clone its chain across all ARXML, renamed")
|
|
111
|
+
al.add_argument("--like", required=True, help="name stem of the template (e.g. BCM_DoorStatus)")
|
|
112
|
+
al.add_argument("--name", required=True, help="the new stem (e.g. BCM_SettingsAck)")
|
|
113
|
+
al.add_argument("--seed", help="one element of the template chain, when several chains carry the stem")
|
|
114
|
+
al.add_argument("--also", help='JSON {"template member stem": "new stem"}, e.g. {"DrvDoorOpenSts": "SettingsAckCounter"}')
|
|
115
|
+
al.add_argument("--set", dest="set_values", help='JSON {"ParamDef"|"XML-TAG"|"Owner:key": value}')
|
|
116
|
+
al.add_argument("--keep-members", action="store_true")
|
|
117
|
+
al.add_argument("--dry-run", action="store_true")
|
|
81
118
|
fd = sub.add_parser("find")
|
|
82
119
|
fd.add_argument("--module")
|
|
83
120
|
fd.add_argument("--container-def", dest="container_def")
|
|
@@ -123,7 +160,7 @@ def main(argv=None) -> int:
|
|
|
123
160
|
bc.add_argument("--task", required=True, help="task text, @file, or - for stdin")
|
|
124
161
|
bc.add_argument("--budget-tokens", dest="budget_tokens", type=int, default=24000)
|
|
125
162
|
bc.add_argument("--no-history", dest="history", action="store_false")
|
|
126
|
-
for p in (r, e, v, d, au, so, vs, si, fd, cx, ae, an, ac, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
|
|
163
|
+
for p in (r, e, v, d, au, so, vs, si, fd, cx, ae, an, ac, al, asg, cg, cd, ra, vr, bc): # allow --json after the subcommand too
|
|
127
164
|
p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
|
|
128
165
|
a = ap.parse_args(argv)
|
|
129
166
|
t0 = time.time()
|
|
@@ -193,7 +230,7 @@ def run(a) -> dict:
|
|
|
193
230
|
from .specids import check_ids
|
|
194
231
|
return check_ids(os.path.abspath(a.root), a.ids, docs=a.docs)
|
|
195
232
|
from .model import Project
|
|
196
|
-
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit", "find", "crosscheck", "signal-owners", "arxml-edit", "add-signal", "arxml-check"))
|
|
233
|
+
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit", "find", "crosscheck", "signal-owners", "arxml-edit", "add-signal", "arxml-check", "add-like"))
|
|
197
234
|
if a.cmd == "add-signal":
|
|
198
235
|
from .add_signal import AddSignalError, add_signal
|
|
199
236
|
try:
|
|
@@ -202,12 +239,27 @@ def run(a) -> dict:
|
|
|
202
239
|
transfer_property=a.transfer_property, dry_run=a.dry_run)
|
|
203
240
|
except AddSignalError as ex:
|
|
204
241
|
return {"error": str(ex), "written": False}
|
|
242
|
+
if a.cmd == "add-like":
|
|
243
|
+
from .add_like import AddLikeError, add_like
|
|
244
|
+
try:
|
|
245
|
+
also = _loads(a.also) if a.also else None
|
|
246
|
+
sv = _loads(a.set_values) if a.set_values else None
|
|
247
|
+
except json.JSONDecodeError as ex:
|
|
248
|
+
return {"ok": False, "error": f"also/set is not valid JSON: {ex}"}
|
|
249
|
+
try:
|
|
250
|
+
return add_like(project, a.like, a.name, seed=a.seed, set_values=sv, keep_members=a.keep_members,
|
|
251
|
+
dry_run=a.dry_run, also=also)
|
|
252
|
+
except AddLikeError as ex:
|
|
253
|
+
return {"ok": False, "error": str(ex)}
|
|
205
254
|
if a.cmd == "arxml-check":
|
|
206
255
|
from .arxml_edit import arxml_check
|
|
207
256
|
return arxml_check(project, a.file or None)
|
|
208
257
|
if a.cmd == "arxml-edit":
|
|
209
258
|
from .arxml_edit import arxml_edit
|
|
210
|
-
|
|
259
|
+
res = arxml_edit(project, a.ops, dry_run=a.dry_run)
|
|
260
|
+
if REPAIRED:
|
|
261
|
+
res.setdefault("notes", []).extend(REPAIRED)
|
|
262
|
+
return res
|
|
211
263
|
if a.cmd == "find":
|
|
212
264
|
from .find import find
|
|
213
265
|
where = a.where
|
package/skills/autosar/SKILL.md
CHANGED
|
@@ -71,6 +71,7 @@ grounding, no tools, no exploring the project.
|
|
|
71
71
|
| Verify ALL ARXML (ECUC rules + extract/SWC schema, references, DEST) | `ecuc_validate` (unfiltered) |
|
|
72
72
|
| Edit System extract / SWC / interface XML | `arxml_edit` (DEST inferred, schema-ordered, refs checked) |
|
|
73
73
|
| Add a signal end to end (ECUC + extract + DBC) | `autosar_add_signal` |
|
|
74
|
+
| Add a new message / port / runnable+event / Dem event / NvM block / interface **like an existing one**, in every file at once | `autosar_add_like like=<existing stem> name=<new stem> also={member: new} set={...}` |
|
|
74
75
|
| What may be set/added here (types, ranges, literals, multiplicity) | `ecuc_read path=… schema=true`, or a def path `/AUTOSAR/EcucDefs/<Mod>/…` |
|
|
75
76
|
| Change config | `ecuc_edit ops=[…]` (`dry_run` first for large edits) |
|
|
76
77
|
| Check config | `ecuc_validate` |
|
|
@@ -123,6 +124,20 @@ Do not decide what to change from memory or from the prompt alone. Each change g
|
|
|
123
124
|
(unvalidated edits, failing validation, unverified C, changed elements not yet checked against the documents)
|
|
124
125
|
until it is resolved.
|
|
125
126
|
|
|
127
|
+
## A new thing that resembles an existing one: autosar_add_like (preferred)
|
|
128
|
+
Most changes add something the project already has one of: a message, a port, a runnable with its timing event, a
|
|
129
|
+
Dem event with its DTC, an NvM block with its Fee block. Do not build those op by op. Pick the existing one most like
|
|
130
|
+
the new one (same direction, bus, kind), then:
|
|
131
|
+
1. `autosar_add_like like=<its stem> name=<new stem> dry_run=true`: read the Created list (every file it touches)
|
|
132
|
+
and the "Not copied" / "single-valued" lists.
|
|
133
|
+
2. Name the members you want too: `also={"DrvDoorOpenSts": "SettingsAckCounter"}` gives the new message one signal
|
|
134
|
+
cloned from that one.
|
|
135
|
+
3. Set the values the request gives with `set` (ECUC param name, XML tag, or `Owner:key` when several clones have
|
|
136
|
+
it): CAN id (`IDENTIFIER`, `CanIfTxPduCanId`), lengths (`PduLength`, `FRAME-LENGTH`, `I-SIGNAL-I-PDU:LENGTH`),
|
|
137
|
+
period (`ComTxModeTimePeriod`), signal layout (`<sig>:ComBitPosition`, `<sig>:ComBitSize`, `START-POSITION`) ...
|
|
138
|
+
4. Run it without dry_run. It validates and rolls back on any new finding. Then fix what is listed as single-valued
|
|
139
|
+
(e.g. the S/R-to-signal mapping of a new port) with arxml_edit, and verify as below.
|
|
140
|
+
|
|
126
141
|
## New ARXML file (SWC, interface, data types, extract) from nothing
|
|
127
142
|
1. ONE `arxml_edit` call. Its first op creates the file:
|
|
128
143
|
`{op:"new_file", file:"Input/SWC_X.arxml", packages:["/DataTypes", "/Interfaces", "/Swcs"]}` (R25-11 header and empty
|