@h2loop/autosar 1.5.4 → 1.7.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.
@@ -0,0 +1,288 @@
1
+ """Completeness of the communication chain: what a message / signal needs in every layer and file.
2
+
3
+ Consistency rules (overlap, ranges, extract layout) do not see an UNFINISHED change: a new message with no PduR
4
+ route or no CanIf PDU, a signal added to the extract but not to Com, a ComSignal attached to no I-PDU, an I-PDU
5
+ triggered on a channel but carried by no frame. These rules do, so the verify -> fix loop has something to fix.
6
+ Nothing is keyed by names: navigation is by ECUC definition (module-relative) and by reference.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from . import rule
11
+ from .system import mappings_for, system_ipdus
12
+
13
+ CITE_ROUTE = ("SWS_PduR (an I-PDU handled by Com is routed by a PduR routing path to/from a lower-layer PDU); "
14
+ "SWS_CanIf CanIfTxPduRef/CanIfRxPduRef; Com ComPduIdRef")
15
+ CITE_SIG = "Com ComIPduSignalRef (each ComSignal belongs to an I-PDU)"
16
+ CITE_EXTRACT = "System Template ISignalToIPduMapping vs ECUC_Com ComSignal (the ECU configuration implements the extract)"
17
+ CITE_TRIG = "System Template PduTriggering/FrameTriggering/ISignalTriggering (what is sent on a channel is triggered)"
18
+ UPPER = {"Com", "PduR", "EcuC", "EcucValueCollection"}
19
+
20
+
21
+ def _module(ctx, e) -> str:
22
+ return ctx.module_of(e).name
23
+
24
+
25
+ def _pdu_users(ctx) -> dict:
26
+ """EcuC Pdu path -> {module names of containers that reference it}."""
27
+ users: dict = {}
28
+ for path, group in ctx.p.by_path.items():
29
+ e = group[0]
30
+ if e.kind != "container":
31
+ continue
32
+ mod = _module(ctx, e)
33
+ for g in group:
34
+ for pv in g.params:
35
+ if pv.is_ref and pv.value:
36
+ users.setdefault(pv.value.strip(), set()).add(mod)
37
+ return users
38
+
39
+
40
+ def _routes(ctx) -> dict:
41
+ """EcuC Pdu path -> set of EcuC Pdu paths at the other end of a PduR routing path."""
42
+ out: dict = {}
43
+ for rp in ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
44
+ src = ctx.target(ctx.value(rp, "PduRSrcPduRRef"))
45
+ spdu = ctx.target(ctx.value(src, "PduRSrcPduRef")) if src is not None else None
46
+ for dpv in ctx.values(rp, "PduRDestPduRRef"):
47
+ d = ctx.target(dpv)
48
+ dpdu = ctx.target(ctx.value(d, "PduRDestPduRef")) if d is not None else None
49
+ if spdu is not None and dpdu is not None:
50
+ out.setdefault(spdu.path, set()).add(dpdu.path)
51
+ out.setdefault(dpdu.path, set()).add(spdu.path)
52
+ return out
53
+
54
+
55
+ @rule("H2L-CHAIN-001", "error", CITE_ROUTE, "every Com I-PDU is routed by PduR to/from a lower-layer (bus) PDU")
56
+ def com_ipdu_routed(ctx):
57
+ if not ctx.containers("PduR/PduRRoutingPaths/PduRRoutingPath"):
58
+ return # no PduR configured in this project: nothing to check against
59
+ users = _pdu_users(ctx)
60
+ routes = _routes(ctx)
61
+ for ip in ctx.containers("Com/ComConfig/ComIPdu"):
62
+ pv = ctx.value(ip, "ComPduIdRef")
63
+ pdu = ctx.target(pv)
64
+ if pdu is None:
65
+ yield ip, ip.line, f"{ip.name}: ComPduIdRef is missing or points nowhere, so the I-PDU has no EcuC Pdu", None
66
+ continue
67
+ ends = routes.get(pdu.path, set())
68
+ if not ends:
69
+ yield ip, ip.line, (f"{ip.name}: no PduR routing path has its EcuC Pdu {pdu.name} as source or destination, "
70
+ "so Com can neither send nor receive it (add the routing path)"), pv.def_ref
71
+ continue
72
+ lower = [e for e in ends if users.get(e, set()) - UPPER]
73
+ if not lower:
74
+ names = ", ".join(sorted(p.rsplit("/", 1)[-1] for p in ends))
75
+ yield ip, ip.line, (f"{ip.name}: routed to/from {names}, but no lower-layer module (CanIf, LinIf, FrIf, "
76
+ "SoAd, ...) refers to that Pdu, so nothing reaches the bus (add the bus-interface PDU)"), pv.def_ref
77
+
78
+
79
+ @rule("H2L-CHAIN-002", "error", CITE_SIG, "every ComSignal belongs to a Com I-PDU")
80
+ def com_signal_attached(ctx):
81
+ owned = {pv.value.strip() for ip in ctx.containers("Com/ComConfig/ComIPdu")
82
+ for pv in ctx.values(ip, "ComIPduSignalRef") if pv.value}
83
+ for s in ctx.containers("Com/ComConfig/ComSignal"):
84
+ if s.path not in owned:
85
+ yield s, s.line, f"{s.name}: no ComIPdu lists it in ComIPduSignalRef, so it is never sent or received", None
86
+
87
+
88
+ @rule("H2L-CHAIN-003", "error", CITE_EXTRACT, "every signal the extract maps into a Com I-PDU has a ComSignal")
89
+ def extract_signals_have_comsignals(ctx):
90
+ sysi = ctx.p.system
91
+ if not sysi.by_path:
92
+ return
93
+ for ip in ctx.containers("Com/ComConfig/ComIPdu"):
94
+ pdu = ctx.target(ctx.value(ip, "ComPduIdRef"))
95
+ s_ip = system_ipdus(ctx, pdu)
96
+ if not s_ip:
97
+ continue
98
+ mapped = {(m.parent, m.one("I-SIGNAL-REF")) for x in s_ip for m in
99
+ (sysi.get(c) for c in (sysi.get(x).children if sysi.get(x) else [])) if m is not None and m.one("I-SIGNAL-REF")}
100
+ if not mapped:
101
+ continue
102
+ covered = set()
103
+ linked_any = False
104
+ members = [ctx.target(pv) for pv in ctx.values(ip, "ComIPduSignalRef")]
105
+ for pv in ctx.values(ip, "ComIPduSignalGroupRef"): # signal groups: their ComGroupSignals implement members
106
+ grp = ctx.target(pv)
107
+ if grp is not None:
108
+ members.append(grp)
109
+ members.extend(c for c in grp.children if ctx.rel(c.def_ref).endswith("ComGroupSignal"))
110
+ for s in members:
111
+ if s is None:
112
+ continue
113
+ ms = mappings_for(ctx, s, s_ip)
114
+ linked_any = linked_any or bool(ms)
115
+ covered |= {m.one("I-SIGNAL-REF") for m in ms}
116
+ if not linked_any and (ctx.values(ip, "ComIPduSignalRef") or ctx.values(ip, "ComIPduSignalGroupRef")):
117
+ continue # this project's ComSignals do not link to the extract at all: nothing to compare
118
+ for parent, isig in sorted(mapped):
119
+ if isig not in covered:
120
+ yield ip, ip.line, (f"{ip.name}: the extract maps I-SIGNAL {isig.rsplit('/', 1)[-1]} into {parent} "
121
+ "but no ComSignal of this I-PDU implements it (add the ComSignal, e.g. "
122
+ "autosar_add_signal / autosar_add_like)"), None
123
+
124
+
125
+ @rule("H2L-CHAIN-004", "warning", CITE_TRIG, "a triggered I-PDU is carried by a triggered frame; its signals are triggered")
126
+ def extract_triggering_complete(ctx):
127
+ sysi = ctx.p.system
128
+ if not sysi.by_path:
129
+ return
130
+ frame_of = {}
131
+ for m in sysi.tags("PDU-TO-FRAME-MAPPING"):
132
+ for p in m.values.get("PDU-REF", []):
133
+ frame_of.setdefault(p, set()).add(m.parent)
134
+ triggered_frames = {ft.one("FRAME-REF") for tag in ("CAN-FRAME-TRIGGERING", "FLEXRAY-FRAME-TRIGGERING",
135
+ "LIN-FRAME-TRIGGERING", "ETHERNET-FRAME-TRIGGERING")
136
+ for ft in sysi.tags(tag)}
137
+ lines = {}
138
+ for pt in sysi.tags("PDU-TRIGGERING"):
139
+ ipdu = pt.one("I-PDU-REF")
140
+ ii = sysi.get(ipdu)
141
+ if ii is None or ii.tag != "I-SIGNAL-I-PDU":
142
+ continue
143
+ frames = frame_of.get(ipdu, set())
144
+ if frames and not (frames & triggered_frames):
145
+ lines.setdefault(pt.path, []).append(
146
+ f"I-PDU {ipdu.rsplit('/', 1)[-1]} is triggered on {pt.parent} but its frame(s) "
147
+ f"{', '.join(sorted(f.rsplit('/', 1)[-1] for f in frames))} have no frame triggering")
148
+ trig = {sysi.get(st).one("I-SIGNAL-REF") for st in pt.values.get("I-SIGNAL-TRIGGERING-REF", []) if sysi.get(st)}
149
+ if not pt.values.get("I-SIGNAL-TRIGGERING-REF"):
150
+ continue # this project does not list signal triggerings on PDU triggerings
151
+ for c in ii.children:
152
+ m = sysi.get(c)
153
+ isig = m.one("I-SIGNAL-REF") if m is not None else None
154
+ if isig and isig not in trig:
155
+ lines.setdefault(pt.path, []).append(
156
+ f"I-SIGNAL {isig.rsplit('/', 1)[-1]} is mapped into {ipdu.rsplit('/', 1)[-1]} but {pt.path.rsplit('/', 1)[-1]} "
157
+ "has no I-SIGNAL-TRIGGERING for it")
158
+ if not lines:
159
+ return
160
+ # report against an ECUC element is the framework's shape; anchor each finding on a Com I-PDU that uses it
161
+ anchors = {}
162
+ for ip in ctx.containers("Com/ComConfig/ComIPdu"):
163
+ pdu = ctx.target(ctx.value(ip, "ComPduIdRef"))
164
+ for x in system_ipdus(ctx, pdu):
165
+ anchors.setdefault(x, ip)
166
+ for ptp, msgs in sorted(lines.items()):
167
+ pt = sysi.get(ptp)
168
+ ip = anchors.get(pt.one("I-PDU-REF")) if pt else None
169
+ if ip is None:
170
+ continue
171
+ for msg in msgs:
172
+ yield ip, ip.line, msg, None
173
+
174
+
175
+ def _mapped_system_signals(sysi) -> set:
176
+ """System signals that a SYSTEM-MAPPING maps to an SWC data element (SENDER-RECEIVER-TO-SIGNAL-MAPPING)."""
177
+ out = set()
178
+ for sm in sysi.tags("SYSTEM-MAPPING"):
179
+ out.update(sm.values.get("SYSTEM-SIGNAL-REF", []))
180
+ return out
181
+
182
+
183
+ @rule("H2L-CHAIN-005", "warning", "System Template SenderReceiverToSignalMapping (application data reaches the bus "
184
+ "through a data element mapped to the system signal)", "a sent signal is mapped to an SWC data element")
185
+ def sent_signal_mapped_to_swc(ctx):
186
+ sysi = ctx.p.system
187
+ if not sysi.by_path:
188
+ return
189
+ mapped = _mapped_system_signals(sysi)
190
+ if not mapped:
191
+ return # this project maps no data elements to signals: nothing to compare against
192
+ for ip in ctx.containers("Com/ComConfig/ComIPdu"):
193
+ if (ctx.text(ip, "ComIPduDirection") or "").upper() != "SEND":
194
+ continue
195
+ pdu = ctx.target(ctx.value(ip, "ComPduIdRef"))
196
+ s_ip = system_ipdus(ctx, pdu)
197
+ sigs = [ctx.target(pv) for pv in ctx.values(ip, "ComIPduSignalRef")]
198
+ linked = [(s, mappings_for(ctx, s, s_ip)) for s in sigs if s is not None]
199
+ # only where the I-PDU's other signals ARE mapped: a whole BSW-filled I-PDU (counters, NM) is not flagged
200
+ has = [s for s, ms in linked if any(sysi.get(m.one("I-SIGNAL-REF")) and
201
+ sysi.get(m.one("I-SIGNAL-REF")).one("SYSTEM-SIGNAL-REF") in mapped for m in ms)]
202
+ if not has and len(linked) > 1:
203
+ continue
204
+ for s, ms in linked:
205
+ ss = {sysi.get(m.one("I-SIGNAL-REF")).one("SYSTEM-SIGNAL-REF") for m in ms if sysi.get(m.one("I-SIGNAL-REF"))}
206
+ if ms and not (ss & mapped):
207
+ yield s, s.line, (f"{s.name} (sent in {ip.name}): its system signal {', '.join(sorted(x.rsplit('/', 1)[-1] for x in ss if x))} "
208
+ "is not mapped to any SWC data element (no SENDER-RECEIVER-TO-SIGNAL-MAPPING), so no "
209
+ "application value reaches it; map the providing port's data element to it"), None
210
+
211
+
212
+ @rule("H2L-CHAIN-006", "warning", "SWS_Rte data send/receive points (a port no runnable accesses is never served)",
213
+ "every sender-receiver port of an SWC is accessed by a runnable")
214
+ def port_accessed(ctx):
215
+ sysi = ctx.p.system
216
+ if not sysi.by_path:
217
+ return
218
+ accessed = set()
219
+ for tag in ("VARIABLE-ACCESS", "RUNNABLE-ENTITY", "SWC-INTERNAL-BEHAVIOR"):
220
+ for i in sysi.tags(tag):
221
+ accessed.update(i.values.get("PORT-PROTOTYPE-REF", []))
222
+ if not accessed:
223
+ return
224
+ swcs = [i for t in ("APPLICATION-SW-COMPONENT-TYPE", "SENSOR-ACTUATOR-SW-COMPONENT-TYPE") for i in sysi.tags(t)]
225
+ anchors = ctx.containers("Rte/RteSwComponentInstance") or ctx.containers("Com/ComConfig/ComIPdu")
226
+ for swc in swcs:
227
+ ports = [sysi.get(c) for c in swc.children]
228
+ sr = [p for p in ports if p is not None and p.tag in ("P-PORT-PROTOTYPE", "R-PORT-PROTOTYPE")
229
+ and (sysi.get(p.one("PROVIDED-INTERFACE-TREF") or p.one("REQUIRED-INTERFACE-TREF")) or p).tag == "SENDER-RECEIVER-INTERFACE"]
230
+ if not any(p.path in accessed for p in sr):
231
+ continue # this SWC has no accessed S/R port at all (not modelled to that depth)
232
+ for p in sr:
233
+ if p.path not in accessed and anchors:
234
+ a = anchors[0]
235
+ yield a, a.line, (f"port {p.path} of {swc.path.rsplit('/', 1)[-1]} is not accessed by any runnable "
236
+ "(no data send/receive point refers to it), so the RTE never reads or writes it"), None
237
+
238
+
239
+ def _ident_int(v):
240
+ try:
241
+ return int(str(v).strip(), 0)
242
+ except (TypeError, ValueError):
243
+ return None
244
+
245
+
246
+ @rule("H2L-CHAIN-007", "error", "System Template FrameTriggering.identifier (one frame per CAN id on a channel)",
247
+ "two frames do not use the same CAN id on one channel")
248
+ def frame_ids_unique(ctx):
249
+ sysi = ctx.p.system
250
+ if not sysi.by_path:
251
+ return
252
+ seen: dict = {}
253
+ for ft in sysi.tags("CAN-FRAME-TRIGGERING"):
254
+ i = _ident_int(ft.one("IDENTIFIER"))
255
+ if i is not None:
256
+ seen.setdefault((ft.parent, i), []).append(ft)
257
+ anchors = ctx.containers("CanIf/CanIfInitCfg/CanIfTxPduCfg") or ctx.containers("Com/ComConfig/ComIPdu")
258
+ if not anchors:
259
+ return
260
+ for (ch, i), fts in sorted(seen.items(), key=lambda x: (x[0][0] or "", x[0][1])):
261
+ if len(fts) > 1:
262
+ a = anchors[0]
263
+ yield a, a.line, (f"CAN id 0x{i:X} is used by {len(fts)} frame triggerings on {ch}: "
264
+ f"{', '.join(f.path.rsplit('/', 1)[-1] for f in fts)} (a new frame kept its template's id?)"), None
265
+
266
+
267
+ @rule("H2L-SYS-003", "error", "System Template CanFrameTriggering.identifier vs CanIf CanIfTxPduCanId/CanIfRxPduCanId",
268
+ "a CanIf PDU's CAN id equals the frame triggering id of the frame that carries it")
269
+ def canif_id_matches_extract(ctx):
270
+ sysi = ctx.p.system
271
+ if not sysi.by_path:
272
+ return
273
+ ft_by_ipdu: dict = {}
274
+ for ft in sysi.tags("CAN-FRAME-TRIGGERING"):
275
+ for ip in sysi.ipdus_of_triggering(ft.path):
276
+ ft_by_ipdu.setdefault(ip, []).append(ft)
277
+ for kind, idp, refp in (("Tx", "CanIfTxPduCanId", "CanIfTxPduRef"), ("Rx", "CanIfRxPduCanId", "CanIfRxPduRef")):
278
+ for c in ctx.containers(f"CanIf/CanIfInitCfg/CanIf{kind}PduCfg"):
279
+ pv = ctx.value(c, idp)
280
+ cid = _ident_int(pv.value) if pv is not None else None
281
+ pdu = ctx.target(ctx.value(c, refp))
282
+ if cid is None or pdu is None:
283
+ continue
284
+ fts = [ft for ip in system_ipdus(ctx, pdu) for ft in ft_by_ipdu.get(ip, [])]
285
+ ids = {_ident_int(ft.one("IDENTIFIER")) for ft in fts} - {None}
286
+ if ids and cid not in ids:
287
+ yield c, pv.node.line, (f"{c.name}: {idp}=0x{cid:X} but the extract sends its I-PDU in a frame triggered "
288
+ f"with id {', '.join(f'0x{x:X}' for x in sorted(ids))}"), pv.def_ref
@@ -23,6 +23,8 @@ Hand-written semantic rules (ecuc_engine/rules, ids H2L-*) run afterwards, with
23
23
  """
