@h2loop/autosar 1.0.1 → 1.2.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 (53) hide show
  1. package/README.md +11 -1
  2. package/agents/autosar-integrator.md +45 -2
  3. package/commands/autosar-trace.md +2 -2
  4. package/package.json +4 -1
  5. package/plugins/index.js +55 -23
  6. package/python/ecuc_engine/__init__.py +1 -1
  7. package/python/ecuc_engine/add_signal.py +351 -0
  8. package/python/ecuc_engine/arxml_edit.py +387 -0
  9. package/python/ecuc_engine/audit.py +352 -50
  10. package/python/ecuc_engine/cli.py +125 -5
  11. package/python/ecuc_engine/codegen/__init__.py +233 -0
  12. package/python/ecuc_engine/codegen/binding.py +305 -0
  13. package/python/ecuc_engine/codegen/bindings/sip_standin.json +378 -0
  14. package/python/ecuc_engine/codegen/cdiff.py +395 -0
  15. package/python/ecuc_engine/codegen/cwriter.py +150 -0
  16. package/python/ecuc_engine/codegen/emit.py +463 -0
  17. package/python/ecuc_engine/codegen/handles.py +176 -0
  18. package/python/ecuc_engine/codegen/mcal/tc4d.json +60 -0
  19. package/python/ecuc_engine/codegen/model.py +264 -0
  20. package/python/ecuc_engine/codegen/modules/__init__.py +7 -0
  21. package/python/ecuc_engine/codegen/modules/com.py +145 -0
  22. package/python/ecuc_engine/codegen/modules/comstack.py +14 -0
  23. package/python/ecuc_engine/codegen/modules/dem.py +22 -0
  24. package/python/ecuc_engine/codegen/modules/mcal.py +170 -0
  25. package/python/ecuc_engine/codegen/modules/memstack.py +24 -0
  26. package/python/ecuc_engine/codegen/registry.py +208 -0
  27. package/python/ecuc_engine/codegen/schm_memmap.py +159 -0
  28. package/python/ecuc_engine/context.py +481 -0
  29. package/python/ecuc_engine/csrc.py +189 -0
  30. package/python/ecuc_engine/defs.py +30 -0
  31. package/python/ecuc_engine/ecucjson.py +75 -22
  32. package/python/ecuc_engine/find.py +164 -0
  33. package/python/ecuc_engine/gen.py +105 -0
  34. package/python/ecuc_engine/history.py +146 -0
  35. package/python/ecuc_engine/model.py +4 -1
  36. package/python/ecuc_engine/probe.py +229 -0
  37. package/python/ecuc_engine/rte_api.py +935 -0
  38. package/python/ecuc_engine/rules/__init__.py +5 -3
  39. package/python/ecuc_engine/rules/mem.py +84 -0
  40. package/python/ecuc_engine/rules/nvm.py +131 -0
  41. package/python/ecuc_engine/rules/pdur.py +87 -0
  42. package/python/ecuc_engine/rules/system.py +162 -0
  43. package/python/ecuc_engine/specids.py +163 -0
  44. package/python/ecuc_engine/sysmodel.py +152 -0
  45. package/python/ecuc_engine/validate.py +15 -2
  46. package/python/ecuc_engine/values.py +26 -0
  47. package/python/ecuc_engine/verify_rte.py +445 -0
  48. package/python/pyproject.toml +1 -1
  49. package/skills/autosar/SKILL.md +78 -6
  50. package/skills/autosar/references/codegen.md +58 -0
  51. package/skills/autosar/references/ecuc-model.md +4 -1
  52. package/skills/autosar/references/project-layout.md +10 -1
  53. package/skills/autosar/references/runnable-code.md +50 -0
