@h2loop/autosar 0.1.2 → 1.1.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.
Files changed (49) hide show
  1. package/README.md +4 -0
  2. package/agents/autosar-integrator.md +34 -2
  3. package/package.json +4 -1
  4. package/plugins/index.js +53 -23
  5. package/python/ecuc_engine/__init__.py +1 -1
  6. package/python/ecuc_engine/add_signal.py +351 -0
  7. package/python/ecuc_engine/arxml_edit.py +387 -0
  8. package/python/ecuc_engine/audit.py +372 -69
  9. package/python/ecuc_engine/cli.py +119 -10
  10. package/python/ecuc_engine/codegen/__init__.py +185 -0
  11. package/python/ecuc_engine/codegen/binding.py +210 -0
  12. package/python/ecuc_engine/codegen/bindings/sip_standin.json +111 -0
  13. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  14. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  15. package/python/ecuc_engine/codegen/emit.py +395 -0
  16. package/python/ecuc_engine/codegen/handles.py +89 -0
  17. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  18. package/python/ecuc_engine/codegen/model.py +259 -0
  19. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  20. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  21. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  22. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  23. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  24. package/python/ecuc_engine/codegen/registry.py +133 -0
  25. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  26. package/python/ecuc_engine/context.py +481 -0
  27. package/python/ecuc_engine/csrc.py +189 -0
  28. package/python/ecuc_engine/defs.py +30 -0
  29. package/python/ecuc_engine/diff.py +13 -5
  30. package/python/ecuc_engine/find.py +162 -0
  31. package/python/ecuc_engine/gen.py +105 -0
  32. package/python/ecuc_engine/history.py +146 -0
  33. package/python/ecuc_engine/model.py +33 -29
  34. package/python/ecuc_engine/paths.py +221 -0
  35. package/python/ecuc_engine/probe.py +229 -0
  36. package/python/ecuc_engine/rte_api.py +935 -0
  37. package/python/ecuc_engine/rules/__init__.py +5 -3
  38. package/python/ecuc_engine/rules/mem.py +84 -0
  39. package/python/ecuc_engine/rules/nvm.py +131 -0
  40. package/python/ecuc_engine/rules/pdur.py +87 -0
  41. package/python/ecuc_engine/rules/system.py +162 -0
  42. package/python/ecuc_engine/sysmodel.py +152 -0
  43. package/python/ecuc_engine/validate.py +52 -3
  44. package/python/ecuc_engine/verify_rte.py +445 -0
  45. package/python/pyproject.toml +1 -1
  46. package/skills/autosar/SKILL.md +57 -1
  47. package/skills/autosar/references/codegen.md +30 -0
  48. package/skills/autosar/references/project-layout.md +11 -2
  49. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,387 @@
