@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,159 @@
1
+ """SchM_<Mip>.h and <Mip>_MemMap.h (+ an aggregating MemMap.h when the project has none).
2
+
3
+ SchM_<Mip>.h (SWS_Rte BSW scheduler part): exclusive-area enter/exit macros and the module's main
4
+ function prototypes.
5
+ * Exclusive areas: the ones the module's static code actually uses (`SchM_Enter_<Mip>_<EA>` found
6
+ in the project's non-generated C), plus any configured for the module in the Rte ECUC.
7
+ Mechanism per RteExclusiveAreaImplMechanism when configured (RteBswExclusiveAreaImpl), else
8
+ ALL_INTERRUPT_BLOCKING (OS SuspendAllInterrupts/ResumeAllInterrupts).
9
+ * Main functions: <Mip>_MainFunction for a `...MainFunctionPeriod` parameter, and one per instance
10
+ of a `<Mip>MainFunction<X>` container (Com-style named main functions).
11
+ <Mip>_MemMap.h: SWS_MemMap_00032 file name, keyword evaluation and START/STOP sequence checks in the
12
+ shape of the SWS_MemMap example (chapter A.3); pragmas are left to the project's MemMapAllocation
13
+ (`.hydron/autosar.json` codegen.memmap.pragmas), so the default build is compiler-neutral.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+
19
+ from . import cwriter as W
20
+
21
+ DEFAULT_SECTIONS = ["CODE", "CONST_UNSPECIFIED", "CONFIG_DATA_UNSPECIFIED", "VAR_INIT_UNSPECIFIED",
22
+ "VAR_NOINIT_UNSPECIFIED", "VAR_CLEARED_UNSPECIFIED"]
23
+
24
+ MECHANISMS = {
25
+ "ALL_INTERRUPT_BLOCKING": ("SuspendAllInterrupts()", "ResumeAllInterrupts()"),
26
+ "OS_INTERRUPT_BLOCKING": ("SuspendOSInterrupts()", "ResumeOSInterrupts()"),
27
+ }
28
+
29
+
30
+ def _scan(ctx, rx) -> dict:
31
+ hits: dict = {}
32
+ for f in ctx.sources:
33
+ try:
34
+ with open(f, errors="replace") as fh:
35
+ text = fh.read()
36
+ except OSError:
37
+ continue
38
+ for m in rx.finditer(text):
39
+ hits.setdefault(m.group(1), set()).add(ctx.cg.p.rel(f))
40
+ return hits
41
+
42
+
43
+ def _ea_mechanisms(cg) -> dict:
44
+ """EA short name -> (mechanism, os resource) from the Rte ECUC, when configured."""
45
+ out = {}
46
+ rte = cg.module("Rte")
47
+ if rte is None:
48
+ return out
49
+ for c in rte.containers:
50
+ if c.def_name != "RteBswExclusiveAreaImpl":
51
+ continue
52
+ ref = c.text("RteBswExclusiveAreaRef") or ""
53
+ mech = c.text("RteExclusiveAreaImplMechanism") or "ALL_INTERRUPT_BLOCKING"
54
+ res = (c.text("RteBswExclusiveAreaOsResourceRef") or "").rsplit("/", 1)[-1]
55
+ if ref:
56
+ out[ref.rsplit("/", 1)[-1]] = (mech, res)
57
+ return out
58
+
59
+
60
+ def main_functions(cg, mo) -> list:
61
+ mc = mo.mc
62
+ out = list(getattr(mo, "main_functions", []))
63
+ for c in mc.containers:
64
+ if c.defn is None:
65
+ continue
66
+ m = re.match(rf"^{re.escape(mc.name)}MainFunction(\w*)$", c.def_name)
67
+ if m:
68
+ suffix = m.group(1)
69
+ fn = f"{mo.mip}_MainFunction{suffix}" + ("" if c.name == c.def_name else f"_{c.name}")
70
+ if fn not in out:
71
+ out.append(fn)
72
+ continue
73
+ if cg.is_multi(c):
74
+ continue
75
+ for p in c.defn.params:
76
+ if p.endswith("MainFunctionPeriod") and c.text(p.rsplit("/", 1)[-1]) is not None:
77
+ fn = f"{mo.mip}_MainFunction"
78
+ if fn not in out:
79
+ out.append(fn)
80
+ return out
81
+
82
+
83
+ def schm_header(ctx, mo) -> tuple:
84
+ mip, MIP = mo.mip, mo.MIP
85
+ eas = _scan(ctx, re.compile(rf"\bSchM_(?:Enter|Exit)_{re.escape(mip)}_(\w+)\s*\("))
86
+ mech = _ea_mechanisms(ctx.cg)
87
+ for ea in mech:
88
+ if ea.upper().startswith(MIP + "_") and ea not in eas:
89
+ eas[ea] = {"Rte ECUC"}
90
+ mfs = main_functions(ctx.cg, mo)
91
+ name = f"SchM_{mip}.h"
92
+ g = W.guard(name)
93
+ lines = [mo._banner(name), "", f"#ifndef {g}", f"#define {g}", "", '#include "Std_Types.h"']
94
+ if eas:
95
+ lines.append('#include "Os.h"')
96
+ if eas:
97
+ lines += ["", "/* Exclusive areas (SWS_Rte SchM_Enter/SchM_Exit); used in: "
98
+ + ", ".join(sorted({f for s in eas.values() for f in s})) + " */"]
99
+ for ea in sorted(eas):
100
+ m, res = mech.get(ea, ("ALL_INTERRUPT_BLOCKING", ""))
101
+ if m == "OS_RESOURCE" and res:
102
+ enter, exit_ = f"(void)GetResource({res})", f"(void)ReleaseResource({res})"
103
+ else:
104
+ enter, exit_ = MECHANISMS.get(m, ("do { } while (0)", "do { } while (0)"))
105
+ lines += [W.define(f"SchM_Enter_{mip}_{ea}()", enter, m), W.define(f"SchM_Exit_{mip}_{ea}()", exit_)]
106
+ if mfs:
107
+ lines += ["", "/* Main functions called by the BSW scheduler */"]
108
+ lines += [f"extern FUNC(void, {MIP}_CODE) {fn}(void);" for fn in mfs]
109
+ lines += ["", f"#endif /* {g} */", ""]
110
+ return name, "\n".join(lines)
111
+
112
+
113
+ def module_memmap(ctx, mo, pragmas: dict) -> tuple:
114
+ mip, MIP = mo.mip, mo.MIP
115
+ used = _scan(ctx, re.compile(rf"\b{re.escape(MIP)}_START_SEC_(\w+)"))
116
+ sections = list(DEFAULT_SECTIONS) + sorted(s for s in used if s not in DEFAULT_SECTIONS)
117
+ name = f"{mip}_MemMap.h"
118
+ lines = [mo._banner(name),
119
+ "/* Memory mapping of the module's sections (SWS_MemMap_00032). No include guard: it is included",
120
+ " * once per section switch. Pragmas come from codegen.memmap.pragmas (MemMapAllocation); none by default. */",
121
+ "", "#define MEMMAP_ERROR", ""]
122
+ for i, sec in enumerate(sections):
123
+ kw = f"{MIP}_SEC_{sec}"
124
+ start = pragmas.get(sec, {}).get("start", "") if isinstance(pragmas.get(sec), dict) else ""
125
+ stop = pragmas.get(sec, {}).get("stop", "") if isinstance(pragmas.get(sec), dict) else ""
126
+ lines += [
127
+ f"{'#if' if i == 0 else '#elif'} defined({MIP}_START_SEC_{sec})",
128
+ f" #undef {MIP}_START_SEC_{sec}",
129
+ " #undef MEMMAP_ERROR",
130
+ f" #ifdef {MIP}_MEMMAP_SEQUENCE_OPEN",
131
+ f' #error "{name}: {kw}: STOP the open section before starting a new one"',
132
+ " #endif",
133
+ f" #define {MIP}_MEMMAP_SEQUENCE_OPEN",
134
+ *( [f" {start}"] if start else []),
135
+ f"#elif defined({MIP}_STOP_SEC_{sec})",
136
+ f" #undef {MIP}_STOP_SEC_{sec}",
137
+ " #undef MEMMAP_ERROR",
138
+ f" #ifndef {MIP}_MEMMAP_SEQUENCE_OPEN",
139
+ f' #error "{name}: {kw}: STOP without START"',
140
+ " #endif",
141
+ f" #undef {MIP}_MEMMAP_SEQUENCE_OPEN",
142
+ *( [f" {stop}"] if stop else []),
143
+ ]
144
+ lines += ["#endif", "", "#if defined(MEMMAP_ERROR)", f' #error "{name}: unknown memory section"', "#endif", ""]
145
+ return name, "\n".join(lines), sections
146
+
147
+
148
+ def aggregate_memmap(ctx, entries: list) -> str:
149
+ """Legacy MemMap.h dispatching to the per-module files (only emitted when the project has none)."""
150
+ lines = [W.banner("MemMap.h", "MemMap", "-", ctx.release, [], "-"),
151
+ "/* Dispatches each <MIP>_START_SEC_/<MIP>_STOP_SEC_ keyword to <Mip>_MemMap.h. No include guard. */", ""]
152
+ first = True
153
+ for mip, sections in entries:
154
+ MIP = mip.upper()
155
+ cond = " || \\\n ".join(f"defined({MIP}_{k}_SEC_{s})" for s in sections for k in ("START", "STOP"))
156
+ lines += [f"{'#if' if first else '#elif'} {cond}", f' #include "{mip}_MemMap.h"']
157
+ first = False
158
+ lines += ["#else", ' #error "MemMap.h: unknown memory section"', "#endif", ""]
159
+ return "\n".join(lines)
@@ -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