@@ -0,0 +1,481 @@
1
+ """Deterministic context assembly for "implement / change a runnable" tasks (autosar_build_context).
2
+
3
+ Fixed section order:
4
+ 1 TASK the task text, verbatim
5
+ 2 SWC DEFINITION ports, interfaces, data types, runnables, events (from ARXML, by reference)
6
+ 3 ALLOWED RTE API for the runnable(s): exact declaration in Rte_<Swc>.h when present, else the
7
+ SWS_RTE signature from the model; never dropped or truncated
8
+ 4 RUNNABLE CODE the entry function body from the C sources (local parse)
9
+ 5 CALLEES functions it reaches: bodies of in-house ones, prototypes of the rest
10
+ 6 RELATED CONFIG Com signals / NvM blocks behind its ports and calls (ECUC values + file:line)
11
+ 7 FUZZY: SIMILAR FUNCTIONS in other SW-Cs, by lexical/structural similarity of RTE API + call names
12
+ 8 FUZZY: SIMILAR PAST CHANGES git-history-lite (history.similar_changes)
13
+
14
+ Budget (about 4 characters per token): drop 8, then 7, then reduce 5 to prototypes, truncate 4, truncate 6,
15
+ reduce 5 to names, cut 4 to its head, compact 2. Sections 1 and 3 are never reduced. Same inputs -> byte-identical
16
+ output: everything is sorted, paths are project-relative, nothing time- or environment-dependent is emitted.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import hashlib
21
+ import json
22
+ import os
23
+ import re
24
+ from typing import Optional
25
+
26
+ from . import paths
27
+ from .csrc import CIndex, Func
28
+ from .history import similar_changes, terms_of
29
+ from .rte_api import ArIndex, SwcModel, build_all, find_header, find_swc, header_entries
30
+
31
+ CHARS_PER_TOKEN = 4
32
+ SIM_TOP = 3
33
+ CODE_TRUNC_LINES = 40
34
+ CODE_HEAD_LINES = 12
35
+
36
+
37
+ def _h(text: str) -> str:
38
+ return hashlib.sha1(text.encode("utf-8")).hexdigest()[:12]
39
+
40
+
41
+ def _cut(text: str, n: int) -> str:
42
+ lines = text.split("\n")
43
+ if len(lines) <= n:
44
+ return text
45
+ return "\n".join(lines[:n] + [f" /* … {len(lines) - n} more line(s) truncated by the context budget */", "}"])
46
+
47
+
48
+ class Ctx:
49
+ def __init__(self, root: str):
50
+ self.root = root
51
+ self.ix = ArIndex(root)
52
+ self.models = build_all(root, self.ix)
53
+ self.cx = CIndex(root)
54
+ self._project = None
55
+ self._hdr_text: Optional[dict] = None
56
+
57
+ def rel(self, f: Optional[str]) -> str:
58
+ if not f:
59
+ return "?"
60
+ r = os.path.relpath(f, self.root)
61
+ return f if r.startswith("..") else r
62
+
63
+ def project(self):
64
+ if self._project is None:
65
+ try:
66
+ from .model import Project
67
+ self._project = Project(self.root)
68
+ except Exception: # noqa: BLE001
69
+ self._project = False
70
+ return self._project or None
71
+
72
+ # ---- C helpers
73
+ def headers(self) -> dict:
74
+ if self._hdr_text is None:
75
+ self._hdr_text = {}
76
+ for f in paths.walk(self.root, paths.is_c).files:
77
+ if f.lower().endswith(".h"):
78
+ try:
79
+ self._hdr_text[f] = open(f, encoding="utf-8", errors="replace").read()
80
+ except OSError:
81
+ pass
82
+ return self._hdr_text
83
+
84
+ def prototype(self, name: str) -> Optional[tuple]:
85
+ rx = re.compile(rf"^[^\n#;{{}}]*\b{re.escape(name)}\s*\([^;{{}}]*\)\s*;", re.M)
86
+ for f in sorted(self.headers()):
87
+ m = rx.search(self.headers()[f])
88
+ if m:
89
+ return " ".join(m.group(0).split()), f"{self.rel(f)}:{self.headers()[f].count(chr(10), 0, m.start()) + 1}"
90
+ for name2, lst in self.cx.funcs.items():
91
+ if name2 == name:
92
+ return lst[0].signature + ";", f"{self.rel(lst[0].file)}:{lst[0].line}"
93
+ return None
94
+
95
+ def body(self, fn: Func) -> str:
96
+ return fn.text(self.cx.src[fn.file])
97
+
98
+ def swc_files(self, m: SwcModel) -> tuple:
99
+ """(ARXML files, C files) of a SW-C: its description + its interfaces; C reached from its runnables or
100
+ including Rte_<Swc>.h."""
101
+ ax = {os.path.join(self.root, m.file)}
102
+ for p in m.ports:
103
+ n = self.ix.get(p.get("interface"))
104
+ if n is not None:
105
+ ax.add(self.ix.where(n)[0])
106
+ cf = set()
107
+ for r in m.runnables:
108
+ fn = self.cx.get(r.symbol)
109
+ if fn is not None:
110
+ for g in self.cx.reachable(fn):
111
+ cf.add(g.file)
112
+ want = f"rte_{m.name}.h".lower()
113
+ for f, incs in self.cx.includes.items():
114
+ if any(os.path.basename(i).lower() == want for i in incs):
115
+ cf.add(f)
116
+ return sorted(x for x in ax if x), sorted(cf)
117
+
118
+
119
+ # ---------------------------------------------------------------- sections
120
+ def sec_swc(c: Ctx, m: SwcModel, runs: list) -> list:
121
+ """-> [full, compact] renderings + sources."""
122
+ lines = [f"SW-C {m.name} ({m.tag}) {m.path} @ {m.file}:{m.line}"
123
+ + ("; supports multiple instantiation (Rte_Instance self first)" if m.multiple_instantiation else "")]
124
+ lines.append("Ports:")
125
+ for p in m.ports:
126
+ lines.append(f" {p['direction']}-port {p['name']} : {p['interfaceKind']} {p['interface']} @ {p['at']}")
127
+ for e in p["elements"]:
128
+ extra = ", ".join(x for x in (e.get("type"), e.get("semantics") and f"{e['semantics']} semantics",
129
+ e.get("kind")) if x)
130
+ lines.append(f" - {e['name']}" + (f" ({extra})" if extra else ""))
131
+ if m.types:
132
+ lines.append("Data types: " + "; ".join(f"{k}={v['kind']}" + (f"[{v['base']}]" if v.get("base") else "")
133
+ for k, v in sorted(m.types.items())))
134
+ lines.append("Runnables:")
135
+ for r in m.runnables:
136
+ ev = "; ".join(f"{e['kind']} {e['event']}" + (f" period {e['period']}s" if e.get("period") else "")
137
+ + (f" on {e.get('port')}.{(e.get('target') or '').rsplit('/', 1)[-1]}" if e.get("port") else "")
138
+ for e in r.events) or "no event (not scheduled)"
139
+ mark = " <== target" if r in runs and len(runs) < len(m.runnables) else ""
140
+ lines.append(f" {r.name} symbol {r.symbol} [{ev}] @ {r.file}:{r.line}{mark}")
141
+ lines.append(f" entry: {r.signature}")
142
+ for pt in r.points:
143
+ lines.append(f" {pt['role']}: {pt.get('port') or ''}{'.' if pt.get('port') else ''}"
144
+ f"{(pt.get('target') or '').rsplit('/', 1)[-1]}" + (f" !! {pt['unresolved']}" if pt.get("unresolved") else ""))
145
+ full = "\n".join(lines)
146
+ compact = "\n".join(l for l in lines if not l.startswith(" ") or "<== target" in l or l.startswith(" -"))
147
+ src = [{"kind": "arxml", "ref": m.path, "at": f"{m.file}:{m.line}"}]
148
+ return [full, compact], src
149
+
150
+
151
+ def sec_api(c: Ctx, m: SwcModel, runs: list, header: Optional[str]) -> tuple:
152
+ decl = header_entries([header]) if header else {}
153
+ lines, src = [], []
154
+ if header:
155
+ lines.append(f"Generated contract header: {c.rel(header)}")
156
+ else:
157
+ lines.append(f"No Rte_{m.name}.h in the project: signatures below are derived from the ARXML (SWS_RTE 5.6).")
158
+ for r in runs:
159
+ allowed = sorted(m.allowed(r.name), key=lambda a: a.name)
160
+ lines.append(f"Runnable {r.name}: {len(allowed)} allowed API(s) — call ONLY these from {r.symbol} and the "
161
+ "functions it calls:")
162
+ for a in allowed:
163
+ d = decl.get(a.name)
164
+ lines.append(f" {a.signature}; [{a.cite}]")
165
+ if d:
166
+ lines.append(f" declared {c.rel(d['file'])}:{d['line']}: {d['text']}")
167
+ src.append({"kind": "rte-decl", "ref": a.name, "at": f"{c.rel(d['file'])}:{d['line']}"})
168
+ else:
169
+ src.append({"kind": "rte-model", "ref": a.name, "at": a.target})
170
+ others = sorted(a.name for a in m.generated() if not a.allowed_for(r.name))
171
+ if others:
172
+ lines.append(f" NOT allowed in {r.name} (other runnables' access points; adding a call is NOT-ALLOWED "
173
+ f"until an access point is added): {', '.join(others)}")
174
+ return "\n".join(lines), src
175
+
176
+
177
+ def sec_code(c: Ctx, runs: list) -> tuple:
178
+ full, trunc, head, src, missing = [], [], [], [], []
179
+ for r in runs:
180
+ fn = c.cx.get(r.symbol)
181
+ if fn is None:
182
+ missing.append(r.symbol)
183
+ msg = f"/* {r.symbol}: no definition found in the project C sources (new runnable?) */"
184
+ full.append(msg)
185
+ trunc.append(msg)
186
+ head.append(msg)
187
+ continue
188
+ b = c.body(fn)
189
+ hdr = f"/* {c.rel(fn.file)}:{fn.line}-{fn.end_line} */"
190
+ full.append(hdr + "\n" + b)
191
+ trunc.append(hdr + "\n" + _cut(b, CODE_TRUNC_LINES))
192
+ head.append(hdr + "\n" + _cut(b, CODE_HEAD_LINES))
193
+ src.append({"kind": "c-function", "ref": fn.name, "at": f"{c.rel(fn.file)}:{fn.line}-{fn.end_line}", "sha1": _h(b)})
194
+ return ["\n\n".join(full), "\n\n".join(trunc), "\n\n".join(head)], src, missing
195
+
196
+
197
+ def sec_callees(c: Ctx, runs: list) -> tuple:
198
+ entries = [c.cx.get(r.symbol) for r in runs]
199
+ entries = [e for e in entries if e is not None]
200
+ entry_ids = {id(e) for e in entries}
201
+ own, ext = [], set()
202
+ seen = set()
203
+ for e in entries:
204
+ for g in c.cx.reachable(e):
205
+ if id(g) in entry_ids or id(g) in seen:
206
+ continue
207
+ seen.add(id(g))
208
+ own.append(g)
209
+ for g in [e] + own:
210
+ for name in g.calls:
211
+ if name.startswith("Rte_") or c.cx.callee(g, name) is not None:
212
+ continue
213
+ ext.add(name)
214
+ own.sort(key=lambda g: (c.rel(g.file), g.line))
215
+ ext_l = []
216
+ for name in sorted(ext):
217
+ p = c.prototype(name)
218
+ if p:
219
+ ext_l.append((name, p))
220
+ bodies = [f"/* {c.rel(g.file)}:{g.line}-{g.end_line} */\n{c.body(g)}" for g in own]
221
+ sigs = [f"{g.signature}; /* {c.rel(g.file)}:{g.line} */" for g in own]
222
+ protos = [f"{p[0]} /* {p[1]} */" for _n, p in ext_l]
223
+ names = ", ".join([g.name for g in own] + [n for n, _p in ext_l])
224
+ full = "\n\n".join(bodies + (["/* called, defined elsewhere: */\n" + "\n".join(protos)] if protos else []))
225
+ sig = "\n".join(sigs + protos)
226
+ src = [{"kind": "c-function", "ref": g.name, "at": f"{c.rel(g.file)}:{g.line}-{g.end_line}", "sha1": _h(c.body(g))}
227
+ for g in own] + [{"kind": "c-prototype", "ref": n, "at": p[1]} for n, p in ext_l]
228
+ return [full or "(no callees)", sig or "(no callees)", ("calls: " + names) if names else "(no callees)"], src
229
+
230
+
231
+ def _sysmap(c: Ctx) -> dict:
232
+ """(port path, data element path) -> system signal path, from SENDER-RECEIVER-TO-SIGNAL-MAPPINGs."""
233
+ out = {}
234
+ for sysn in c.ix.of_tag("SYSTEM"):
235
+ for n in sysn.iter():
236
+ if n.tag == "SENDER-RECEIVER-TO-SIGNAL-MAPPING":
237
+ port = None
238
+ el = None
239
+ for x in n.iter():
240
+ if x.tag == "CONTEXT-PORT-REF":
241
+ port = x.text.strip()
242
+ elif x.tag == "TARGET-DATA-PROTOTYPE-REF":
243
+ el = x.text.strip()
244
+ sig = n.child_text("SYSTEM-SIGNAL-REF")
245
+ if port and el and sig:
246
+ out.setdefault((port, el), sig)
247
+ return out
248
+
249
+
250
+ def sec_config(c: Ctx, m: SwcModel, runs: list, header: Optional[str]) -> tuple:
251
+ p = c.project()
252
+ if p is None:
253
+ return ["(ECUC configuration not loadable)", "(ECUC configuration not loadable)"], [], []
254
+ from .audit import signal_owners
255
+ from .find import _own
256
+ decl = header_entries([header]) if header else {}
257
+ by_name: dict = {}
258
+ for path, group in p.by_path.items():
259
+ e = group[0]
260
+ if e.kind == "container":
261
+ by_name.setdefault(((e.def_ref or "").rsplit("/", 1)[-1], e.name), e)
262
+ sysmap = _sysmap(c)
263
+ sig_to_com: dict = {}
264
+ for (dname, _n), e in sorted(by_name.items()):
265
+ if dname in ("ComSignal", "ComGroupSignal"):
266
+ ref = (_own(p, e).get("ComSystemTemplateSystemSignalRef") or [None])[0]
267
+ mp = c.ix.get(ref)
268
+ if mp is not None:
269
+ isig = c.ix.get(mp.child_text("I-SIGNAL-REF"))
270
+ ssig = isig.child_text("SYSTEM-SIGNAL-REF") if isig is not None else None
271
+ if ssig:
272
+ sig_to_com.setdefault(ssig, []).append(e)
273
+ lines, src, terms = [], [], []
274
+ want = []
275
+ for r in runs:
276
+ for a in sorted(m.allowed(r.name), key=lambda a: a.name):
277
+ if a.port and a not in want:
278
+ want.append(a)
279
+ for a in want:
280
+ hits = []
281
+ d = decl.get(a.name)
282
+ if d:
283
+ for mod, dname, short in re.findall(r"\b([A-Z][A-Za-z]+)Conf_([A-Za-z]+)_(\w+)", d["text"]):
284
+ e = by_name.get((dname, short))
285
+ if e is not None:
286
+ hits.append((e, f"via {c.rel(d['file'])}:{d['line']}"))
287
+ if not hits:
288
+ ssig = sysmap.get((a.port_path, a.target))
289
+ for e in sig_to_com.get(ssig or "", []):
290
+ hits.append((e, f"via SenderReceiverToSignalMapping -> {ssig}"))
291
+ for e, how in hits:
292
+ own = _own(p, e)
293
+ vals = ", ".join(f"{k}={v[0] if len(v) == 1 else v}" for k, v in sorted(own.items())
294
+ if k in ("ComBitPosition", "ComBitSize", "ComSignalType", "ComSignalEndianness",
295
+ "ComHandleId", "ComTransferProperty", "ComSignalInitValue"))
296
+ owners = signal_owners(p, [e.path]).get(e.path) or []
297
+ ip = "; ".join(f"I-PDU {o['ipdu']} {o.get('direction') or '?'}" for o in owners)
298
+ lines.append(f"{a.port}.{a.element} -> {e.path} [{vals}] {ip} @ {p.rel(e.file)}:{e.line} ({how})")
299
+ src.append({"kind": "ecuc", "ref": e.path, "at": f"{p.rel(e.file)}:{e.line}"})
300
+ terms.append(e.name)
301
+ # NvM blocks used by the runnable's code
302
+ blocks = set()
303
+ for r in runs:
304
+ fn = c.cx.get(r.symbol)
305
+ if fn is None:
306
+ continue
307
+ for g in c.cx.reachable(fn):
308
+ blocks.update(re.findall(r"\bNvMConf_NvMBlockDescriptor_(\w+)", c.body(g)))
309
+ for b in sorted(blocks):
310
+ e = by_name.get(("NvMBlockDescriptor", b))
311
+ if e is None:
312
+ lines.append(f"NvM block {b}: not configured (NvMConf_NvMBlockDescriptor_{b} has no NvMBlockDescriptor)")
313
+ continue
314
+ own = _own(p, e)
315
+ vals = ", ".join(f"{k}={v[0] if len(v) == 1 else v}" for k, v in sorted(own.items())
316
+ if k in ("NvMNvBlockLength", "NvMRamBlockDataAddress", "NvMBlockUseCrc", "NvMBlockCrcType",
317
+ "NvMBlockManagementType", "NvMNvramBlockIdentifier", "NvMSelectBlockForReadAll"))
318
+ lines.append(f"NvM {e.path} [{vals}] @ {p.rel(e.file)}:{e.line}")
319
+ src.append({"kind": "ecuc", "ref": e.path, "at": f"{p.rel(e.file)}:{e.line}"})
320
+ if not lines:
321
+ lines.append("(no Com signal or NvM block found behind this runnable's ports and calls)")
322
+ full = "\n".join(lines)
323
+ return [full, _cut(full, 15)], src, terms
324
+
325
+
326
+ def _features(c: Ctx, fn: Optional[Func], extra: set) -> set:
327
+ feats = set(extra)
328
+ if fn is None:
329
+ return feats
330
+ for g in c.cx.reachable(fn)[:20]:
331
+ for name, _l, _a in g.rte:
332
+ feats.add("api:" + name.split("_")[1] if "_" in name else name)
333
+ feats |= {t for t in terms_of(name)}
334
+ for n in g.calls:
335
+ if not n.startswith("Rte_"):
336
+ feats.add("call:" + n)
337
+ feats |= terms_of(c.body(g))
338
+ return feats
339
+
340
+
341
+ def sec_similar(c: Ctx, m: SwcModel, runs: list, task: str) -> tuple:
342
+ target = set()
343
+ task_t = terms_of(task)
344
+ for r in runs:
345
+ target |= _features(c, c.cx.get(r.symbol), set())
346
+ for a in m.allowed(r.name):
347
+ target.add("api:" + a.api)
348
+ target |= terms_of(a.element or "")
349
+ target |= task_t
350
+ cands = []
351
+ for o in c.models:
352
+ if o is m:
353
+ continue
354
+ for r in o.runnables:
355
+ fn = c.cx.get(r.symbol)
356
+ if fn is None:
357
+ continue
358
+ f = _features(c, fn, {"api:" + a.api for a in o.allowed(r.name)})
359
+ inter, union = len(target & f), len(target | f) or 1
360
+ score = round(inter / union, 4)
361
+ if inter:
362
+ cands.append((-score, o.name, r.name, fn, sorted(target & f)))
363
+ cands.sort(key=lambda x: (x[0], x[1], x[2]))
364
+ top = cands[:SIM_TOP]
365
+ lines, src = [], []
366
+ for s, swc, rn, fn, common in top:
367
+ lines.append(f"[{swc}.{rn} similarity {-s:.3f}; shared: {', '.join(common[:12])}]\n"
368
+ f"/* {c.rel(fn.file)}:{fn.line}-{fn.end_line} */\n" + _cut(c.body(fn), 30))
369
+ src.append({"kind": "c-function", "ref": f"{swc}.{rn}", "at": f"{c.rel(fn.file)}:{fn.line}", "score": -s,
370
+ "sha1": _h(c.body(fn))})
371
+ return ("\n\n".join(lines) or "(no similar function in other SW-Cs)"), src, [x[1] for x in top]
372
+
373
+
374
+ def sec_history(c: Ctx, m: SwcModel, runs: list, task: str, similar_swcs: list) -> tuple:
375
+ ax, cf = c.swc_files(m)
376
+ rel_files = []
377
+ for name in sorted(set(similar_swcs)):
378
+ o = find_swc(c.models, name)
379
+ if o is not None:
380
+ a2, c2 = c.swc_files(o)
381
+ rel_files += a2 + c2
382
+ terms = terms_of(task)
383
+ for r in runs:
384
+ for a in m.allowed(r.name):
385
+ terms |= terms_of(f"{a.port or ''} {a.element or ''}")
386
+ terms.add(r.name.lower())
387
+ res = similar_changes(c.root, ax + cf, sorted(set(rel_files) - set(ax + cf)), terms)
388
+ lines, src = [], []
389
+ for ch in res["changes"]:
390
+ lines.append(f"[commit {ch['commit']} score {ch['score']}: {ch['subject']}; overlap: {', '.join(ch['overlap'][:10])}]")
391
+ for d in ch["arxml"]:
392
+ lines.append(f" ARXML {d['file']}: " + (d.get("status") or d.get("error") or json.dumps(d.get("summary"))))
393
+ lines.extend(f" {x}" for x in d.get("items", []))
394
+ for h in ch["c"]:
395
+ lines.append(f" C {h['file']}:\n" + "\n".join(" " + x for x in h["diff"].splitlines()))
396
+ src.append({"kind": "git-commit", "ref": ch["commit"], "score": ch["score"]})
397
+ if not lines:
398
+ lines = [f"(no similar past change: {'; '.join(res['notes']) or 'nothing matched'})"]
399
+ return "\n".join(lines), src
400
+
401
+
402
+ # ---------------------------------------------------------------- assembly
403
+ def build_context(root: str, swc: str, task: str, runnable: Optional[str] = None, budget_tokens: int = 24000,
404
+ history: bool = True) -> dict:
405
+ c = Ctx(root)
406
+ m = find_swc(c.models, swc)
407
+ if m is None:
408
+ return {"error": f"no SW-COMPONENT-TYPE {swc!r}; known: {', '.join(x.name for x in c.models) or 'none'}"}
409
+ if runnable:
410
+ r = m.runnable(runnable)
411
+ if r is None:
412
+ return {"error": f"{m.name} has no runnable {runnable!r}; runnables: {', '.join(x.name for x in m.runnables)} "
413
+ "(to add a runnable, add the RUNNABLE-ENTITY + event with arxml_edit first)"}
414
+ runs = [r]
415
+ else:
416
+ runs = list(m.runnables)
417
+ header = find_header(root, m.name, sorted(c.headers()))
418
+ s_swc, src_swc = sec_swc(c, m, runs)
419
+ s_api, src_api = sec_api(c, m, runs, header)
420
+ s_code, src_code, missing = sec_code(c, runs)
421
+ s_cal, src_cal = sec_callees(c, runs)
422
+ s_cfg, src_cfg, _t = sec_config(c, m, runs, header)
423
+ s_sim, src_sim, sim_swcs = sec_similar(c, m, runs, task)
424
+ if history:
425
+ s_hist, src_hist = sec_history(c, m, runs, task, sim_swcs)
426
+ else:
427
+ s_hist, src_hist = "(history disabled)", []
428
+ sections = [
429
+ {"id": "task", "title": "1. TASK", "variants": [task.strip()], "sources": []},
430
+ {"id": "swc", "title": f"2. SW-C DEFINITION ({m.name})", "variants": s_swc, "sources": src_swc},
431
+ {"id": "api", "title": "3. ALLOWED RTE API", "variants": [s_api], "sources": src_api},
432
+ {"id": "code", "title": "4. CURRENT RUNNABLE CODE", "variants": s_code, "sources": src_code},
433
+ {"id": "callees", "title": "5. FUNCTIONS IT CALLS", "variants": s_cal, "sources": src_cal},
434
+ {"id": "config", "title": "6. RELATED CONFIGURATION (Com signals / NvM blocks)", "variants": s_cfg, "sources": src_cfg},
435
+ {"id": "similar", "title": "7. FUZZY: SIMILAR FUNCTIONS IN OTHER SW-Cs", "variants": [s_sim, ""], "sources": src_sim},
436
+ {"id": "history", "title": "8. FUZZY: SIMILAR PAST CHANGES", "variants": [s_hist, ""], "sources": src_hist},
437
+ ]
438
+ mode_names = {"similar": ["full", "dropped"], "history": ["full", "dropped"],
439
+ "callees": ["bodies", "prototypes", "names"], "code": ["full", "truncated", "head"],
440
+ "config": ["full", "truncated"], "swc": ["full", "compact"]}
441
+ level = {s["id"]: 0 for s in sections}
442
+ steps = [("history", 1), ("similar", 1), ("callees", 1), ("code", 1), ("config", 1), ("callees", 2),
443
+ ("code", 2), ("swc", 1)]
444
+ budget = max(1, budget_tokens) * CHARS_PER_TOKEN
445
+ applied = []
446
+
447
+ def render() -> str:
448
+ parts = [f"# AUTOSAR context: {m.name}" + (f" / {runs[0].name}" if len(runs) == 1 else " (all runnables)")]
449
+ for s in sections:
450
+ v = s["variants"][min(level[s["id"]], len(s["variants"]) - 1)]
451
+ if v == "":
452
+ parts.append(f"## {s['title']}\n(dropped: context budget)")
453
+ else:
454
+ parts.append(f"## {s['title']}\n{v}")
455
+ return "\n\n".join(parts) + "\n"
456
+
457
+ text = render()
458
+ for sid, lv in steps:
459
+ if len(text) <= budget:
460
+ break
461
+ sec = next(s for s in sections if s["id"] == sid)
462
+ if lv < len(sec["variants"]) and level[sid] < lv:
463
+ level[sid] = lv
464
+ applied.append(f"{sid}->{mode_names[sid][lv]}")
465
+ text = render()
466
+ manifest = []
467
+ for s in sections:
468
+ mode = mode_names.get(s["id"], ["full"])[level[s["id"]]]
469
+ for x in s["sources"]:
470
+ manifest.append({"section": s["id"], **x, "included": mode})
471
+ out = {"text": text, "manifest": manifest,
472
+ "sections": [{"id": s["id"], "title": s["title"], "mode": mode_names.get(s["id"], ["full"])[level[s["id"]]],
473
+ "chars": len(s["variants"][min(level[s["id"]], len(s["variants"]) - 1)])} for s in sections],
474
+ "swc": m.name, "runnables": [r.name for r in runs], "budgetTokens": budget_tokens,
475
+ "estimatedTokens": (len(text) + CHARS_PER_TOKEN - 1) // CHARS_PER_TOKEN, "reductions": applied,
476
+ "sha1": _h(text)}
477
+ if len(text) > budget:
478
+ out["overBudget"] = True
479
+ if missing:
480
+ out["codeMissing"] = missing
481
+ return out
@@ -0,0 +1,189 @@
1
+ """Local, dependency-free C function index: definitions, bodies, callees, Rte_* calls and the call graph.
2
+
3
+ Function definitions are found at brace depth 0 in comment/string-stripped text (audit.strip_comments, which
4
+ keeps offsets): a '{' preceded by ')' whose matching '(' follows an identifier is a definition of that
5
+ identifier. Works with AUTOSAR compiler-abstraction macros (FUNC(void, X_CODE) Name(void)) because the name is
6
+ the identifier directly before the parameter list. Preprocessor lines are ignored.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import re
12
+ from dataclasses import dataclass, field
13
+ from typing import Optional
14
+
15
+ from . import paths
16
+ from .audit import is_generated, iter_calls, strip_comments
17
+
18
+ KEYWORDS = {"if", "for", "while", "switch", "return", "sizeof", "defined", "else", "do", "case", "goto", "typeof",
19
+ "_Alignof", "__attribute__", "__asm__", "asm", "offsetof"}
20
+ RTE_CALL = re.compile(r"\b(Rte_\w+)\s*\(")
21
+ ANY_CALL = re.compile(r"\b([A-Za-z_]\w*)\s*\(")
22
+
23
+
24
+ @dataclass
25
+ class Func:
26
+ name: str
27
+ file: str
28
+ line: int
29
+ end_line: int
30
+ start: int # offset of the declaration start
31
+ body_start: int # offset of '{'
32
+ end: int # offset after '}'
33
+ static: bool
34
+ signature: str
35
+ calls: list = field(default_factory=list) # callee names in order of first use
36
+ rte: list = field(default_factory=list) # [(name, line, args)]
37
+
38
+ def text(self, src: str) -> str:
39
+ return src[self.start:self.end]
40
+
41
+
42
+ def _blank_preproc(text: str) -> str:
43
+ out = []
44
+ cont = False
45
+ for ln in text.split("\n"):
46
+ if cont or ln.lstrip().startswith("#"):
47
+ cont = ln.rstrip().endswith("\\")
48
+ out.append(" " * len(ln))
49
+ else:
50
+ out.append(ln)
51
+ return "\n".join(out)
52
+
53
+
54
+ def _match_open(s: str, close: int) -> int:
55
+ depth = 0
56
+ for i in range(close, -1, -1):
57
+ if s[i] == ")":
58
+ depth += 1
59
+ elif s[i] == "(":
60
+ depth -= 1
61
+ if depth == 0:
62
+ return i
63
+ return -1
64
+
65
+
66
+ def functions(path: str, raw: Optional[str] = None) -> tuple:
67
+ """-> (list of Func, raw source)."""
68
+ if raw is None:
69
+ try:
70
+ raw = open(path, encoding="utf-8", errors="replace").read()
71
+ except OSError:
72
+ return [], ""
73
+ s = _blank_preproc(strip_comments(raw))
74
+ out = []
75
+ depth = 0
76
+ i, n = 0, len(s)
77
+ last_end = 0 # end of the previous top-level construct (for the declaration start)
78
+ while i < n:
79
+ c = s[i]
80
+ if c == "{":
81
+ if depth == 0:
82
+ j = i - 1
83
+ while j >= 0 and s[j].isspace():
84
+ j -= 1
85
+ fn = None
86
+ if j >= 0 and s[j] == ")":
87
+ o = _match_open(s, j)
88
+ k = o - 1
89
+ while k >= 0 and s[k].isspace():
90
+ k -= 1
91
+ m = re.search(r"([A-Za-z_]\w*)$", s[max(0, k - 200):k + 1]) if k >= 0 else None
92
+ if m and m.group(1) not in KEYWORDS:
93
+ fn = m.group(1)
94
+ if fn:
95
+ # matching close brace
96
+ d, e = 0, i
97
+ while e < n:
98
+ if s[e] == "{":
99
+ d += 1
100
+ elif s[e] == "}":
101
+ d -= 1
102
+ if d == 0:
103
+ break
104
+ e += 1
105
+ head_start = max(s.rfind(";", 0, i), s.rfind("}", 0, i), last_end - 1) + 1
106
+ while head_start < i and s[head_start].isspace():
107
+ head_start += 1
108
+ sig = " ".join(s[head_start:i].split())
109
+ f = Func(fn, path, s.count("\n", 0, head_start) + 1, s.count("\n", 0, e) + 1, head_start, i,
110
+ e + 1, bool(re.match(r"(?:\w+\s+)*?static\b", sig)), sig)
111
+ body = s[i:e + 1]
112
+ seen = []
113
+ for mm in ANY_CALL.finditer(body):
114
+ name = mm.group(1)
115
+ if name in KEYWORDS or name == fn:
116
+ continue
117
+ if name not in seen:
118
+ seen.append(name)
119
+ f.calls = seen
120
+ for name, off, _line, args in iter_calls(body, RTE_CALL):
121
+ f.rte.append((name, f.line + body.count("\n", 0, off) + (s.count("\n", head_start, i)), args))
122
+ out.append(f)
123
+ i = e + 1
124
+ last_end = i
125
+ continue
126
+ depth += 1
127
+ elif c == "}":
128
+ depth = max(0, depth - 1)
129
+ if depth == 0:
130
+ last_end = i + 1
131
+ elif c == ";" and depth == 0:
132
+ last_end = i + 1
133
+ i += 1
134
+ return out, raw
135
+
136
+
137
+ class CIndex:
138
+ """All function definitions under the given C files (in-house first; generated files kept separately)."""
139
+
140
+ def __init__(self, root: str, files: Optional[list] = None, include_generated: bool = False):
141
+ self.root = root
142
+ all_c = files if files is not None else paths.walk(root, paths.is_c).files
143
+ self.cfiles = sorted(f for f in all_c if f.lower().endswith(".c"))
144
+ self.generated = {f for f in self.cfiles if is_generated(f)}
145
+ self.funcs: dict = {} # name -> [Func]
146
+ self.src: dict = {}
147
+ self.includes: dict = {}
148
+ for f in self.cfiles:
149
+ if f in self.generated and not include_generated:
150
+ continue
151
+ fs, raw = functions(f)
152
+ self.src[f] = raw
153
+ self.includes[f] = re.findall(r'^\s*#\s*include\s*[<"]([^>"]+)[>"]', raw, re.M)
154
+ for fn in fs:
155
+ self.funcs.setdefault(fn.name, []).append(fn)
156
+
157
+ def get(self, name: str, prefer_file: Optional[str] = None) -> Optional[Func]:
158
+ lst = self.funcs.get(name) or []
159
+ if prefer_file:
160
+ for f in lst:
161
+ if f.file == prefer_file:
162
+ return f
163
+ nonstatic = [f for f in lst if not f.static]
164
+ return (nonstatic or lst or [None])[0]
165
+
166
+ def callee(self, caller: Func, name: str) -> Optional[Func]:
167
+ """Resolve a call from ``caller``: a static function in the same file wins, else a global one."""
168
+ lst = self.funcs.get(name) or []
169
+ same = [f for f in lst if f.file == caller.file]
170
+ if same:
171
+ return same[0]
172
+ glob = [f for f in lst if not f.static]
173
+ return glob[0] if glob else None
174
+
175
+ def reachable(self, start: Func, limit: int = 2000) -> list:
176
+ seen, order, stack = {id(start)}, [start], [start]
177
+ while stack and len(order) < limit:
178
+ f = stack.pop()
179
+ for c in f.calls:
180
+ g = self.callee(f, c)
181
+ if g is not None and id(g) not in seen:
182
+ seen.add(id(g))
183
+ order.append(g)
184
+ stack.append(g)
185
+ return order
186
+
187
+ def rel(self, f: str) -> str:
188
+ r = os.path.relpath(f, self.root)
189
+ return f if r.startswith("..") else r