1
+ """Structured, in-place edits of non-ECUC ARXML (System/ECU extract, SWCs, interfaces).
2
+
3
+ Ops (paths are AUTOSAR paths of identifiables; tags are XML element names):
4
+ {"op": "add", "parent_path": "/Communication/ISignals", "element": "I-SIGNAL", "short_name": "X",
5
+ "values": {"LENGTH": 8, "SYSTEM-SIGNAL-REF": "->/Communication/SystemSignals/X",
6
+ "INIT-VALUE": {"NUMERICAL-VALUE-SPECIFICATION": {"VALUE": 0}}}}
7
+ {"op": "set", "path": "/Communication/Pdus/P/X", "element": "START-POSITION", "value": 40}
8
+ {"op": "remove", "path": "/Communication/ISignals/X"}
9
+
10
+ - The aggregating collection (ELEMENTS, I-SIGNAL-TO-PDU-MAPPINGS, ...) is inferred from the AUTOSAR schema
11
+ (autosar-data) when not given; children are written in schema order.
12
+ - Every *-REF / *-TREF gets its DEST from the referenced element's actual tag ("->/path"), looked up in all
13
+ loaded ARXML (config, extract, and the elements added in the same call). A target that does not exist is an
14
+ error, unless given explicitly as {"ref": "/path", "dest": "TAG"}.
15
+ - Bytes outside the inserted/changed spans are untouched; the file is strictly re-loaded with autosar-data
16
+ afterwards and nothing is written if any op or check fails.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import difflib
21
+ import os
22
+ import re
23
+ from typing import Optional
24
+
25
+ from .model import Project
26
+ from .xmltree import Node, escape, escape_attr, line_indent, parse
27
+
28
+
29
+ class ArxmlEditError(Exception):
30
+ pass
31
+
32
+
33
+ def _ad():
34
+ try:
35
+ import autosar_data as ad # type: ignore
36
+ return ad
37
+ except Exception: # noqa: BLE001
38
+ return None
39
+
40
+
41
+ def _version(data: bytes):
42
+ ad = _ad()
43
+ if ad is None:
44
+ return None
45
+ m = re.search(rb"AUTOSAR_([0-9A-Za-z_-]+)\.xsd", data[:2000])
46
+ name = "AUTOSAR_" + (m.group(1).decode().replace("-", "_") if m else "00054")
47
+ return getattr(ad.AutosarVersion, name, None) or ad.AutosarVersion.AUTOSAR_00054
48
+
49
+
50
+ class FileTree:
51
+ def __init__(self, path: str, data: bytes):
52
+ self.path, self.data = path, data
53
+ self.root = parse(data)
54
+ self.by_path: dict = {}
55
+ self._index(self.root, "")
56
+ self.nl = b"\r\n" if b"\r\n" in data[:4096] else b"\n"
57
+
58
+ def _index(self, n: Node, prefix: str):
59
+ sn = n.short_name
60
+ p = f"{prefix}/{sn}" if sn is not None else prefix
61
+ if sn is not None:
62
+ self.by_path.setdefault(p, n)
63
+ for c in n.children:
64
+ self._index(c, p)
65
+
66
+ def chain(self, node: Node) -> list:
67
+ out = []
68
+ while node is not None:
69
+ out.append(node.tag)
70
+ node = node.parent
71
+ return list(reversed(out))
72
+
73
+
74
+ def _etype(tags: list, version):
75
+ """ElementType for a tag chain from the AUTOSAR root (None without autosar-data)."""
76
+ ad = _ad()
77
+ if ad is None or version is None:
78
+ return None
79
+ m = ad.AutosarModel()
80
+ t = m.root_element.element_type
81
+ for tag in tags[1:]:
82
+ t = t.find_sub_element(tag, version)
83
+ if t is None:
84
+ return None
85
+ return t
86
+
87
+
88
+ def _order(etype) -> list:
89
+ return [s.element_name for s in etype.sub_elements_spec] if etype is not None else []
90
+
91
+
92
+ class ArxmlEditor:
93
+ def __init__(self, project: Project):
94
+ self.p = project
95
+ self.files: dict = {}
96
+ self.original: dict = {}
97
+ self.added_tags: dict = {} # path -> tag of elements added in this call
98
+
99
+ # ---------- files / lookup ----------
100
+ def tree_for(self, path: str) -> tuple:
101
+ """(FileTree, node) holding the identifiable ``path``."""
102
+ for ft in self.files.values():
103
+ if path in ft.by_path:
104
+ return ft, ft.by_path[path]
105
+ hit = self.p.identifiables.get(path)
106
+ f = hit[2] if hit and len(hit) > 2 else None
107
+ if f is None:
108
+ for cand in self.p.sources.input + self.p.sources.config:
109
+ ft = self.load(cand)
110
+ if path in ft.by_path:
111
+ return ft, ft.by_path[path]
112
+ raise ArxmlEditError(f"no ARXML element at {path}")
113
+ ft = self.load(f)
114
+ if path not in ft.by_path:
115
+ raise ArxmlEditError(f"{path} not found in {self.p.rel(f)}")
116
+ return ft, ft.by_path[path]
117
+
118
+ def load(self, f: str) -> FileTree:
119
+ if f not in self.files:
120
+ with open(f, "rb") as fh:
121
+ data = fh.read()
122
+ self.original.setdefault(f, data)
123
+ self.files[f] = FileTree(f, data)
124
+ return self.files[f]
125
+
126
+ def patch(self, ft: FileTree, start: int, end: int, repl: bytes):
127
+ data = ft.data[:start] + repl + ft.data[end:]
128
+ self.files[ft.path] = FileTree(ft.path, data)
129
+
130
+ def dest_of(self, target: str) -> Optional[str]:
131
+ if target in self.added_tags:
132
+ return self.added_tags[target]
133
+ hit = self.p.identifiables.get(target)
134
+ if hit:
135
+ return hit[0]
136
+ for ft in self.files.values():
137
+ n = ft.by_path.get(target)
138
+ if n is not None:
139
+ return n.tag
140
+ return None
141
+
142
+ # ---------- rendering ----------
143
+ def render(self, tag: str, short_name: Optional[str], values, etype, version, depth: int) -> list:
144
+ lines = []
145
+ attrs = ""
146
+ if isinstance(values, dict) and "@attrs" in values:
147
+ attrs = "".join(f' {k}="{escape_attr(str(v))}"' for k, v in values["@attrs"].items())
148
+ if not isinstance(values, dict):
149
+ return [(depth, self.leaf(tag, values))]
150
+ lines.append((depth, f"<{tag}{attrs}>"))
151
+ if short_name is not None:
152
+ lines.append((depth + 1, f"<SHORT-NAME>{escape(short_name)}</SHORT-NAME>"))
153
+ order = _order(etype)
154
+ keys = [k for k in values if not k.startswith("@") and k != "SHORT-NAME"]
155
+ if order:
156
+ unknown = [k for k in keys if k not in order]
157
+ if unknown:
158
+ raise ArxmlEditError(f"{tag} has no sub-element(s) {unknown} in this AUTOSAR schema; allowed: "
159
+ f"{', '.join(o for o in order if o != 'SHORT-NAME')[:600]}")
160
+ keys.sort(key=order.index)
161
+ for k in keys:
162
+ v = values[k]
163
+ for one in (v if isinstance(v, list) else [v]):
164
+ sub = etype.find_sub_element(k, version) if etype is not None else None
165
+ if isinstance(one, dict) and "ref" not in one:
166
+ sn = one.get("SHORT-NAME")
167
+ lines.extend(self.render(k, sn, one, sub, version, depth + 1))
168
+ else:
169
+ lines.append((depth + 1, self.leaf(k, one)))
170
+ lines.append((depth, f"</{tag}>"))
171
+ return lines
172
+
173
+ def leaf(self, tag: str, v) -> str:
174
+ if isinstance(v, dict) and "ref" in v:
175
+ target, dest = str(v["ref"]), v.get("dest") or self.dest_of(str(v["ref"]))
176
+ if not dest:
177
+ raise ArxmlEditError(f"{tag} -> {target}: target does not exist (add it first, or give "
178
+ "{{\"ref\": ..., \"dest\": \"TAG\"}})")
179
+ return f'<{tag} DEST="{escape_attr(dest)}">{escape(target)}</{tag}>'
180
+ if isinstance(v, str) and v.startswith("->"):
181
+ target = v[2:].strip()
182
+ if not (tag.endswith("-REF") or tag.endswith("-TREF") or tag.endswith("-IREF")):
183
+ raise ArxmlEditError(f"{tag} is not a reference element but got {v!r}")
184
+ dest = self.dest_of(target)
185
+ if not dest:
186
+ raise ArxmlEditError(f"{tag} -> {target}: no element with that path in the loaded ARXML (add it first "
187
+ "in the same call, or pass {\"ref\": path, \"dest\": TAG})")
188
+ return f'<{tag} DEST="{escape_attr(dest)}">{escape(target)}</{tag}>'
189
+ if isinstance(v, bool):
190
+ v = "true" if v else "false"
191
+ if (tag.endswith("-REF") or tag.endswith("-TREF")) and isinstance(v, str) and v.startswith("/"):
192
+ return self.leaf(tag, "->" + v)
193
+ return f"<{tag}>{escape(str(v))}</{tag}>"
194
+
195
+ def emit(self, ft: FileTree, lines: list, base: bytes, unit: bytes) -> bytes:
196
+ return ft.nl.join(base + unit * d + s.encode("utf-8") for d, s in lines)
197
+
198
+ @staticmethod
199
+ def unit_of(ft: FileTree, node: Node) -> bytes:
200
+ if node.children:
201
+ ci = line_indent(ft.data, node.children[0].start) or b""
202
+ ni = line_indent(ft.data, node.start) or b""
203
+ if len(ci) > len(ni) and ci.startswith(ni):
204
+ return ci[len(ni):]
205
+ return b" "
206
+
207
+ # ---------- ops ----------
208
+ def op_add(self, op: dict):
209
+ parent_path, tag = op.get("parent_path"), op.get("element")
210
+ if not parent_path or not tag:
211
+ raise ArxmlEditError("add needs parent_path and element")
212
+ ft, parent = self.tree_for(parent_path.rstrip("/"))
213
+ version = _version(ft.data)
214
+ ptype = _etype(ft.chain(parent), version)
215
+ coll = op.get("collection")
216
+ if coll is None and ptype is not None:
217
+ if ptype.find_sub_element(tag, version) is None:
218
+ for c in _order(ptype):
219
+ ct = ptype.find_sub_element(c, version)
220
+ if ct is not None and ct.find_sub_element(tag, version) is not None:
221
+ coll = c
222
+ break
223
+ if coll is None:
224
+ raise ArxmlEditError(f"{tag} cannot be placed under {parent.tag} {parent_path} in this schema")
225
+ elif coll is None:
226
+ coll = "ELEMENTS" if parent.tag == "AR-PACKAGE" else None
227
+ sn = op.get("short_name")
228
+ new_path = f"{parent_path.rstrip('/')}/{sn}" if sn else None
229
+ if new_path and (new_path in ft.by_path or self.p.identifiables.get(new_path)):
230
+ raise ArxmlEditError(f"{new_path} already exists")
231
+ ctype = ptype.find_sub_element(coll, version) if (ptype is not None and coll) else ptype
232
+ etype = ctype.find_sub_element(tag, version) if ctype is not None else None
233
+ if new_path:
234
+ self.added_tags[new_path] = tag
235
+ lines = self.render(tag, sn, op.get("values") or {}, etype, version, 0)
236
+ unit = self.unit_of(ft, parent)
237
+ if not coll: # a direct child: place it at its schema position
238
+ ind = (line_indent(ft.data, parent.children[0].start) if parent.children else None) or b""
239
+ self._insert_leaf(ft, parent, tag, self.emit(ft, lines, ind, unit).lstrip())
240
+ return new_path
241
+ holder = parent.child(coll)
242
+ if holder is not None and holder.children:
243
+ last = holder.children[-1]
244
+ ind = line_indent(ft.data, last.start) or b""
245
+ self.patch(ft, last.end, last.end, ft.nl + self.emit(ft, lines, ind, unit))
246
+ return new_path
247
+ if holder is not None: # empty collection element
248
+ ind = line_indent(ft.data, holder.start) or b""
249
+ body = [(0, f"<{holder.tag}>")] + [(d + 1, s) for d, s in lines] + [(0, f"</{holder.tag}>")]
250
+ self.patch(ft, holder.start, holder.end, self.emit(ft, body, ind, unit).lstrip())
251
+ return new_path
252
+ # create the collection at its schema position among the parent's children
253
+ body = [(0, f"<{coll}>")] + [(d + 1, s) for d, s in lines] + [(0, f"</{coll}>")]
254
+ order = _order(ptype)
255
+ after = set(order[order.index(coll) + 1:]) if coll in order else set()
256
+ ind = (line_indent(ft.data, parent.children[0].start) if parent.children else None) or \
257
+ ((line_indent(ft.data, parent.start) or b"") + unit)
258
+ before = next((c for c in parent.children if c.tag in after), None)
259
+ if before is not None:
260
+ self.patch(ft, before.start, before.start, self.emit(ft, body, ind, unit).lstrip() + ft.nl + ind)
261
+ else:
262
+ last = parent.children[-1]
263
+ self.patch(ft, last.end, last.end, ft.nl + self.emit(ft, body, ind, unit))
264
+ return new_path
265
+
266
+ def own_child(self, node: Node, tag: str) -> Optional[Node]:
267
+ stack = list(node.children)
268
+ while stack:
269
+ c = stack.pop(0)
270
+ if c.tag == tag:
271
+ return c
272
+ if c.short_name is None:
273
+ stack.extend(c.children)
274
+ return None
275
+
276
+ def op_set(self, op: dict):
277
+ path, tag = op.get("path"), op.get("element")
278
+ if not path or not tag or "value" not in op:
279
+ raise ArxmlEditError("set needs path, element and value")
280
+ ft, node = self.tree_for(path.rstrip("/"))
281
+ leaf = self.own_child(node, tag)
282
+ text = self.leaf(tag, op["value"]).encode("utf-8")
283
+ if leaf is None:
284
+ self._insert_leaf(ft, node, tag, text)
285
+ return
286
+ self.patch(ft, leaf.start, leaf.end, text)
287
+
288
+ def _insert_leaf(self, ft: FileTree, node: Node, tag: str, text: bytes):
289
+ version = _version(ft.data)
290
+ order = _order(_etype(ft.chain(node), version))
291
+ if order and tag not in order:
292
+ raise ArxmlEditError(f"{node.tag} has no sub-element {tag} in this schema")
293
+ after = set(order[order.index(tag) + 1:]) if tag in order else set()
294
+ ind = line_indent(ft.data, node.children[0].start) if node.children else b""
295
+ before = next((c for c in node.children if c.tag in after), None)
296
+ if before is not None:
297
+ self.patch(ft, before.start, before.start, text + ft.nl + (ind or b""))
298
+ else:
299
+ last = node.children[-1]
300
+ self.patch(ft, last.end, last.end, ft.nl + (ind or b"") + text)
301
+
302
+ def op_remove(self, op: dict):
303
+ path = (op.get("path") or "").rstrip("/")
304
+ ft, node = self.tree_for(path)
305
+ start = node.start
306
+ if line_indent(ft.data, start) is not None:
307
+ nl = ft.data.rfind(b"\n", 0, start)
308
+ start = nl if nl >= 0 else start
309
+ self.patch(ft, start, node.end, b"")
310
+
311
+ # ---------- checks ----------
312
+ def check(self) -> list:
313
+ """References in touched files resolve, with DEST equal to the target's tag; schema re-load."""
314
+ problems = []
315
+ known = dict(self.p.identifiables)
316
+ for ft in self.files.values():
317
+ for path, n in ft.by_path.items():
318
+ known[path] = (n.tag, n.line, ft.path)
319
+ for f, ft in self.files.items():
320
+ if ft.data == self.original.get(f):
321
+ continue
322
+ before = set(re.findall(rb"<([A-Z0-9-]+-T?REF)\b[^>]*>([^<]*)<", self.original[f]))
323
+ for n in ft.root.iter():
324
+ if not (n.tag.endswith("-REF") or n.tag.endswith("-TREF")):
325
+ continue
326
+ tgt = n.text.strip()
327
+ if (n.tag.encode(), tgt.encode()) in before:
328
+ continue # pre-existing reference, untouched
329
+ dest = n.attrs.get("DEST")
330
+ hit = known.get(tgt)
331
+ if not dest:
332
+ problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} -> {tgt}: missing DEST attribute")
333
+ if hit is None:
334
+ problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} -> {tgt}: target does not exist")
335
+ elif dest and hit[0] != dest:
336
+ problems.append(f"{self.p.rel(f)}:{n.line} {n.tag} DEST={dest} but {tgt} is a {hit[0]}")
337
+ from .edit import schema_check
338
+ if schema_check(f, self.original[f]) is None:
339
+ err = schema_check(f, ft.data)
340
+ if err:
341
+ problems.append(f"{self.p.rel(f)}: schema: {err}")
342
+ return problems
343
+
344
+
345
+ def arxml_edit(project: Project, ops, dry_run: bool = False) -> dict:
346
+ if isinstance(ops, dict):
347
+ ops = [ops]
348
+ if not isinstance(ops, list) or not ops or not all(isinstance(o, dict) for o in ops):
349
+ return {"ok": False, "error": "ops must be a non-empty list of op objects"}
350
+ ed = ArxmlEditor(project)
351
+ created = []
352
+ try:
353
+ for i, op in enumerate(ops):
354
+ kind = op.get("op")
355
+ try:
356
+ if kind == "add":
357
+ created.append(ed.op_add(op))
358
+ elif kind == "set":
359
+ ed.op_set(op)
360
+ elif kind == "remove":
361
+ ed.op_remove(op)
362
+ else:
363
+ raise ArxmlEditError(f"unknown op {kind!r} (add/set/remove)")
364
+ except ArxmlEditError as e:
365
+ raise ArxmlEditError(f"op {i} ({kind} {op.get('path') or op.get('parent_path')}): {e}") from None
366
+ except ArxmlEditError as e:
367
+ return {"ok": False, "error": str(e), "written": False}
368
+ problems = ed.check()
369
+ changes = []
370
+ for f, ft in ed.files.items():
371
+ if ft.data != ed.original[f]:
372
+ a = ed.original[f].decode("utf-8", "replace").splitlines(keepends=True)
373
+ b = ft.data.decode("utf-8", "replace").splitlines(keepends=True)
374
+ rel = project.rel(f)
375
+ changes.append({"file": rel, "diff": "".join(difflib.unified_diff(a, b, f"a/{rel}", f"b/{rel}", n=1))})
376
+ if problems:
377
+ return {"ok": False, "error": "edit rejected; nothing written", "problems": problems, "changes": changes,
378
+ "written": False}
379
+ if not dry_run:
380
+ for f, ft in ed.files.items():
381
+ if ft.data != ed.original[f]:
382
+ tmp = f + ".arxml-tmp"
383
+ with open(tmp, "wb") as fh:
384
+ fh.write(ft.data)
385
+ os.replace(tmp, f)
386
+ return {"ok": True, "written": not dry_run, "created": [c for c in created if c], "changes": changes,
387
+ "schemaChecked": _ad() is not None}