24
24
  from __future__ import annotations
25
25
 
26
+ import os
27
+
26
28
  from typing import Optional
27
29
 
28
30
  from . import defs as D
@@ -211,7 +213,8 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
211
213
  check_elem_group(m.path)
212
214
 
213
215
  from .rules import run_rules
214
- dg.items.extend(run_rules(project))
216
+ found = list(run_rules(project))
217
+ dg.items.extend(_delta_only(project, found))
215
218
  from .probe import load_cfg
216
219
  cfg = load_cfg(project.root)
217
220
  if code: # configuration completeness seen from the C code: handles used but not configured
@@ -282,3 +285,67 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
282
285
  def _nomatch(project: Project, msg: str) -> dict:
283
286
  return {"file": "", "line": 0, "path": project.root, "rule": "H2L-FILTER-NOMATCH", "severity": "error",
284
287
  "message": msg, "defRef": None}
288
+
289
+
290
+ # Convention rules (a sent signal mapped to an SWC data element, a port accessed by a runnable) are true for what a
291
+ # change adds but not for every project's legacy (BSW-filled counters, unused ports). They report only findings that
292
+ # the last commit did not already have, i.e. what this working copy's changes left incomplete. Without git (or
293
+ # without a commit) they report nothing.
294
+ DELTA_RULES = ("H2L-CHAIN-005", "H2L-CHAIN-006")
295
+
296
+
297
+ def _delta_only(project: Project, found: list) -> list:
298
+ keep = [d for d in found if d["rule"] not in DELTA_RULES]
299
+ delta = [d for d in found if d["rule"] in DELTA_RULES]
300
+ if not delta:
301
+ return keep
302
+ base = _baseline_findings(project)
303
+ if base is None:
304
+ return keep
305
+ import re as _re
306
+ key = lambda d: (d["rule"], d["path"], _re.sub(r"(\.arxml|\.ARXML):\d+", r"\1", d["message"])) # noqa: E731
307
+ return keep + [d for d in delta if key(d) not in {key(b) for b in base}]
308
+
309
+
310
+ def _baseline_findings(project: Project) -> Optional[list]:
311
+ """DELTA_RULES findings on the files as of git HEAD (None when there is no usable HEAD)."""
312
+ import shutil
313
+ import subprocess
314
+ import tempfile
315
+ root = project.root
316
+ try:
317
+ top = subprocess.run(["git", "-C", root, "rev-parse", "--show-toplevel"], capture_output=True, text=True,
318
+ timeout=20).stdout.strip()
319
+ if not top or subprocess.run(["git", "-C", root, "rev-parse", "--verify", "-q", "HEAD"], capture_output=True,
320
+ timeout=20).returncode != 0:
321
+ return None
322
+ except (OSError, subprocess.SubprocessError):
323
+ return None
324
+ files = list(project.sources.config) + list(project.sources.input)
325
+ tmp = tempfile.mkdtemp(prefix="ecuc-base-")
326
+ try:
327
+ for f in files:
328
+ rel_top = os.path.relpath(os.path.realpath(f), os.path.realpath(top))
329
+ r = subprocess.run(["git", "-C", top, "show", f"HEAD:{rel_top}"], capture_output=True, timeout=60)
330
+ if r.returncode != 0:
331
+ continue # new in this working copy: absent from the baseline
332
+ dst = os.path.join(tmp, os.path.relpath(f, root))
333
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
334
+ with open(dst, "wb") as fh:
335
+ fh.write(r.stdout)
336
+ cfg = [os.path.join(tmp, os.path.relpath(f, root)) for f in project.sources.config
337
+ if os.path.exists(os.path.join(tmp, os.path.relpath(f, root)))]
338
+ inp = os.path.join(tmp, ".hydron")
339
+ os.makedirs(inp, exist_ok=True)
340
+ import json as _json
341
+ with open(os.path.join(inp, "autosar.json"), "w") as fh:
342
+ _json.dump({"input": [os.path.relpath(os.path.join(tmp, os.path.relpath(f, root)), tmp)
343
+ for f in project.sources.input
344
+ if os.path.exists(os.path.join(tmp, os.path.relpath(f, root)))]}, fh)
345
+ from .rules import run_rules
346
+ bp = Project(tmp, config_files=cfg or None, def_files=project.sources.defs or None)
347
+ return [d for d in run_rules(bp) if d["rule"] in DELTA_RULES]
348
+ except Exception: # noqa: BLE001 -- a baseline that cannot be built just turns the delta rules off
349
+ return None
350
+ finally:
351
+ shutil.rmtree(tmp, ignore_errors=True)
@@ -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` |
@@ -110,7 +111,13 @@ Do not decide what to change from memory or from the prompt alone. Each change g
110
111
  3. **Change** with the structured tools only (`ecuc_edit`, `arxml_edit` incl. `new_file`, `autosar_add_signal`). Prefer
111
112
  one call per coherent change; nothing is written if any op fails.
112
113
  4. **Verify in two steps → fix → verify, until clean.**
113
- - Validation 1 (deterministic, the hard gate): `ecuc_validate` (ECUC and extract/SWC files alike), then
114
+ - Validation 1 (deterministic, the hard gate): `ecuc_validate` (ECUC and extract/SWC files alike, including the
115
+ COMPLETENESS rules: H2L-CHAIN-001 Com I-PDU routed by PduR to a bus PDU, -002 ComSignal attached to an I-PDU,
116
+ -003 extract signal implemented by a ComSignal, -004 triggered I-PDU in a triggered frame, -005 sent signal mapped
117
+ to a port data element, -006 port accessed by a runnable, -007 no CAN id used twice on a channel, H2L-SYS-003 CanIf
118
+ CAN id = frame id; -005/-006 report only what this working copy's changes added). An H2L-CHAIN finding means the
119
+ change is UNFINISHED: do the missing part, then validate again. `arxml_edit`, `autosar_add_like` and
120
+ `autosar_add_signal` validate after every write and say what is still to do.
114
121
  `autosar_generate` + `autosar_codegen_diff` / `autosar_build` and `autosar_verify_rte` where the project has them.
115
122
  - Validation 2 (against the documents): `ecuc_validate docs=true` checks every element changed in this session
116
123
  against the ingested specs/manuals and returns a checklist; answer it line by line (OK / VIOLATED / NOT COVERED,
@@ -118,11 +125,26 @@ Do not decide what to change from memory or from the prompt alone. Each change g
118
125
  On any problem: read it, go back to step 1 for that fact, fix with the structured tools, verify again. Repeat until
119
126
  every check is clean. Then re-read the request and tick each todo against the evidence. Never report done with a
120
127
  failing or unrun check; if something cannot be made clean, say exactly what and why.
121
- The pack enforces this: the first edit of a session is refused until a grounding tool ran, C edits need
128
+ The pack enforces this: the first edit of a session is refused until a grounding tool ran (and the refusal
129
+ carries the context engine's facts for that edit's target, run for you), C edits need
122
130
  `autosar_build_context` (or `autosar_audit`) first, and the system prompt lists every open verification item
123
131
  (unvalidated edits, failing validation, unverified C, changed elements not yet checked against the documents)
124
132
  until it is resolved.
125
133
 
134
+ ## A new thing that resembles an existing one: autosar_add_like (preferred)
135
+ Most changes add something the project already has one of: a message, a port, a runnable with its timing event, a
136
+ 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
137
+ the new one (same direction, bus, kind), then:
138
+ 1. `autosar_add_like like=<its stem> name=<new stem> dry_run=true`: read the Created list (every file it touches)
139
+ and the "Not copied" / "single-valued" lists.
140
+ 2. Name the members you want too: `also={"DrvDoorOpenSts": "SettingsAckCounter"}` gives the new message one signal
141
+ cloned from that one.
142
+ 3. Set the values the request gives with `set` (ECUC param name, XML tag, or `Owner:key` when several clones have
143
+ it): CAN id (`IDENTIFIER`, `CanIfTxPduCanId`), lengths (`PduLength`, `FRAME-LENGTH`, `I-SIGNAL-I-PDU:LENGTH`),
144
+ period (`ComTxModeTimePeriod`), signal layout (`<sig>:ComBitPosition`, `<sig>:ComBitSize`, `START-POSITION`) ...
145
+ 4. Run it without dry_run. It validates and rolls back on any new finding. Then fix what is listed as single-valued
146
+ (e.g. the S/R-to-signal mapping of a new port) with arxml_edit, and verify as below.
147
+
126
148
  ## New ARXML file (SWC, interface, data types, extract) from nothing
127
149
  1. ONE `arxml_edit` call. Its first op creates the file:
128
150
  `{op:"new_file", file:"Input/SWC_X.arxml", packages:["/DataTypes", "/Interfaces", "/Swcs"]}` (R25-11 header and empty