@h2loop/autosar 1.6.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@h2loop/autosar",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "AUTOSAR Classic pack for Hydron: ECUC read/edit-in-place/validate/diff on ARXML, config-to-code trace, generator and build runners",
5
5
  "type": "module",
6
6
  "private": false,
package/plugins/index.js CHANGED
@@ -1,84 +1,93 @@
1
1
  // @bun
2
- import{readFileSync as Le}from"fs";import{isAbsolute as Ue,join as W,resolve as Pe}from"path";import{tool as w}from"@kilocode/plugin";import{homedir as me}from"os";import{join as v}from"path";var ye=()=>process.env.HYDRON_TEST_HOME||me(),U=(r,n)=>process.env[r]||v(ye(),n),be=()=>v(U("XDG_CONFIG_HOME",".config"),"hydron-cli"),we=()=>v(U("XDG_DATA_HOME",".local/share"),"hydron-cli"),_e=()=>v(U("XDG_STATE_HOME",".local/state"),"hydron-cli");async function P(r){let n=Bun.file(r);if(!await n.exists())return;return n.json().catch(()=>{return})}async function L(){let r=process.env.KILO_API_URL?.trim();if(r)return r.replace(/\/+$/,"");let n=await P(v(be(),"server-config.json")),a=typeof n?.apiBaseUrl==="string"?n.apiBaseUrl:void 0;if(!a)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return a.replace(/\/+$/,"")}function $e(r){let n=r?.split(".")[1];if(!n)return"";try{let a=JSON.parse(Buffer.from(n.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof a.sub==="string"?a.sub:""}catch{return""}}async function Se(r){let n=$e(r.access);if(n)return n;if(!r.refresh)return"";return new Bun.CryptoHasher("sha256").update(r.refresh).digest("hex").slice(0,12)}async function X(){let n=(await P(v(we(),"auth.json")))?.hydron;if(!n||n.type!=="oauth"||!n.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return n}async function q(){let r=await X();if(typeof r.expires==="number"&&r.expires>0&&Date.now()>=r.expires-30000)throw Error("The H2Loop session token has expired. Send any message in Hydron (the CLI refreshes it) or re-login with `hydron auth login`, then retry.");return r.access}async function G(r){let n=await fetch(`${await L()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await q()}`},signal:r?AbortSignal.any([r,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(n.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!n.ok)throw Error(`could not list H2Loop projects: HTTP ${n.status}`);let a=await n.json();return Array.isArray(a)?a:a?.items??[]}var V=()=>v(_e(),"h2loop-project.json");async function N(r,n){let a=r===void 0||r===null?"":String(r).trim();if(/^\d+$/.test(a))return a;if(a){let t=await G(n),d=t.filter((p)=>p.name===a),l=d.length?d:t.filter((p)=>p.name?.toLowerCase()===a.toLowerCase());if(l.length===1)return String(l[0].id);if(l.length>1)throw Error(`workspace "${a}" is ambiguous: ${l.map((p)=>`${p.name} (id ${p.id})`).join(", ")}; pass the numeric id`);let f=t.filter((p)=>p.name?.toLowerCase().includes(a.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${a}"`+(f.length?`; similar: ${f.map((p)=>`${p.name} (id ${p.id})`).join(", ")}`:`; you have ${t.length} project(s)`))}let e=await P(V());if(!e)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let s=await X();if(e.project&&typeof e.project==="object"){let t=e.project;if(typeof e.account==="string"&&e.account!==await Se(s))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(t.id!==void 0)return String(t.id)}let o=e;if(o.id===void 0)throw Error(`${V()} holds no project id; select a project again or pass workspace_id.`);if(!(await G(n)).some((t)=>String(t.id)===String(o.id)))throw Error(`The selected project ${o.name??""} (id ${o.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(o.id)}async function x(r,n,a,e,s=120000){let o=`${await L()}${r}?workspace_id=${encodeURIComponent(n)}`,i=await fetch(o,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await q()}`},body:JSON.stringify(a),signal:e?AbortSignal.any([e,AbortSignal.timeout(s)]):AbortSignal.timeout(s)}),t=await i.text();if(i.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(i.status===404&&t.includes("Not Found"))throw Error(`${r} is not available on ${await L()} (backend predates the AUTOSAR endpoints).`);if(!i.ok)throw Error(`H2Loop ${r} failed: HTTP ${i.status} ${t.slice(0,800)}`);try{return JSON.parse(t)}catch(d){throw Error(`H2Loop ${r} returned non-JSON (${String(d)}): ${t.slice(0,300)}`)}}var M=/\.arxml\b/i,F="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit for ECUC values, arxml_edit for System-extract/SWC elements (or autosar_add_signal for a new signal), then ecuc_validate. To start a NEW .arxml file, use arxml_edit with {op:'new_file', file, packages} followed by add ops.";function Y(r,n){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!n)return;let a=typeof n.filePath==="string"?n.filePath:typeof n.file_path==="string"?n.file_path:"";if(["edit","write","multiedit"].includes(r)){let e=Array.isArray(n.edits)?n.edits.some((s)=>typeof s?.filePath==="string"&&M.test(s.filePath)):!1;if(M.test(a)||e)return`${F} (${r} on ${a||"an .arxml file"})`;return}if(r==="apply_patch"||r==="patch"){let e=String(n.patchText??n.patch??""),s=e.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??e.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return s?`${F} (${r} on ${s[1].trim()})`:void 0}if(r==="bash"){let e=String(n.command??"");if(!M.test(e))return;let s=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(e)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(e),o=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(e)||/\btee\b[^|;&]*\.arxml\b/i.test(e),i=/\b(?:mv|cp|ln|install|rsync)\b[^|;&]*\.arxml['"]?\s*(?:$|[|;&])/im.test(e)||/\b(?:os\.(?:rename|replace)|shutil\.(?:move|copy\w*)|fs\.(?:rename|copyFile)\w*|Path\([^)]*\)\.rename)\([^)]*\.arxml/i.test(e)||/\.(?:rename|replace)\(\s*['"][^'"]*\.arxml/i.test(e),t=/open\([^)]*\.arxml[^)]*,\s*(?:mode\s*=\s*)?['"][wa+]/i.test(e)||/\.arxml['"]?\s*\)?\s*\.write_(?:text|bytes)\b/i.test(e)||/write_(?:text|bytes)\([^)]*\)[^;\n]*\.arxml/i.test(e)||/\bwriteFile(?:Sync)?\([^)]*\.arxml/i.test(e)||/\.write\(\s*['"][^'"]*\.arxml/i.test(e)||/\b(?:awk|gawk)\b[^|;&]*-i\s+inplace/.test(e)||/\bed\b\s+-s\s+[^|;&]*\.arxml/.test(e);if(s||o||t||i)return`${F} (bash: ${e.slice(0,120)})`}return}var ke=/(?:^|[;&|(]\s*|\bnode\s+\S*|\bnpx\s+)(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\b/m,ve='Not a shell command: ecuc_*/arxml_*/autosar_* are TOOLS of this session. Call the tool directly (a tool call, not bash) with its arguments as JSON, e.g. arxml_edit with {"ops": [ ... ]}. If a tool call fails, read its error, fix the arguments and call the tool again.';function K(r,n){if(r!=="bash"||!n)return;let a=String(n.command??""),e=/\s(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\s+(?:--|<|\{|\[|'|")/.test(a);if(ke.test(a)||e)return`${ve} (bash: ${a.slice(0,100)})`;if(/\becuc_engine\b|hydron-cli[\\/]store[\\/]/.test(a))return`Blocked: do not run the AUTOSAR pack's engine or files from the plugin store by hand. Use its tools (ecuc_read/ecuc_edit/arxml_edit/ecuc_validate ...) as tool calls. (bash: ${a.slice(0,100)})`;if(/(?:^|[;&|]\s*)(?:\S*\/)?(?:hydron|kilo|opencode)\s+(?:run|serve)\b/m.test(a))return"Blocked: do not start another agent session from inside this one; do the work here with your tools.";return}var Ae=new Set(["autosar_build_context","ecuc_read","ecuc_find","ecuc_crosscheck","autosar_config_lookup","autosar_trace","autosar_audit","query_hydron_project","get_symbol_info"]),Z=new Set(["ecuc_edit","arxml_edit","autosar_add_signal","autosar_add_like"]),Q=/\.(c|h)$/i,ee=/(^|[\\/])(Output|gen|Gen|GenData|generated)[\\/]/;var H=new Map;function I(r){let n=r??"-",a=H.get(n);if(!a)a={grounded:[],contextFor:[],changed:[],validation:"never",cFiles:[],rte:"n/a",docs:"n/a",blocks:0},H.set(n,a);return a}function te(r){if(!r)return"";return String(r.filePath??r.file_path??"")}function ne(r,n,a){if(process.env.AUTOSAR_LOOP_GATES==="0")return;let e=I(r);if(e.blocks>=2)return;if(Z.has(n)&&!a?.dry_run&&e.grounded.length===0)return e.blocks++,"GROUND FIRST: no facts have been gathered in this session, so this change would be decided from memory. Before editing, get every fact the change depends on from the tools and list them with their source: autosar_build_context (target = the module/container/element, or swc+runnable) gives the project facts AND the facts from the ingested documents in one call; ecuc_read (schema=true) for what may be set; ecuc_find for what exists and references what. Then plan from those facts only and retry this edit.";let s=te(a);if((n==="edit"||n==="write"||n==="multiedit")&&Q.test(s)&&!ee.test(s)){if(!e.grounded.includes("autosar_build_context")&&!e.grounded.includes("autosar_audit"))return e.blocks++,`GROUND FIRST: call autosar_build_context for the SW-C/runnable this C file implements (it lists the allowed RTE APIs with exact signatures, the related configuration and the relevant document facts) before changing ${s}. For C that is not runnable code, autosar_audit grounds BSW usage instead. Then retry.`}return}function ie(r,n,a,e){let s=I(r),o=/^\s*(ERROR|Blocked|GROUND FIRST)/.test(e)||e.includes("edit rejected");if(Ae.has(n)&&!o){if(!s.grounded.includes(n))s.grounded.push(n);if(n==="autosar_build_context"){let t=String(a?.target??a?.runnable??a?.swc??"");if(t&&!s.contextFor.includes(t))s.contextFor.push(t)}}if(Z.has(n)&&!o&&!a?.dry_run&&!/^Dry run/.test(e)){s.validation="stale",s.docs="pending";for(let t of Re(n,a,e))if(!s.changed.includes(t))s.changed.push(t)}let i=te(a);if((n==="edit"||n==="write"||n==="multiedit")&&Q.test(i)&&!ee.test(i)&&!o){if(!s.cFiles.includes(i))s.cFiles.push(i);s.rte="pending"}if(n==="ecuc_validate"&&!o){let t=/"error":\s*[1-9]/.test(e.split(`
3
- `)[0]??"")||/^\S+:\d+ error /m.test(e);if(s.validation=t?"errors":"clean",a?.docs&&s.docs==="pending"&&!/DOCUMENT CHECK UNAVAILABLE/.test(e))s.docs="done"}if(n==="autosar_verify_rte"&&!o)s.rte=/\u2014 FAILING/.test(e)?"fail":"pass"}function Re(r,n,a){let e=[];if(r==="ecuc_edit"){let s=z(n?.ops);for(let o of s){let i=String(o.path??""),t=o.param?`${i}/${o.param}`:o.container?`${i}/${o.container}`:i;e.push(o.value!==void 0?`${t} = ${JSON.stringify(o.value).slice(0,80)}`:`${t} (${o.op})`)}}else if(r==="arxml_edit"){let s=a.match(/created (\[[^\]]*\])/);if(s)try{e.push(...JSON.parse(s[1]))}catch{}for(let o of z(n?.ops))if(o.op==="set")e.push(`${o.path}/${o.element} = ${JSON.stringify(o.value)}`)}else if(r==="autosar_add_signal")e.push(`new Com signal ${n?.name} in ${n?.ipdu}`);else if(r==="autosar_add_like"){let s=a.match(/Created:\n((?: .*\n?)+)/);if(s)e.push(...s[1].split(`
4
- `).map((o)=>o.trim()).filter(Boolean));else e.push(`${n?.name} like ${n?.like}`)}return e.filter(Boolean)}function z(r){let n=r;for(let a=0;a<3&&typeof n==="string";a++)try{n=JSON.parse(n)}catch{return[]}if(Array.isArray(n))return n.filter((a)=>a&&typeof a==="object");if(n&&typeof n==="object")return[n];return[]}function re(r){let n=H.get(r??"-");if(!n)return;let a=[];if(n.validation==="stale")a.push("Config/ARXML changed since the last ecuc_validate: run it (unfiltered) now.");if(n.validation==="errors")a.push("The last ecuc_validate reported errors: fix them and validate again.");if(n.rte==="pending")a.push(`C changed (${n.cFiles.slice(-5).join(", ")}): run autosar_verify_rte on it.`);if(n.rte==="fail")a.push("autosar_verify_rte is FAILING: fix the calls (or apply its arxml_edit ops) and verify again.");if(n.docs==="pending"&&n.validation==="clean")a.push(`Changed elements not yet checked against the ingested documents (${n.changed.slice(0,8).join("; ")}${n.changed.length>8?"; \u2026":""}): run ecuc_validate docs=true and answer its checklist item by item.`);if(!a.length)return;return["# AUTOSAR: open verification items (do not report the task as done until each is resolved)",...a.map((e)=>`- ${e}`)].join(`
5
- `)}var Ee=/^(?:hi+|hello+|hey+|hiya|yo|hola|namaste|good\s+(?:morning|afternoon|evening|day)|thanks?|thank\s+you|thx|ty|ok(?:ay)?|cool|nice|great|bye|good\s*bye|see\s+you)(?:\s+(?:there|all|team|hydron|again|so\s+much|a\s+lot))*$/i,se=new Map;function Oe(r){let n=r.trim().replace(/[\p{Extended_Pictographic}\u200D\uFE0F]/gu,"").replace(/[!.?,:;~\-]+/g," ").replace(/\s+/g," ").trim();return n.length>0&&n.length<=40&&Ee.test(n)}function oe(r,n){se.set(r??"-",Oe(n))}function ae(r,n){if(process.env.AUTOSAR_LOOP_GATES==="0")return;if(!se.get(r??"-"))return;return`NO TOOLS: the user only greeted you (no task). Do not call ${n} or any other tool, and do not read or explore the project. Reply in one short line (e.g. what you can help with here) and stop.`}import{existsSync as Ce}from"fs";import{dirname as Te,join as B}from"path";import{fileURLToPath as Ne}from"url";var xe="autosar-data==0.17.0";function je(){let r=Te(Ne(import.meta.url)),n=[B(r,"..","python"),B(r,"..","..","python")].find((a)=>Ce(B(a,"ecuc_engine","__main__.py")));if(!n)throw Error(`ECUC engine not found next to ${r} (expected ../python/ecuc_engine)`);return n}async function Ie(r){try{return await Bun.spawn([...r,"-c","import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}var D={};async function De(){if(D.cmd!==void 0)return D.cmd;let r=process.env.AUTOSAR_PYTHON?.trim(),a=r?[[r]]:[["uv","run","--no-project","--quiet","--with",xe,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let e of a)if(await Ie(e))return D.cmd=e,e;return D.cmd=null,null}async function y(r,n,a){let e=await De();if(!e)return{ok:!1,error:"no Python >= 3.10 found. Install uv (https://docs.astral.sh/uv/) or Python 3.10+, or set AUTOSAR_PYTHON to an interpreter path."};let s=je(),o=Bun.spawn([...e,"-m","ecuc_engine","--root",r,...n,"--json"],{cwd:r,env:{...process.env,PYTHONPATH:[s,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:a}),[i,t]=await Promise.all([new Response(o.stdout).text(),new Response(o.stderr).text()]);await o.exited;let d=i.trim().split(`
6
- `).filter(Boolean).at(-1);if(!d)return{ok:!1,error:`engine produced no output (exit ${o.exitCode}): ${t.trim().slice(-2000)}`};try{return JSON.parse(d)}catch(l){return{ok:!1,error:`engine output was not JSON (${String(l)}): ${d.slice(0,500)} ${t.slice(-500)}`}}}var c=w.schema,Me=["# AUTOSAR pack: only for AUTOSAR work","If the request is about this AUTOSAR project or its work (ARXML/ECUC config, BSW/RTE/MCAL code, AUTOSAR","specs): load the `autosar` skill and use its tools (ecuc_read/find, ecuc_edit/arxml_edit, ecuc_validate,","autosar_build_context). Otherwise (a greeting, an unrelated question) just answer: no tools, no exploring."].join(`
7
- `);function C(r){if(typeof r!=="string")return r;let n=r.trim();if(!n.startsWith("[")&&!n.startsWith("{")&&!n.startsWith('"'))return r;try{return C(JSON.parse(n))}catch{return r}}var A=(r)=>c.preprocess(C,r);function E(r){let n=C(r);if(n===void 0||n===null||n==="")return;if(typeof n==="string")return[n];if(Array.isArray(n))return n.map(String).filter((a)=>a.trim());return[String(n)]}var O=()=>c.preprocess(E,c.array(c.string()).optional()),R=" In answers cite, for every element, its AUTOSAR path (the value's @path, e.g. /Pkg/Com/ComConfig/MySig), its"+" DEFINITION-REF (@def, e.g. /AUTOSAR/EcucDefs/Com/ComConfig/ComSignal) and file:line \u2014 never give the"+` DEFINITION-REF when the path is asked. A value's file:line is its "@at:<Param>" (the VALUE element; "@at" is`+' the container); integers come with "@hex:<Param>" \u2014 copy both, never compute a line or a base conversion'+" yourself. Cite a requirement ID only if it appears verbatim in a tool result or spec text read this session.";function j(r,n=200){let a=r.slice(0,n).map((e)=>`${e.file}:${e.line} ${e.severity.toUpperCase()} ${e.rule} ${e.path} \u2014 ${e.message}${e.defRef?` [def ${e.defRef}]`:""}`);if(r.length>n)a.push(`\u2026 ${r.length-n} more`);return a}function _(r){let n=r.didYouMean?`
8
- Did you mean: ${r.didYouMean.join(", ")}`:"",a=r.schema?`
9
- Schema errors: ${JSON.stringify(r.schema)}`:"";return`ERROR: ${r.error}${n}${a}`}function S(r){let n=r.warnings??[];return n.length?n.map((a)=>`WARNING: ${a}`).join(`
2
+ import{readFileSync as Pe}from"fs";import{isAbsolute as Me,join as X,resolve as Fe}from"path";import{tool as S}from"@kilocode/plugin";import{homedir as be}from"os";import{join as R}from"path";var we=()=>process.env.HYDRON_TEST_HOME||be(),M=(n,t)=>process.env[n]||R(we(),t),_e=()=>R(M("XDG_CONFIG_HOME",".config"),"hydron-cli"),$e=()=>R(M("XDG_DATA_HOME",".local/share"),"hydron-cli"),Se=()=>R(M("XDG_STATE_HOME",".local/state"),"hydron-cli");async function F(n){let t=Bun.file(n);if(!await t.exists())return;return t.json().catch(()=>{return})}async function P(){let n=process.env.KILO_API_URL?.trim();if(n)return n.replace(/\/+$/,"");let t=await F(R(_e(),"server-config.json")),s=typeof t?.apiBaseUrl==="string"?t.apiBaseUrl:void 0;if(!s)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return s.replace(/\/+$/,"")}function ke(n){let t=n?.split(".")[1];if(!t)return"";try{let s=JSON.parse(Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof s.sub==="string"?s.sub:""}catch{return""}}async function Ae(n){let t=ke(n.access);if(t)return t;if(!n.refresh)return"";return new Bun.CryptoHasher("sha256").update(n.refresh).digest("hex").slice(0,12)}async function K(){let t=(await F(R($e(),"auth.json")))?.hydron;if(!t||t.type!=="oauth"||!t.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return t}async function z(){let n=await K();if(typeof n.expires==="number"&&n.expires>0&&Date.now()>=n.expires-30000)throw Error("The H2Loop session token has expired. Send any message in Hydron (the CLI refreshes it) or re-login with `hydron auth login`, then retry.");return n.access}async function q(n){let t=await fetch(`${await P()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await z()}`},signal:n?AbortSignal.any([n,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(t.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!t.ok)throw Error(`could not list H2Loop projects: HTTP ${t.status}`);let s=await t.json();return Array.isArray(s)?s:s?.items??[]}var Y=()=>R(Se(),"h2loop-project.json");async function I(n,t){let s=n===void 0||n===null?"":String(n).trim();if(/^\d+$/.test(s))return s;if(s){let i=await q(t),r=i.filter((l)=>l.name===s),o=r.length?r:i.filter((l)=>l.name?.toLowerCase()===s.toLowerCase());if(o.length===1)return String(o[0].id);if(o.length>1)throw Error(`workspace "${s}" is ambiguous: ${o.map((l)=>`${l.name} (id ${l.id})`).join(", ")}; pass the numeric id`);let u=i.filter((l)=>l.name?.toLowerCase().includes(s.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${s}"`+(u.length?`; similar: ${u.map((l)=>`${l.name} (id ${l.id})`).join(", ")}`:`; you have ${i.length} project(s)`))}let c=await F(Y());if(!c)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let f=await K();if(c.project&&typeof c.project==="object"){let i=c.project;if(typeof c.account==="string"&&c.account!==await Ae(f))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(i.id!==void 0)return String(i.id)}let e=c;if(e.id===void 0)throw Error(`${Y()} holds no project id; select a project again or pass workspace_id.`);if(!(await q(t)).some((i)=>String(i.id)===String(e.id)))throw Error(`The selected project ${e.name??""} (id ${e.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(e.id)}async function D(n,t,s,c,f=120000){let e=`${await P()}${n}?workspace_id=${encodeURIComponent(t)}`,a=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await z()}`},body:JSON.stringify(s),signal:c?AbortSignal.any([c,AbortSignal.timeout(f)]):AbortSignal.timeout(f)}),i=await a.text();if(a.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(a.status===404&&i.includes("Not Found"))throw Error(`${n} is not available on ${await P()} (backend predates the AUTOSAR endpoints).`);if(!a.ok)throw Error(`H2Loop ${n} failed: HTTP ${a.status} ${i.slice(0,800)}`);try{return JSON.parse(i)}catch(r){throw Error(`H2Loop ${n} returned non-JSON (${String(r)}): ${i.slice(0,300)}`)}}var H=/\.arxml\b/i,B="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit for ECUC values, arxml_edit for System-extract/SWC elements (or autosar_add_signal for a new signal), then ecuc_validate. To start a NEW .arxml file, use arxml_edit with {op:'new_file', file, packages} followed by add ops.";function Z(n,t){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!t)return;let s=typeof t.filePath==="string"?t.filePath:typeof t.file_path==="string"?t.file_path:"";if(["edit","write","multiedit"].includes(n)){let c=Array.isArray(t.edits)?t.edits.some((f)=>typeof f?.filePath==="string"&&H.test(f.filePath)):!1;if(H.test(s)||c)return`${B} (${n} on ${s||"an .arxml file"})`;return}if(n==="apply_patch"||n==="patch"){let c=String(t.patchText??t.patch??""),f=c.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??c.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return f?`${B} (${n} on ${f[1].trim()})`:void 0}if(n==="bash"){let c=String(t.command??"");if(!H.test(c))return;let f=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(c)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(c),e=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(c)||/\btee\b[^|;&]*\.arxml\b/i.test(c),a=/\b(?:mv|cp|ln|install|rsync)\b[^|;&]*\.arxml['"]?\s*(?:$|[|;&])/im.test(c)||/\b(?:os\.(?:rename|replace)|shutil\.(?:move|copy\w*)|fs\.(?:rename|copyFile)\w*|Path\([^)]*\)\.rename)\([^)]*\.arxml/i.test(c)||/\.(?:rename|replace)\(\s*['"][^'"]*\.arxml/i.test(c),i=/open\([^)]*\.arxml[^)]*,\s*(?:mode\s*=\s*)?['"][wa+]/i.test(c)||/\.arxml['"]?\s*\)?\s*\.write_(?:text|bytes)\b/i.test(c)||/write_(?:text|bytes)\([^)]*\)[^;\n]*\.arxml/i.test(c)||/\bwriteFile(?:Sync)?\([^)]*\.arxml/i.test(c)||/\.write\(\s*['"][^'"]*\.arxml/i.test(c)||/\b(?:awk|gawk)\b[^|;&]*-i\s+inplace/.test(c)||/\bed\b\s+-s\s+[^|;&]*\.arxml/.test(c);if(f||e||i||a)return`${B} (bash: ${c.slice(0,120)})`}return}var ve=/(?:^|[;&|(]\s*|\bnode\s+\S*|\bnpx\s+)(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\b/m,Re='Not a shell command: ecuc_*/arxml_*/autosar_* are TOOLS of this session. Call the tool directly (a tool call, not bash) with its arguments as JSON, e.g. arxml_edit with {"ops": [ ... ]}. If a tool call fails, read its error, fix the arguments and call the tool again.';function Q(n,t){if(n!=="bash"||!t)return;let s=String(t.command??""),c=/\s(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\s+(?:--|<|\{|\[|'|")/.test(s);if(ve.test(s)||c)return`${Re} (bash: ${s.slice(0,100)})`;if(/\becuc_engine\b|hydron-cli[\\/]store[\\/]/.test(s))return`Blocked: do not run the AUTOSAR pack's engine or files from the plugin store by hand. Use its tools (ecuc_read/ecuc_edit/arxml_edit/ecuc_validate ...) as tool calls. (bash: ${s.slice(0,100)})`;if(/(?:^|[;&|]\s*)(?:\S*\/)?(?:hydron|kilo|opencode)\s+(?:run|serve)\b/m.test(s))return"Blocked: do not start another agent session from inside this one; do the work here with your tools.";return}var Ee=new Set(["autosar_build_context","ecuc_read","ecuc_find","ecuc_crosscheck","autosar_config_lookup","autosar_trace","autosar_audit","query_hydron_project","get_symbol_info"]),te=new Set(["ecuc_edit","arxml_edit","autosar_add_signal","autosar_add_like"]),ne=/\.(c|h)$/i,ie=/(^|[\\/])(Output|gen|Gen|GenData|generated)[\\/]/;var J=new Map;function L(n){let t=n??"-",s=J.get(t);if(!s)s={grounded:[],contextFor:[],changed:[],validation:"never",cFiles:[],rte:"n/a",docs:"n/a",blocks:0},J.set(t,s);return s}function re(n){if(!n)return"";return String(n.filePath??n.file_path??"")}function se(n,t,s){if(process.env.AUTOSAR_LOOP_GATES==="0")return;let c=L(n);if(c.blocks>=2)return;if(te.has(t)&&!s?.dry_run&&c.grounded.length===0)return c.blocks++,"GROUND FIRST: no facts have been gathered in this session, so this change would be decided from memory. Before editing, get every fact the change depends on from the tools and list them with their source: autosar_build_context (target = the module/container/element, or swc+runnable) gives the project facts AND the facts from the ingested documents in one call; ecuc_read (schema=true) for what may be set; ecuc_find for what exists and references what. Then plan from those facts only and retry this edit.";let f=re(s);if((t==="edit"||t==="write"||t==="multiedit")&&ne.test(f)&&!ie.test(f)){if(!c.grounded.includes("autosar_build_context")&&!c.grounded.includes("autosar_audit"))return c.blocks++,`GROUND FIRST: call autosar_build_context for the SW-C/runnable this C file implements (it lists the allowed RTE APIs with exact signatures, the related configuration and the relevant document facts) before changing ${f}. For C that is not runnable code, autosar_audit grounds BSW usage instead. Then retry.`}return}function W(n,t,s,c){let f=L(n),e=/^\s*(ERROR|Blocked|GROUND FIRST)/.test(c)||c.includes("edit rejected");if(Ee.has(t)&&!e){if(!f.grounded.includes(t))f.grounded.push(t);if(t==="autosar_build_context"){let i=String(s?.target??s?.runnable??s?.swc??"");if(i&&!f.contextFor.includes(i))f.contextFor.push(i)}}if(te.has(t)&&!e&&!s?.dry_run&&!/^Dry run/.test(c)){f.validation=/AUTO-VALIDATE: clean/.test(c)?"clean":/AUTO-VALIDATE:/.test(c)?"errors":"stale",f.docs="pending";for(let i of Oe(t,s,c))if(!f.changed.includes(i))f.changed.push(i)}let a=re(s);if((t==="edit"||t==="write"||t==="multiedit")&&ne.test(a)&&!ie.test(a)&&!e){if(!f.cFiles.includes(a))f.cFiles.push(a);f.rte="pending"}if(t==="ecuc_validate"&&!e){let i=/"error":\s*[1-9]/.test(c.split(`
3
+ `)[0]??"")||/^\S+:\d+ error /m.test(c);if(f.validation=i?"errors":"clean",s?.docs&&f.docs==="pending"&&!/DOCUMENT CHECK UNAVAILABLE/.test(c))f.docs="done"}if(t==="autosar_verify_rte"&&!e)f.rte=/\u2014 FAILING/.test(c)?"fail":"pass"}function Oe(n,t,s){let c=[];if(n==="ecuc_edit"){let f=ee(t?.ops);for(let e of f){let a=String(e.path??""),i=e.param?`${a}/${e.param}`:e.container?`${a}/${e.container}`:a;c.push(e.value!==void 0?`${i} = ${JSON.stringify(e.value).slice(0,80)}`:`${i} (${e.op})`)}}else if(n==="arxml_edit"){let f=s.match(/created (\[[^\]]*\])/);if(f)try{c.push(...JSON.parse(f[1]))}catch{}for(let e of ee(t?.ops))if(e.op==="set")c.push(`${e.path}/${e.element} = ${JSON.stringify(e.value)}`)}else if(n==="autosar_add_signal")c.push(`new Com signal ${t?.name} in ${t?.ipdu}`);else if(n==="autosar_add_like"){let f=s.match(/Created:\n((?: .*\n?)+)/);if(f)c.push(...f[1].split(`
4
+ `).map((e)=>e.trim()).filter(Boolean));else c.push(`${t?.name} like ${t?.like}`)}return c.filter(Boolean)}function ee(n){let t=n;for(let s=0;s<3&&typeof t==="string";s++)try{t=JSON.parse(t)}catch{return[]}if(Array.isArray(t))return t.filter((s)=>s&&typeof s==="object");if(t&&typeof t==="object")return[t];return[]}function oe(n){let t=J.get(n??"-");if(!t)return;let s=[];if(t.validation==="stale")s.push("Config/ARXML changed since the last ecuc_validate: run it (unfiltered) now.");if(t.validation==="errors")s.push("The last ecuc_validate reported errors: fix them and validate again.");if(t.rte==="pending")s.push(`C changed (${t.cFiles.slice(-5).join(", ")}): run autosar_verify_rte on it.`);if(t.rte==="fail")s.push("autosar_verify_rte is FAILING: fix the calls (or apply its arxml_edit ops) and verify again.");if(t.docs==="pending"&&t.validation==="clean")s.push(`Changed elements not yet checked against the ingested documents (${t.changed.slice(0,8).join("; ")}${t.changed.length>8?"; \u2026":""}): run ecuc_validate docs=true and answer its checklist item by item.`);if(!s.length)return;return["# AUTOSAR: open verification items (do not report the task as done until each is resolved)",...s.map((c)=>`- ${c}`)].join(`
5
+ `)}var Ce=/^(?:hi+|hello+|hey+|hiya|yo|hola|namaste|good\s+(?:morning|afternoon|evening|day)|thanks?|thank\s+you|thx|ty|ok(?:ay)?|cool|nice|great|bye|good\s*bye|see\s+you)(?:\s+(?:there|all|team|hydron|again|so\s+much|a\s+lot))*$/i,ae=new Map;function Te(n){let t=n.trim().replace(/[\p{Extended_Pictographic}\u200D\uFE0F]/gu,"").replace(/[!.?,:;~\-]+/g," ").replace(/\s+/g," ").trim();return t.length>0&&t.length<=40&&Ce.test(t)}function ce(n,t){ae.set(n??"-",Te(t))}function de(n,t){if(process.env.AUTOSAR_LOOP_GATES==="0")return;if(!ae.get(n??"-"))return;return`NO TOOLS: the user only greeted you (no task). Do not call ${t} or any other tool, and do not read or explore the project. Reply in one short line (e.g. what you can help with here) and stop.`}import{existsSync as xe}from"fs";import{dirname as Ne,join as G}from"path";import{fileURLToPath as je}from"url";var Ie="autosar-data==0.17.0";function De(){let n=Ne(je(import.meta.url)),t=[G(n,"..","python"),G(n,"..","..","python")].find((s)=>xe(G(s,"ecuc_engine","__main__.py")));if(!t)throw Error(`ECUC engine not found next to ${n} (expected ../python/ecuc_engine)`);return t}async function Le(n){try{return await Bun.spawn([...n,"-c","import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}var U={};async function Ue(){if(U.cmd!==void 0)return U.cmd;let n=process.env.AUTOSAR_PYTHON?.trim(),s=n?[[n]]:[["uv","run","--no-project","--quiet","--with",Ie,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let c of s)if(await Le(c))return U.cmd=c,c;return U.cmd=null,null}async function b(n,t,s){let c=await Ue();if(!c)return{ok:!1,error:"no Python >= 3.10 found. Install uv (https://docs.astral.sh/uv/) or Python 3.10+, or set AUTOSAR_PYTHON to an interpreter path."};let f=De(),e=Bun.spawn([...c,"-m","ecuc_engine","--root",n,...t,"--json"],{cwd:n,env:{...process.env,PYTHONPATH:[f,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:s}),[a,i]=await Promise.all([new Response(e.stdout).text(),new Response(e.stderr).text()]);await e.exited;let r=a.trim().split(`
6
+ `).filter(Boolean).at(-1);if(!r)return{ok:!1,error:`engine produced no output (exit ${e.exitCode}): ${i.trim().slice(-2000)}`};try{return JSON.parse(r)}catch(o){return{ok:!1,error:`engine output was not JSON (${String(o)}): ${r.slice(0,500)} ${i.slice(-500)}`}}}var d=S.schema,He=["# AUTOSAR pack: only for AUTOSAR work","If the request is about this AUTOSAR project or its work (ARXML/ECUC config, BSW/RTE/MCAL code, AUTOSAR","specs): load the `autosar` skill and use its tools (ecuc_read/find, ecuc_edit/arxml_edit, ecuc_validate,","autosar_build_context). Otherwise (a greeting, an unrelated question) just answer: no tools, no exploring."].join(`
7
+ `);function T(n){if(typeof n!=="string")return n;let t=n.trim();if(!t.startsWith("[")&&!t.startsWith("{")&&!t.startsWith('"'))return n;try{return T(JSON.parse(t))}catch{return n}}var E=(n)=>d.preprocess(T,n);function C(n){let t=T(n);if(t===void 0||t===null||t==="")return;if(typeof t==="string")return[t];if(Array.isArray(t))return t.map(String).filter((s)=>s.trim());return[String(t)]}var x=()=>d.preprocess(C,d.array(d.string()).optional()),O=" In answers cite, for every element, its AUTOSAR path (the value's @path, e.g. /Pkg/Com/ComConfig/MySig), its"+" DEFINITION-REF (@def, e.g. /AUTOSAR/EcucDefs/Com/ComConfig/ComSignal) and file:line \u2014 never give the"+` DEFINITION-REF when the path is asked. A value's file:line is its "@at:<Param>" (the VALUE element; "@at" is`+' the container); integers come with "@hex:<Param>" \u2014 copy both, never compute a line or a base conversion'+" yourself. Cite a requirement ID only if it appears verbatim in a tool result or spec text read this session.";function N(n,t=200){let s=n.slice(0,t).map((c)=>`${c.file}:${c.line} ${c.severity.toUpperCase()} ${c.rule} ${c.path} \u2014 ${c.message}${c.defRef?` [def ${c.defRef}]`:""}`);if(n.length>t)s.push(`\u2026 ${n.length-t} more`);return s}function k(n){let t=n.didYouMean?`
8
+ Did you mean: ${n.didYouMean.join(", ")}`:"",s=n.schema?`
9
+ Schema errors: ${JSON.stringify(n.schema)}`:"";return`ERROR: ${n.error}${t}${s}`}function A(n){let t=n.warnings??[];return t.length?t.map((s)=>`WARNING: ${s}`).join(`
10
10
  `)+`
11
- `:""}function b(r,n){if(!n)return r;return Ue(n)?n:Pe(r,n)}async function Fe(r){let n=Bun.file(W(r,".hydron","autosar.json"));if(!await n.exists())return{};return n.json().catch((a)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(a)}`)})}async function He(r,n){let a=(await Fe(r))[n];if(typeof a==="string")return{command:a};if(a&&typeof a==="object"&&typeof a.command==="string")return a;return}function ce(r,n=150){let a=r.split(`
12
- `);return a.length>n?[`\u2026 (${a.length-n} earlier lines omitted)`,...a.slice(-n)].join(`
13
- `):r}async function Be(r,n,a,e){let s=process.platform==="win32"?["cmd","/d","/s","/c",r]:["sh","-c",r],o=Date.now(),i=Bun.spawn(s,{cwd:n,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([e,AbortSignal.timeout(a*1000)])}),[t,d]=await Promise.all([new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:await i.exited,out:t,err:d,secs:Math.round((Date.now()-o)/1000)}}var Je=(r)=>(r??[]).map((n)=>`CAN ID ${n.canIdHex??"?"} (${n.canId??"?"}) ${n.direction} via ${n.canIfPdu}, controller ${n.controller??"?"}, network ${(n.network??[]).join(", ")||"?"}`).join("; ");async function de(r,n,a){let e=n.filter((t)=>t?.path&&/\/Com((Group)?Signal|IPdu)$/.test(String(t.definition_ref??""))&&t.ipdu===void 0&&t.direction===void 0);if(!e.length)return[];let s=await y(r,["signal-owners",...new Set(e.map((t)=>t.path))],a);if(!s.ok)return[`NOTE: owning I-PDU not computed locally (${s.error})`];let o=s.owners,i=[];for(let t of e){let d=o[t.path];if(d===null||d===void 0){i.push(`NOTE: ${t.path} is not in the local checkout (${r}); owning I-PDU/direction unknown`);continue}t.ipdu_local=d,i.push(d.length?`${t.path}: owning I-PDU ${d.map((l)=>`${l.ipdu} (ComIPduDirection=${l.direction??"?"}${l.can?.length?`; ${Je(l.can)}`:""})`).join(", ")} [local ECUC]`:`${t.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return i}var We=(r)=>{if(typeof r==="number"&&Number.isSafeInteger(r))return r;if(typeof r==="string"&&/^\s*-?(0x[0-9a-f]+|\d+)\s*$/i.test(r)){let n=r.trim(),a=n.startsWith("-")?-Number(n.slice(1)):Number(n);return Number.isSafeInteger(a)?a:null}return null},he=(r)=>r<0?`-0x${(-r).toString(16).toUpperCase()}`:`0x${r.toString(16).toUpperCase()}`;async function le(r,n,a){let e=n.filter((t)=>typeof t?.path==="string");if(!e.length)return[];let s=await y(r,["value-sites",...new Set(e.map((t)=>t.path))],a),o=s.ok?s.sites:{},i=[];if(!s.ok)i.push(`NOTE: value lines not computed locally (${s.error})`);for(let t of e){let d=o[t.path];if(d){t.values_local=d.values,i.push(`VALUES ${t.path} (container ${d.at}) [local ECUC; cite these lines]:`);for(let[p,u]of Object.entries(d.values))for(let h of Array.isArray(u)?u:[u])i.push(` ${p} = ${JSON.stringify(h.value)}${h.hex?` (dec ${h.dec}, hex ${h.hex})`:""} @ ${h.at}`);continue}let l=t.values&&typeof t.values==="object"?t.values:{},f={};for(let[p,u]of Object.entries(l)){let h=We(u);if(h!==null)f[p]=`dec ${h}, hex ${he(h)}`}if(Object.keys(f).length)t.values_int=f;if(s.ok)i.push(`NOTE: ${t.path} is not in the local checkout (${r}): its file:line${t.file?` ${t.file}:${t.line??"?"}`:""} is where the ELEMENT starts, not a value line. To cite a value's line, read that file; never estimate one.`)}return i}var ue=(r)=>{let n=r.notes??[],a=r.errors??[];return[...n.map((e)=>`NOTE: ${typeof e==="string"?e:JSON.stringify(e)}`),...a.map((e)=>`BACKEND ERROR: ${typeof e==="string"?e:JSON.stringify(e)}`)]};async function Ge(r,n){try{let a=await n(),e=typeof a==="string"?a:a.output;if(e&&e.trim())return a;return`${r}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(a){return`ERROR (${r}): ${a instanceof Error?a.message:String(a)}`}}function Ve(r){for(let[n,a]of Object.entries(r)){let e=a.execute;a.execute=(s,o)=>Ge(n,()=>e(s,o))}return r}function Xe(r,n=process.env){let a=new Set;for(let e of(n.AUTOSAR_DISABLE_TOOLS??"").split(/[\s,]+/))if(e)a.add(e);if(r)try{let e=JSON.parse(Le(W(r,".hydron","autosar.json"),"utf8"));if(Array.isArray(e.disableTools)){for(let s of e.disableTools)if(typeof s==="string"&&s)a.add(s)}}catch{}return a}function qe(r,n){if(n.size===0)return r;return Object.fromEntries(Object.entries(r).filter(([a])=>!n.has(a)))}var fe=new Map;async function J(r,n,a,e){let s=`${n??"-"}|${a??""}|${r}`,o=fe.get(s);if(o)return o;let i;try{let t=await N(a,e),d=await x("/api/v1/mcp/retriever/query",t,{query:r,workspace_id:Number(t)||t},e,300000),l=(d.citations??[]).slice(0,12).map((f,p)=>{let u=f.file?`${f.file}${f.start_line?`:${f.start_line}`:""}`:`${f.doc_name??"?"}${f.title?` > ${f.title}`:""}`;return` [D${p+1}] ${u}: "${(f.excerpt??"").replace(/\s+/g," ").slice(0,280)}"`});i=d.answer?`(project ${t}; retriever answer, ${l.length} citation(s). Trust a statement only where a citation supports it.)
14
- ${d.answer.trim()}
11
+ `:""}function w(n,t){if(!t)return n;return Me(t)?t:Fe(n,t)}async function Be(n){let t=Bun.file(X(n,".hydron","autosar.json"));if(!await t.exists())return{};return t.json().catch((s)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(s)}`)})}async function Je(n,t){let s=(await Be(n))[t];if(typeof s==="string")return{command:s};if(s&&typeof s==="object"&&typeof s.command==="string")return s;return}function le(n,t=150){let s=n.split(`
12
+ `);return s.length>t?[`\u2026 (${s.length-t} earlier lines omitted)`,...s.slice(-t)].join(`
13
+ `):n}async function We(n,t,s,c){let f=process.platform==="win32"?["cmd","/d","/s","/c",n]:["sh","-c",n],e=Date.now(),a=Bun.spawn(f,{cwd:t,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([c,AbortSignal.timeout(s*1000)])}),[i,r]=await Promise.all([new Response(a.stdout).text(),new Response(a.stderr).text()]);return{code:await a.exited,out:i,err:r,secs:Math.round((Date.now()-e)/1000)}}var Ge=(n)=>(n??[]).map((t)=>`CAN ID ${t.canIdHex??"?"} (${t.canId??"?"}) ${t.direction} via ${t.canIfPdu}, controller ${t.controller??"?"}, network ${(t.network??[]).join(", ")||"?"}`).join("; ");async function ue(n,t,s){let c=t.filter((i)=>i?.path&&/\/Com((Group)?Signal|IPdu)$/.test(String(i.definition_ref??""))&&i.ipdu===void 0&&i.direction===void 0);if(!c.length)return[];let f=await b(n,["signal-owners",...new Set(c.map((i)=>i.path))],s);if(!f.ok)return[`NOTE: owning I-PDU not computed locally (${f.error})`];let e=f.owners,a=[];for(let i of c){let r=e[i.path];if(r===null||r===void 0){a.push(`NOTE: ${i.path} is not in the local checkout (${n}); owning I-PDU/direction unknown`);continue}i.ipdu_local=r,a.push(r.length?`${i.path}: owning I-PDU ${r.map((o)=>`${o.ipdu} (ComIPduDirection=${o.direction??"?"}${o.can?.length?`; ${Ge(o.can)}`:""})`).join(", ")} [local ECUC]`:`${i.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return a}var Ve=(n)=>{if(typeof n==="number"&&Number.isSafeInteger(n))return n;if(typeof n==="string"&&/^\s*-?(0x[0-9a-f]+|\d+)\s*$/i.test(n)){let t=n.trim(),s=t.startsWith("-")?-Number(t.slice(1)):Number(t);return Number.isSafeInteger(s)?s:null}return null},ge=(n)=>n<0?`-0x${(-n).toString(16).toUpperCase()}`:`0x${n.toString(16).toUpperCase()}`;async function fe(n,t,s){let c=t.filter((i)=>typeof i?.path==="string");if(!c.length)return[];let f=await b(n,["value-sites",...new Set(c.map((i)=>i.path))],s),e=f.ok?f.sites:{},a=[];if(!f.ok)a.push(`NOTE: value lines not computed locally (${f.error})`);for(let i of c){let r=e[i.path];if(r){i.values_local=r.values,a.push(`VALUES ${i.path} (container ${r.at}) [local ECUC; cite these lines]:`);for(let[l,g]of Object.entries(r.values))for(let h of Array.isArray(g)?g:[g])a.push(` ${l} = ${JSON.stringify(h.value)}${h.hex?` (dec ${h.dec}, hex ${h.hex})`:""} @ ${h.at}`);continue}let o=i.values&&typeof i.values==="object"?i.values:{},u={};for(let[l,g]of Object.entries(o)){let h=Ve(g);if(h!==null)u[l]=`dec ${h}, hex ${ge(h)}`}if(Object.keys(u).length)i.values_int=u;if(f.ok)a.push(`NOTE: ${i.path} is not in the local checkout (${n}): its file:line${i.file?` ${i.file}:${i.line??"?"}`:""} is where the ELEMENT starts, not a value line. To cite a value's line, read that file; never estimate one.`)}return a}var he=(n)=>{let t=n.notes??[],s=n.errors??[];return[...t.map((c)=>`NOTE: ${typeof c==="string"?c:JSON.stringify(c)}`),...s.map((c)=>`BACKEND ERROR: ${typeof c==="string"?c:JSON.stringify(c)}`)]};async function Xe(n,t){try{let s=await t(),c=typeof s==="string"?s:s.output;if(c&&c.trim())return s;return`${n}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(s){return`ERROR (${n}): ${s instanceof Error?s.message:String(s)}`}}function qe(n){for(let[t,s]of Object.entries(n)){let c=s.execute;s.execute=(f,e)=>Xe(t,()=>c(f,e))}return n}function Ye(n,t=process.env){let s=new Set;for(let c of(t.AUTOSAR_DISABLE_TOOLS??"").split(/[\s,]+/))if(c)s.add(c);if(n)try{let c=JSON.parse(Pe(X(n,".hydron","autosar.json"),"utf8"));if(Array.isArray(c.disableTools)){for(let f of c.disableTools)if(typeof f==="string"&&f)s.add(f)}}catch{}return s}function Ke(n,t){if(t.size===0)return n;return Object.fromEntries(Object.entries(n).filter(([s])=>!t.has(s)))}function ze(n,t){if(!t)return;if(n==="autosar_add_like")return typeof t.like==="string"?t.like:void 0;if(n==="autosar_add_signal")return typeof t.ipdu==="string"?t.ipdu:void 0;let s=T(t.ops);if(!Array.isArray(s))s=s&&typeof s==="object"?[s]:[];for(let c of s){let f=c?.path??c?.parent_path;if(typeof f==="string"&&f.trim())return f.trim()}return}var pe=new Map;async function V(n,t,s,c){let f=`${t??"-"}|${s??""}|${n}`,e=pe.get(f);if(e)return e;let a;try{let i=await I(s,c),r=await D("/api/v1/mcp/retriever/query",i,{query:n,workspace_id:Number(i)||i},c,300000),o=(r.citations??[]).slice(0,12).map((u,l)=>{let g=u.file?`${u.file}${u.start_line?`:${u.start_line}`:""}`:`${u.doc_name??"?"}${u.title?` > ${u.title}`:""}`;return` [D${l+1}] ${g}: "${(u.excerpt??"").replace(/\s+/g," ").slice(0,280)}"`});a=r.answer?`(project ${i}; retriever answer, ${o.length} citation(s). Trust a statement only where a citation supports it.)
14
+ ${r.answer.trim()}
15
15
  Citations:
16
- ${l.join(`
17
- `)||" (none)"}`:`(project ${t}: no answer${d.error_message?`: ${d.error_message}`:""})`}catch(t){return`UNAVAILABLE: ${t instanceof Error?t.message:String(t)} -- select a Hydron project with the documents ingested (hydron projects select) or pass workspace_id.`}return fe.set(s,i),i}var Ye=async(r)=>{let n=Xe(r?.directory),a=(e)=>w({description:e==="generate"?"Run the project's AUTOSAR code generator (e.g. the vendor's configurator CLI) as configured in .hydron/autosar.json `generate` ({command, cwd?, timeoutSec?} or a string; `{module}` and `{args}` placeholders are substituted). Run it only after ecuc_validate reports no errors. Asks the user for permission. Returns the exit code and the output tail; follow with arxml_diff/autosar_build.":"Run the project's build command as configured in .hydron/autosar.json `build` ({command, cwd?, timeoutSec?} or a string; `{module}`/`{args}` substituted). Asks the user for permission. Returns the exit code and the output tail (compiler/linker errors). Use after autosar_generate.",args:{module:c.string().optional().describe("module name substituted for {module} in the command, if used"),args:c.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(s,o){let i=b(o.directory,s.project_dir),t=await He(i,e);if(!t)return`No \`${e}\` command configured. Add it to ${W(i,".hydron","autosar.json")}, e.g.
18
- {"${e}": {"command": "${e==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let d=s.args??"",l=t.command.replaceAll("{module}",s.module??""),f=l.includes("{args}")?l.replaceAll("{args}",d):`${l}${d?` ${d}`:""}`,p=b(i,t.cwd);await o.ask({permission:`autosar_${e}`,patterns:[f],always:[t.command],metadata:{command:f,cwd:p}}),o.metadata({title:`${e}: ${f}`});let u=await Be(f,p,t.timeoutSec??3600,o.abort),h=u.code===0?"OK":`FAILED (exit ${u.code})`;return{title:`autosar_${e} ${h}`,output:[`$ ${f} (cwd ${p}, ${u.secs}s) -> ${h}`,u.out.trim()?`--- stdout (tail) ---
19
- ${ce(u.out)}`:"",u.err.trim()?`--- stderr (tail) ---
20
- ${ce(u.err,80)}`:""].filter(Boolean).join(`
21
- `),metadata:{exit:u.code}}}});return{"chat.message":async(e,s)=>{let o=(s?.parts??[]).map((i)=>i.type==="text"?String(i.text??""):"").join(" ");oe(e.sessionID,o)},"tool.execute.before":async(e,s)=>{let o=ae(e.sessionID,e.tool);if(o)throw Error(o);let i=Y(e.tool,s.args)??K(e.tool,s.args);if(i)throw Error(i);let t=ne(e.sessionID,e.tool,s.args);if(t)throw Error(t)},"tool.execute.after":async(e,s)=>{ie(e.sessionID,e.tool,e.args,String(s?.output??""))},"experimental.chat.system.transform":async(e,s)=>{s.system.push(Me);let o=re(e?.sessionID);if(o)s.system.push(o)},tool:qe(Ve({ecuc_read:w({description:`THE way to answer any question about configured ECUC values (use it instead of grep/read on .arxml). Read AUTOSAR ECUC configuration as compact ECUC JSON (keyed by def names: "<DefName>:<ShortName>" for containers, params by def short-name, references as "->/path"; every container carries @path = its AUTOSAR path, @def = its DEFINITION-REF and @at = its file:line; every value is followed by "@at:<Param>" = file:line of its VALUE element, and integers by "@hex:<Param>" ("@dec:<Param>" when the ARXML text is hex)). No path: lists modules and the param-def/config files found. path may be a full AUTOSAR path, a SHORT-NAME ('DoorLatch') or a partial path ('ComConfig/DoorLatch', 'DoorLatch/ComBitPosition'); ambiguous names return candidate paths. A module/container: its values to `+"`depth` levels (merged across split files). A param path (\u2026/Container/Param) or a def path "+"(/AUTOSAR/EcucDefs/\u2026): the value and/or the definition. schema=true adds the def summary (allowed "+"params with type/range/literals/multiplicity, allowed sub-containers) \u2014 read it before ecuc_edit. "+"Never read or edit ARXML by hand."+R,args:{path:c.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:c.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:c.boolean().optional().describe("include the param-def summary for the element"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=["read"];if(e.path)o.push(e.path);if(o.push("--depth",String(e.depth??2)),e.schema)o.push("--schema");let i=await y(b(s.directory,e.project_dir),o,s.abort);if(!i.ok)return S(i)+_(i);let{ok:t,engine:d,ms:l,warnings:f,...p}=i;return S(i)+JSON.stringify(p,null,1)}}),ecuc_edit:w({description:"Edit ECUC ARXML in place \u2014 the ONLY way to change configuration (edit/write/sed on .arxml are blocked). "+"Get exact paths and allowed params from ecuc_read (schema=true) first. Ops by AUTOSAR path: {op:'set', path, param, value} (value scalar, '->/ref/path', or a list for multi-valued "+"params), {op:'add', path:<parent>, container:'DefName:ShortName', values:{\u2026ECUC JSON\u2026}}, {op:'add', "+"path, param, value} (append a value), {op:'remove', path[, param[, value]]}. The engine picks the value element type, DEFINITION-REF/DEST and VALUE-REF DEST from the param defs, rejects out-of-range/non-literal values, patches only the changed bytes in the file each element lives in (UUIDs, order, comments preserved), applies all ops or none, and checks ARXML schema validity. Returns the unified diff and the validation of touched files. ALWAYS run ecuc_validate after editing and fix what it reports."+R,args:{ops:A(c.array(A(c.object({op:c.enum(["set","add","remove"]),path:c.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:c.string().optional().describe("parameter/reference def short-name"),value:c.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:c.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:A(c.record(c.string(),c.any()).optional()).describe("for add: ECUC JSON of the new container")}))).min(1)).describe("list of ops (a JSON array; a JSON-encoded string is also accepted)"),dry_run:c.boolean().optional().describe("compute and show the diff without writing"),force:c.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)o.push("--dry-run");if(e.force)o.push("--force");let i=await y(b(s.directory,e.project_dir),o,s.abort);if(!i.ok){let f=i.changes??[];return S(i)+_(i)+(f.length?`
16
+ ${o.join(`
17
+ `)||" (none)"}`:`(project ${i}: no answer${r.error_message?`: ${r.error_message}`:""})`}catch(i){return`UNAVAILABLE: ${i instanceof Error?i.message:String(i)} -- select a Hydron project with the documents ingested (hydron projects select) or pass workspace_id.`}return pe.set(f,a),a}var Ze=async(n)=>{let t=Ye(n?.directory),s=(e)=>S({description:e==="generate"?"Run the project's AUTOSAR code generator (e.g. the vendor's configurator CLI) as configured in .hydron/autosar.json `generate` ({command, cwd?, timeoutSec?} or a string; `{module}` and `{args}` placeholders are substituted). Run it only after ecuc_validate reports no errors. Asks the user for permission. Returns the exit code and the output tail; follow with arxml_diff/autosar_build.":"Run the project's build command as configured in .hydron/autosar.json `build` ({command, cwd?, timeoutSec?} or a string; `{module}`/`{args}` substituted). Asks the user for permission. Returns the exit code and the output tail (compiler/linker errors). Use after autosar_generate.",args:{module:d.string().optional().describe("module name substituted for {module} in the command, if used"),args:d.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(a,i){let r=w(i.directory,a.project_dir),o=await Je(r,e);if(!o)return`No \`${e}\` command configured. Add it to ${X(r,".hydron","autosar.json")}, e.g.
18
+ {"${e}": {"command": "${e==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let u=a.args??"",l=o.command.replaceAll("{module}",a.module??""),g=l.includes("{args}")?l.replaceAll("{args}",u):`${l}${u?` ${u}`:""}`,h=w(r,o.cwd);await i.ask({permission:`autosar_${e}`,patterns:[g],always:[o.command],metadata:{command:g,cwd:h}}),i.metadata({title:`${e}: ${g}`});let p=await We(g,h,o.timeoutSec??3600,i.abort),m=p.code===0?"OK":`FAILED (exit ${p.code})`;return{title:`autosar_${e} ${m}`,output:[`$ ${g} (cwd ${h}, ${p.secs}s) -> ${m}`,p.out.trim()?`--- stdout (tail) ---
19
+ ${le(p.out)}`:"",p.err.trim()?`--- stderr (tail) ---
20
+ ${le(p.err,80)}`:""].filter(Boolean).join(`
21
+ `),metadata:{exit:p.code}}}}),c={},f=n?.directory;return{"chat.message":async(e,a)=>{let i=(a?.parts??[]).map((r)=>r.type==="text"?String(r.text??""):"").join(" ");ce(e.sessionID,i)},"tool.execute.before":async(e,a)=>{let i=de(e.sessionID,e.tool);if(i)throw Error(i);let r=Z(e.tool,a.args)??Q(e.tool,a.args);if(r)throw Error(r);let o=se(e.sessionID,e.tool,a.args);if(o){let u=ze(e.tool,a.args),l="";if(u&&c.autosar_build_context)try{let g=await c.autosar_build_context.execute({target:u,task:`ground the ${e.tool} about to change ${u}`},{directory:f,worktree:f,sessionID:e.sessionID,abort:new AbortController().signal,metadata(){},async ask(){}});if(l=typeof g==="string"?g:String(g?.output??""),l&&!l.startsWith("ERROR"))W(e.sessionID,"autosar_build_context",{target:u},l);else l=""}catch{l=""}throw Error(l?`${o}
22
+
23
+ The context engine was run for you on ${u}; these are the facts to plan from:
24
+ ${l.slice(0,14000)}
25
+
26
+ List the facts your edit relies on (F#/D#), adjust the edit to them, then retry it.`:o)}},"tool.execute.after":async(e,a)=>{W(e.sessionID,e.tool,e.args,String(a?.output??""))},"experimental.chat.system.transform":async(e,a)=>{a.system.push(He);let i=oe(e?.sessionID);if(i)a.system.push(i)},tool:c=Ke(qe({ecuc_read:S({description:`THE way to answer any question about configured ECUC values (use it instead of grep/read on .arxml). Read AUTOSAR ECUC configuration as compact ECUC JSON (keyed by def names: "<DefName>:<ShortName>" for containers, params by def short-name, references as "->/path"; every container carries @path = its AUTOSAR path, @def = its DEFINITION-REF and @at = its file:line; every value is followed by "@at:<Param>" = file:line of its VALUE element, and integers by "@hex:<Param>" ("@dec:<Param>" when the ARXML text is hex)). No path: lists modules and the param-def/config files found. path may be a full AUTOSAR path, a SHORT-NAME ('DoorLatch') or a partial path ('ComConfig/DoorLatch', 'DoorLatch/ComBitPosition'); ambiguous names return candidate paths. A module/container: its values to `+"`depth` levels (merged across split files). A param path (\u2026/Container/Param) or a def path "+"(/AUTOSAR/EcucDefs/\u2026): the value and/or the definition. schema=true adds the def summary (allowed "+"params with type/range/literals/multiplicity, allowed sub-containers) \u2014 read it before ecuc_edit. "+"Never read or edit ARXML by hand."+O,args:{path:d.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:d.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:d.boolean().optional().describe("include the param-def summary for the element"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=["read"];if(e.path)i.push(e.path);if(i.push("--depth",String(e.depth??2)),e.schema)i.push("--schema");let r=await b(w(a.directory,e.project_dir),i,a.abort);if(!r.ok)return A(r)+k(r);let{ok:o,engine:u,ms:l,warnings:g,...h}=r;return A(r)+JSON.stringify(h,null,1)}}),ecuc_edit:S({description:"Edit ECUC ARXML in place \u2014 the ONLY way to change configuration (edit/write/sed on .arxml are blocked). "+"Get exact paths and allowed params from ecuc_read (schema=true) first. Ops by AUTOSAR path: {op:'set', path, param, value} (value scalar, '->/ref/path', or a list for multi-valued "+"params), {op:'add', path:<parent>, container:'DefName:ShortName', values:{\u2026ECUC JSON\u2026}}, {op:'add', "+"path, param, value} (append a value), {op:'remove', path[, param[, value]]}. The engine picks the value element type, DEFINITION-REF/DEST and VALUE-REF DEST from the param defs, rejects out-of-range/non-literal values, patches only the changed bytes in the file each element lives in (UUIDs, order, comments preserved), applies all ops or none, and checks ARXML schema validity. Returns the unified diff and the validation of touched files. ALWAYS run ecuc_validate after editing and fix what it reports."+O,args:{ops:E(d.array(E(d.object({op:d.enum(["set","add","remove"]),path:d.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:d.string().optional().describe("parameter/reference def short-name"),value:d.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:d.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:E(d.record(d.string(),d.any()).optional()).describe("for add: ECUC JSON of the new container")}))).min(1)).describe("list of ops (a JSON array; a JSON-encoded string is also accepted)"),dry_run:d.boolean().optional().describe("compute and show the diff without writing"),force:d.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)i.push("--dry-run");if(e.force)i.push("--force");let r=await b(w(a.directory,e.project_dir),i,a.abort);if(!r.ok){let g=r.changes??[];return A(r)+k(r)+(g.length?`
22
27
  Rejected diff:
23
- ${f.map((p)=>p.diff).join(`
24
- `)}`:"")}let t=i.changes??[],d=i.validation,l=[`${i.written?"Written":"Dry run (nothing written)"}: ${i.applied} op(s), ${t.length} file(s) changed.`,...(i.notes??[]).map((f)=>`NOTE: ${f}`),...t.map((f)=>f.diff)];if(d)l.push(d.validated?`Validation of touched files: ${JSON.stringify(d.summary)}`:"Validation skipped: no param defs loaded.",...j(d.diagnostics,50));return{title:`ecuc_edit ${t.map((f)=>f.file).join(", ")}`,output:S(i)+l.join(`
25
- `)}}}),ecuc_validate:w({description:"Validate ALL ARXML of the project. ECUC configuration against the param defs (project .hydron/autosar.json paramDefs, auto-discovered ECUC-MODULE-DEF files, or $AUTOSAR_PARAMDEFS): multiplicity, mandatory params/containers, integer/float ranges, enum literals, booleans, string length/regex, reference targets and destination types, dangling refs, duplicate paths, unknown definitions, plus semantic rule packs (H2L-*: Com signals inside their I-PDU and non-overlapping incl. Motorola bit order, unique handle IDs) that run even without defs. System/ECU extract, SWC and interface files: strict AUTOSAR schema load and every *-REF resolves with the right DEST (H2L-ARXML-*). Returns LSP-like diagnostics "+"file:line severity rule path \u2014 message [def DEFINITION-REF]. Run after EVERY ecuc_edit / arxml_edit and before "+"autosar_generate. `files` takes files, directories or globs; omit it to validate everything (preferred). The output always lists which files/modules were loaded; a filter that matches nothing is an ERROR. If it reports nothing on a project you expect to have issues, rerun unfiltered and check the loaded-file list."+R,args:{files:O().describe("only report diagnostics in these: files, directories (recursive, trailing slash optional) or globs (`**/Com*.arxml`), relative to the project root or absolute. A filter that matches no loaded file is reported as an ERROR (H2L-FILTER-NOMATCH), never as clean"),module:c.string().optional().describe("only validate this module (e.g. Com); an unknown module is an ERROR, not clean"),docs:c.boolean().optional().describe("validation step 2: after the deterministic checks, check the elements changed in this session (or `about`) against the ingested specs/manuals of the selected Hydron project; returns a checklist to answer item by item"),about:c.string().optional().describe("with docs=true: the elements/values to check when nothing was changed in this session"),workspace_id:c.union([c.string(),c.number()]).optional().describe("Hydron project for docs=true (default: the selected one)"),max:c.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),checkGenerated:c.boolean().optional().describe("regenerate in a scratch copy and report stale generated files (H2L-GEN-001). Default: on when a generate command is configured and the configuration is < 5 MB"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=["validate"];for(let g of E(e.files)??[])i.push("--file",g);if(e.module)i.push("--module",e.module);if(e.checkGenerated===!0)i.push("--check-generated");if(e.checkGenerated===!1)i.push("--no-check-generated");let t=await y(o,i,s.abort);if(!t.ok)return S(t)+_(t);let{diagnostics:d,scope:l}=t,f=d.filter((g)=>g.rule!=="H2L-DISCOVERY"),u=l.configFiles===0&&t.scope.inputFiles?`Checked ${t.scope.inputFiles} extract/SWC file(s) (AUTOSAR schema + references): ${f.length?JSON.stringify(t.summary):"CLEAN, no findings"}`:t.validated?`Validated ${t.modules.join(", ")||"(no modules)"} against ${t.defsLoaded} module def(s): ${f.length?JSON.stringify(t.summary):"no findings"}`:`Param definitions NOT loaded: only the H2L-* semantic rules ran. ${f.length?JSON.stringify(t.summary):"no findings"}`,h=[`Loaded ${l.configFiles} config file(s) and ${t.scope.inputFiles??0} extract/SWC file(s)`+(l.filter?`; files filter ${JSON.stringify(l.filter.files)} matched ${l.filter.matchedCount}`:"")+(l.module?`; module filter ${l.module}`:"")+":",...l.files.slice(0,30).map((g)=>` ${g.file} (${g.modules.join(", ")})`),...l.files.length>30||l.filesTruncated?[` \u2026 ${l.files.length-30+(l.filesTruncated??0)} more`]:[]],m="";if(e.docs){let g=I(s.sessionID).changed,T=e.about?.trim()||g.join("; ");if(!T)m="\n\nDOCUMENT CHECK UNAVAILABLE: nothing was changed in this session; pass `about` with the elements/values to check.";else{let k=await J(`Check these AUTOSAR configuration elements and values against the specifications and manuals in this knowledge base: ${T}. For each one, give every constraint that applies (allowed range or literals, multiplicity, dependencies on other parameters/modules, hardware limits of the target microcontroller) with the document name and section or requirement ID, and say whether the value satisfies it.`,s.sessionID,e.workspace_id,s.abort);m=k.startsWith("UNAVAILABLE")?`
28
+ ${g.map((h)=>h.diff).join(`
29
+ `)}`:"")}let o=r.changes??[],u=r.validation,l=[`${r.written?"Written":"Dry run (nothing written)"}: ${r.applied} op(s), ${o.length} file(s) changed.`,...(r.notes??[]).map((g)=>`NOTE: ${g}`),...o.map((g)=>g.diff)];if(u)l.push(u.validated?`Validation of touched files: ${JSON.stringify(u.summary)}`:"Validation skipped: no param defs loaded.",...N(u.diagnostics,50));return{title:`ecuc_edit ${o.map((g)=>g.file).join(", ")}`,output:A(r)+l.join(`
30
+ `)}}}),ecuc_validate:S({description:"Validate ALL ARXML of the project. ECUC configuration against the param defs (project .hydron/autosar.json paramDefs, auto-discovered ECUC-MODULE-DEF files, or $AUTOSAR_PARAMDEFS): multiplicity, mandatory params/containers, integer/float ranges, enum literals, booleans, string length/regex, reference targets and destination types, dangling refs, duplicate paths, unknown definitions, plus semantic rule packs (H2L-*: Com signals inside their I-PDU and non-overlapping incl. Motorola bit order, unique handle IDs) that run even without defs. System/ECU extract, SWC and interface files: strict AUTOSAR schema load and every *-REF resolves with the right DEST (H2L-ARXML-*). Returns LSP-like diagnostics "+"file:line severity rule path \u2014 message [def DEFINITION-REF]. Run after EVERY ecuc_edit / arxml_edit and before "+"autosar_generate. `files` takes files, directories or globs; omit it to validate everything (preferred). The output always lists which files/modules were loaded; a filter that matches nothing is an ERROR. If it reports nothing on a project you expect to have issues, rerun unfiltered and check the loaded-file list."+O,args:{files:x().describe("only report diagnostics in these: files, directories (recursive, trailing slash optional) or globs (`**/Com*.arxml`), relative to the project root or absolute. A filter that matches no loaded file is reported as an ERROR (H2L-FILTER-NOMATCH), never as clean"),module:d.string().optional().describe("only validate this module (e.g. Com); an unknown module is an ERROR, not clean"),docs:d.boolean().optional().describe("validation step 2: after the deterministic checks, check the elements changed in this session (or `about`) against the ingested specs/manuals of the selected Hydron project; returns a checklist to answer item by item"),about:d.string().optional().describe("with docs=true: the elements/values to check when nothing was changed in this session"),workspace_id:d.union([d.string(),d.number()]).optional().describe("Hydron project for docs=true (default: the selected one)"),max:d.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),checkGenerated:d.boolean().optional().describe("regenerate in a scratch copy and report stale generated files (H2L-GEN-001). Default: on when a generate command is configured and the configuration is < 5 MB"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=["validate"];for(let y of C(e.files)??[])r.push("--file",y);if(e.module)r.push("--module",e.module);if(e.checkGenerated===!0)r.push("--check-generated");if(e.checkGenerated===!1)r.push("--no-check-generated");let o=await b(i,r,a.abort);if(!o.ok)return A(o)+k(o);let{diagnostics:u,scope:l}=o,g=u.filter((y)=>y.rule!=="H2L-DISCOVERY"),p=l.configFiles===0&&o.scope.inputFiles?`Checked ${o.scope.inputFiles} extract/SWC file(s) (AUTOSAR schema + references): ${g.length?JSON.stringify(o.summary):"CLEAN, no findings"}`:o.validated?`Validated ${o.modules.join(", ")||"(no modules)"} against ${o.defsLoaded} module def(s): ${g.length?JSON.stringify(o.summary):"no findings"}`:`Param definitions NOT loaded: only the H2L-* semantic rules ran. ${g.length?JSON.stringify(o.summary):"no findings"}`,m=[`Loaded ${l.configFiles} config file(s) and ${o.scope.inputFiles??0} extract/SWC file(s)`+(l.filter?`; files filter ${JSON.stringify(l.filter.files)} matched ${l.filter.matchedCount}`:"")+(l.module?`; module filter ${l.module}`:"")+":",...l.files.slice(0,30).map((y)=>` ${y.file} (${y.modules.join(", ")})`),...l.files.length>30||l.filesTruncated?[` \u2026 ${l.files.length-30+(l.filesTruncated??0)} more`]:[]],_="";if(e.docs){let y=L(a.sessionID).changed,j=e.about?.trim()||y.join("; ");if(!j)_="\n\nDOCUMENT CHECK UNAVAILABLE: nothing was changed in this session; pass `about` with the elements/values to check.";else{let v=await V(`Check these AUTOSAR configuration elements and values against the specifications and manuals in this knowledge base: ${j}. For each one, give every constraint that applies (allowed range or literals, multiplicity, dependencies on other parameters/modules, hardware limits of the target microcontroller) with the document name and section or requirement ID, and say whether the value satisfies it.`,a.sessionID,e.workspace_id,a.abort);_=v.startsWith("UNAVAILABLE")?`
26
31
 
27
- DOCUMENT CHECK UNAVAILABLE: ${k}`:["","","## VALIDATION 2 \u2014 against the ingested documents (soft check: the deterministic result above is the hard gate)",`Elements: ${T}`,k,"","Answer this checklist before finishing, one line per element: `<element> = <value>: OK | VIOLATED | NOT COVERED \u2014 [D#]`.","VIOLATED \u2192 fix with ecuc_edit/arxml_edit and validate again. NOT COVERED \u2192 say so; never invent a constraint.","Check every requirement ID you cite with autosar_spec_id_check."].join(`
28
- `)}}return{title:`ecuc_validate ${JSON.stringify(t.summary)}`,output:S(t)+[u,...j(d,e.max??200),...h].join(`
29
- `)+m}}}),arxml_diff:w({description:"Semantic ARXML diff by AUTOSAR path between two files or directories (e.g. a git checkout of the old Config/ vs the current one, or Output/ before/after generation inputs). Ignores UUIDs, whitespace, comments, element order and file splits; ECUC values are compared per parameter. Returns added/removed/changed elements with the changed parameter values. Use it to review edits and PRs instead of a textual diff.",args:{a:c.string().describe("old: file, directory (recursive) or glob, relative to the project root or absolute"),b:c.string().describe("new: file, directory (recursive) or glob, relative to the project root or absolute"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=await y(o,["diff",e.a,e.b],s.abort);if(!i.ok)return _(i);let{ok:t,engine:d,ms:l,...f}=i;return JSON.stringify(f,null,1)}}),autosar_trace:w({description:"Trace AUTOSAR configuration to code through the H2Loop code graph of the selected project: query with an AUTOSAR path, a SHORT-NAME, or a C symbol (reverse lookup). Returns the ARXML element (path, module, DEFINITION-REF, file:line, values), the C symbols it configures (generated macros/const tables via the symbolic-name rule or token/value matching, with method + score) and their callers in in-house code. For a ComSignal it also shows the owning I-PDU and its ComIPduDirection (direction lives on the I-PDU). Elements in the local checkout get a VALUES block: each value with the file:line of its VALUE element and integers in dec + hex (the graph's file:line is only where the element starts). Use for impact analysis ('what code changes if I change X?') and 'where does this symbol come from?'. Requires the project's code to be ingested."+R,args:{query:c.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:c.number().int().optional().describe("restrict to one code data source"),limit:c.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:c.union([c.string(),c.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:c.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,s){let o=await N(e.workspace_id,s.abort),i={query:e.query};if(e.data_source_id!==void 0)i.data_source_id=e.data_source_id;if(e.limit!==void 0)i.limit=e.limit;let t=await x("/api/v1/mcp/autosar/trace",o,i,s.abort),d=t.matches??[],l=b(s.directory,e.project_dir),f=d.map((m)=>m.arxml).filter((m)=>!!m),[p,u]=await Promise.all([de(l,f,s.abort),le(l,f,s.abort)]),h=[`${d.length} match(es) for "${e.query}" in workspace ${o}`+(t.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(t.data_sources_queried)})`:""),...ue(t),...p,...u,JSON.stringify(d,null,1)];if(!d.length)h.push("No match: try a SHORT-NAME, a full AUTOSAR path from ecuc_read, or the C symbol.");return{title:`autosar_trace ${e.query}`,output:h.join(`
30
- `)}}}),autosar_config_lookup:w({description:"Look up one ARXML element in the H2Loop code graph by AUTOSAR path: the element (module, DEFINITION-REF, file:line, values), its direct children, references out and in (who points at it), and linked C symbols. Elements in the local checkout get a VALUES block with each value's VALUE-element file:line and integers in dec + hex. Use it to navigate config relations across the ingested repo (including files not in the local checkout); for local edits use ecuc_read/ecuc_edit."+R,args:{path:c.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:c.number().int().optional().describe("restrict to one code data source"),workspace_id:c.union([c.string(),c.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:c.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,s){let o=await N(e.workspace_id,s.abort),i={path:e.path};if(e.data_source_id!==void 0)i.data_source_id=e.data_source_id;let t=await x("/api/v1/mcp/autosar/config-lookup",o,i,s.abort),d=t.matches??[],l=b(s.directory,e.project_dir),f=d.map((h)=>h.element).filter((h)=>!!h),[p,u]=await Promise.all([de(l,f,s.abort),le(l,f,s.abort)]);return{title:`autosar_config_lookup ${e.path}`,output:[`${d.length} match(es) for ${e.path} in workspace ${o}`,...ue(t),...p,...u,JSON.stringify(d,null,1)].join(`
31
- `)}}}),ecuc_find:w({description:"Structured query over the ECUC configuration \u2014 use it instead of grep/read on ARXML. Filter containers by "+`module, container_def (def short-name, e.g. PduRRoutingPath, ComSignal, NvMBlockDescriptor), where ("ComIPduDirection=SEND, ComBitSize>8"; =, !=, >, <, >=, <=, ~regex), ref_to (containers that reference this path directly or through up to 2 references, e.g. all routing paths whose destination is an EcuC Pdu), and referenced_by (e.g. all ComSignals referenced by I-PDU Y). Returns AUTOSAR paths, the container's file:line (at) and its values, each with "@at:<Param>" (file:line of the VALUE element) and, for integers, "@hex:<Param>".`+R,args:{module:c.string().optional(),container_def:c.string().optional().describe("container definition short-name"),where:c.union([c.string(),c.record(c.string(),c.any())]).optional().describe("conditions on parameter values"),ref_to:c.string().optional().describe("AUTOSAR path / SHORT-NAME that the containers must reference"),referenced_by:c.string().optional().describe("container whose references define the result set"),limit:c.number().int().min(1).max(500).optional(),project_dir:c.string().optional()},async execute(e,s){let o=["find"];if(e.module)o.push("--module",e.module);if(e.container_def)o.push("--container-def",e.container_def);if(e.where)o.push("--where",typeof e.where==="string"?e.where:JSON.stringify(e.where));if(e.ref_to)o.push("--ref-to",e.ref_to);if(e.referenced_by)o.push("--referenced-by",e.referenced_by);o.push("--limit",String(e.limit??50));let i=await y(b(s.directory,e.project_dir),o,s.abort);if(!i.ok)return S(i)+_(i);let{ok:t,engine:d,ms:l,warnings:f,...p}=i;return S(i)+JSON.stringify(p,null,1)}}),autosar_spec_id_check:w({description:"Check AUTOSAR requirement IDs (SWS_/SRS_/TPS_/ECUC_/RS_ \u2026, e.g. SWS_Com_00197, 'SWS_NvM_00898/00899') "+"against the project's own AUTOSAR spec texts (.hydron/autosar.json `specDocs` globs of .txt/.md exports, or $AUTOSAR_SPEC_DOCS). Returns per ID: DEFINED (with document, line and the requirement text), MENTIONED-ONLY (occurs only in trace tables / running text), NOT-FOUND or MALFORMED. Run it BEFORE citing any ID that did not come verbatim from a tool result; never cite an ID it does not report DEFINED, and describe the rule with the returned text, not from memory.",args:{ids:O().describe("requirement IDs to check (a list, a JSON-encoded list, or one ID)"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=E(e.ids)??[];if(!o.length)return'ERROR: pass ids, e.g. ["SWS_Com_00197"]';let i=await y(b(s.directory,e.project_dir),["spec-ids",...o],s.abort);if(!i.ok)return _(i);let t=i.results;return{title:`autosar_spec_id_check ${JSON.stringify(i.counts)}`,output:[`Checked ${t.length} ID(s) against ${i.docs} spec text file(s) (${i.docsOrigin}; ${i.indexedIds} IDs indexed): ${JSON.stringify(i.counts)}`,...t.map((d)=>`${d.id} ${d.status}`+(d.doc?` \u2014 ${d.doc}:${d.line}`:"")+(d.didYouMean?` (did you mean ${d.didYouMean}?)`:"")+(d.text?`
32
- ${d.text}`:"")),t.some((d)=>d.status!=="DEFINED")?"Do not cite IDs that are not DEFINED; state the rule without an ID instead.":""].filter(Boolean).join(`
33
- `)}}}),ecuc_crosscheck:w({description:"Per-module cross-check table: runs the semantic rule packs for one module (Com, PduR, EcuC, NvM, Fee, Ea, "+"CanIf, Dem, ...) \u2014 config vs System/ECU extract (H2L-SYS), gateway routing vs extract (H2L-PDUR), NvM/Fee "+"length vs the C RAM object's sizeof (H2L-NVM), memory capacity (H2L-MEM), signal packing and handle IDs \u2014 "+"and returns, per element, the findings, plus notes on what could NOT be checked. Run it for every module present after ecuc_validate; a clean validate alone is not proof of a correct configuration."+R,args:{module:c.string().describe("module name, e.g. PduR"),project_dir:c.string().optional()},async execute(e,s){let o=await y(b(s.directory,e.project_dir),["crosscheck",e.module],s.abort);if(!o.ok)return S(o)+_(o);let i=o.rows,t=o.otherFindings??[];return{title:`ecuc_crosscheck ${e.module}: ${o.summary}`,output:[`${e.module}: ${o.summary} (element kinds: ${o.elementKinds.join(", ")||"none"})`,...i.map((d)=>`${d.element} (${d.at})
34
- ${d.findings.join(`
35
- `)}`),...j(t,50),...(o.notes??[]).map((d)=>`NOTE: ${d}`)].join(`
36
- `)}}}),arxml_edit:w({description:"Structured edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces), including NEW files \u2014 never write "+"that XML by hand. Ops: {op:'new_file', file:'Input/SWC_X.arxml', packages:['/Swcs','/Interfaces']} (empty R25-11 file; later ops in the same call can fill it), {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested} | [list]}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Missing packages along parent_path are created (top level needs `file` when the project has several ARXML files). Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added earlier in the same call), elements are placed and ordered per the AUTOSAR schema, references are re-checked and the file is strictly re-loaded; nothing is written if anything fails. For ECUC values use ecuc_edit; to add a whole signal use autosar_add_signal.",args:{ops:A(c.array(A(c.record(c.string(),c.any()))).min(1)).describe("list of ops"),dry_run:c.boolean().optional(),project_dir:c.string().optional()},async execute(e,s){let o=["arxml-edit","--ops",JSON.stringify(C(e.ops))];if(e.dry_run)o.push("--dry-run");let i=await y(b(s.directory,e.project_dir),o,s.abort),t=i.changes??[];if(!i.ok)return _(i)+(i.problems?.length?`
37
- ${i.problems.join(`
38
- `)}`:"")+(t.length?`
32
+ DOCUMENT CHECK UNAVAILABLE: ${v}`:["","","## VALIDATION 2 \u2014 against the ingested documents (soft check: the deterministic result above is the hard gate)",`Elements: ${j}`,v,"","Answer this checklist before finishing, one line per element: `<element> = <value>: OK | VIOLATED | NOT COVERED \u2014 [D#]`.","VIOLATED \u2192 fix with ecuc_edit/arxml_edit and validate again. NOT COVERED \u2192 say so; never invent a constraint.","Check every requirement ID you cite with autosar_spec_id_check."].join(`
33
+ `)}}return{title:`ecuc_validate ${JSON.stringify(o.summary)}`,output:A(o)+[p,...N(u,e.max??200),...m].join(`
34
+ `)+_}}}),arxml_diff:S({description:"Semantic ARXML diff by AUTOSAR path between two files or directories (e.g. a git checkout of the old Config/ vs the current one, or Output/ before/after generation inputs). Ignores UUIDs, whitespace, comments, element order and file splits; ECUC values are compared per parameter. Returns added/removed/changed elements with the changed parameter values. Use it to review edits and PRs instead of a textual diff.",args:{a:d.string().describe("old: file, directory (recursive) or glob, relative to the project root or absolute"),b:d.string().describe("new: file, directory (recursive) or glob, relative to the project root or absolute"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=await b(i,["diff",e.a,e.b],a.abort);if(!r.ok)return k(r);let{ok:o,engine:u,ms:l,...g}=r;return JSON.stringify(g,null,1)}}),autosar_trace:S({description:"Trace AUTOSAR configuration to code through the H2Loop code graph of the selected project: query with an AUTOSAR path, a SHORT-NAME, or a C symbol (reverse lookup). Returns the ARXML element (path, module, DEFINITION-REF, file:line, values), the C symbols it configures (generated macros/const tables via the symbolic-name rule or token/value matching, with method + score) and their callers in in-house code. For a ComSignal it also shows the owning I-PDU and its ComIPduDirection (direction lives on the I-PDU). Elements in the local checkout get a VALUES block: each value with the file:line of its VALUE element and integers in dec + hex (the graph's file:line is only where the element starts). Use for impact analysis ('what code changes if I change X?') and 'where does this symbol come from?'. Requires the project's code to be ingested."+O,args:{query:d.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:d.number().int().optional().describe("restrict to one code data source"),limit:d.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:d.union([d.string(),d.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:d.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,a){let i=await I(e.workspace_id,a.abort),r={query:e.query};if(e.data_source_id!==void 0)r.data_source_id=e.data_source_id;if(e.limit!==void 0)r.limit=e.limit;let o=await D("/api/v1/mcp/autosar/trace",i,r,a.abort),u=o.matches??[],l=w(a.directory,e.project_dir),g=u.map((_)=>_.arxml).filter((_)=>!!_),[h,p]=await Promise.all([ue(l,g,a.abort),fe(l,g,a.abort)]),m=[`${u.length} match(es) for "${e.query}" in workspace ${i}`+(o.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(o.data_sources_queried)})`:""),...he(o),...h,...p,JSON.stringify(u,null,1)];if(!u.length)m.push("No match: try a SHORT-NAME, a full AUTOSAR path from ecuc_read, or the C symbol.");return{title:`autosar_trace ${e.query}`,output:m.join(`
35
+ `)}}}),autosar_config_lookup:S({description:"Look up one ARXML element in the H2Loop code graph by AUTOSAR path: the element (module, DEFINITION-REF, file:line, values), its direct children, references out and in (who points at it), and linked C symbols. Elements in the local checkout get a VALUES block with each value's VALUE-element file:line and integers in dec + hex. Use it to navigate config relations across the ingested repo (including files not in the local checkout); for local edits use ecuc_read/ecuc_edit."+O,args:{path:d.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:d.number().int().optional().describe("restrict to one code data source"),workspace_id:d.union([d.string(),d.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:d.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,a){let i=await I(e.workspace_id,a.abort),r={path:e.path};if(e.data_source_id!==void 0)r.data_source_id=e.data_source_id;let o=await D("/api/v1/mcp/autosar/config-lookup",i,r,a.abort),u=o.matches??[],l=w(a.directory,e.project_dir),g=u.map((m)=>m.element).filter((m)=>!!m),[h,p]=await Promise.all([ue(l,g,a.abort),fe(l,g,a.abort)]);return{title:`autosar_config_lookup ${e.path}`,output:[`${u.length} match(es) for ${e.path} in workspace ${i}`,...he(o),...h,...p,JSON.stringify(u,null,1)].join(`
36
+ `)}}}),ecuc_find:S({description:"Structured query over the ECUC configuration \u2014 use it instead of grep/read on ARXML. Filter containers by "+`module, container_def (def short-name, e.g. PduRRoutingPath, ComSignal, NvMBlockDescriptor), where ("ComIPduDirection=SEND, ComBitSize>8"; =, !=, >, <, >=, <=, ~regex), ref_to (containers that reference this path directly or through up to 2 references, e.g. all routing paths whose destination is an EcuC Pdu), and referenced_by (e.g. all ComSignals referenced by I-PDU Y). Returns AUTOSAR paths, the container's file:line (at) and its values, each with "@at:<Param>" (file:line of the VALUE element) and, for integers, "@hex:<Param>".`+O,args:{module:d.string().optional(),container_def:d.string().optional().describe("container definition short-name"),where:d.union([d.string(),d.record(d.string(),d.any())]).optional().describe("conditions on parameter values"),ref_to:d.string().optional().describe("AUTOSAR path / SHORT-NAME that the containers must reference"),referenced_by:d.string().optional().describe("container whose references define the result set"),limit:d.number().int().min(1).max(500).optional(),project_dir:d.string().optional()},async execute(e,a){let i=["find"];if(e.module)i.push("--module",e.module);if(e.container_def)i.push("--container-def",e.container_def);if(e.where)i.push("--where",typeof e.where==="string"?e.where:JSON.stringify(e.where));if(e.ref_to)i.push("--ref-to",e.ref_to);if(e.referenced_by)i.push("--referenced-by",e.referenced_by);i.push("--limit",String(e.limit??50));let r=await b(w(a.directory,e.project_dir),i,a.abort);if(!r.ok)return A(r)+k(r);let{ok:o,engine:u,ms:l,warnings:g,...h}=r;return A(r)+JSON.stringify(h,null,1)}}),autosar_spec_id_check:S({description:"Check AUTOSAR requirement IDs (SWS_/SRS_/TPS_/ECUC_/RS_ \u2026, e.g. SWS_Com_00197, 'SWS_NvM_00898/00899') "+"against the project's own AUTOSAR spec texts (.hydron/autosar.json `specDocs` globs of .txt/.md exports, or $AUTOSAR_SPEC_DOCS). Returns per ID: DEFINED (with document, line and the requirement text), MENTIONED-ONLY (occurs only in trace tables / running text), NOT-FOUND or MALFORMED. Run it BEFORE citing any ID that did not come verbatim from a tool result; never cite an ID it does not report DEFINED, and describe the rule with the returned text, not from memory.",args:{ids:x().describe("requirement IDs to check (a list, a JSON-encoded list, or one ID)"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=C(e.ids)??[];if(!i.length)return'ERROR: pass ids, e.g. ["SWS_Com_00197"]';let r=await b(w(a.directory,e.project_dir),["spec-ids",...i],a.abort);if(!r.ok)return k(r);let o=r.results;return{title:`autosar_spec_id_check ${JSON.stringify(r.counts)}`,output:[`Checked ${o.length} ID(s) against ${r.docs} spec text file(s) (${r.docsOrigin}; ${r.indexedIds} IDs indexed): ${JSON.stringify(r.counts)}`,...o.map((u)=>`${u.id} ${u.status}`+(u.doc?` \u2014 ${u.doc}:${u.line}`:"")+(u.didYouMean?` (did you mean ${u.didYouMean}?)`:"")+(u.text?`
37
+ ${u.text}`:"")),o.some((u)=>u.status!=="DEFINED")?"Do not cite IDs that are not DEFINED; state the rule without an ID instead.":""].filter(Boolean).join(`
38
+ `)}}}),ecuc_crosscheck:S({description:"Per-module cross-check table: runs the semantic rule packs for one module (Com, PduR, EcuC, NvM, Fee, Ea, "+"CanIf, Dem, ...) \u2014 config vs System/ECU extract (H2L-SYS), gateway routing vs extract (H2L-PDUR), NvM/Fee "+"length vs the C RAM object's sizeof (H2L-NVM), memory capacity (H2L-MEM), signal packing and handle IDs \u2014 "+"and returns, per element, the findings, plus notes on what could NOT be checked. Run it for every module present after ecuc_validate; a clean validate alone is not proof of a correct configuration."+O,args:{module:d.string().describe("module name, e.g. PduR"),project_dir:d.string().optional()},async execute(e,a){let i=await b(w(a.directory,e.project_dir),["crosscheck",e.module],a.abort);if(!i.ok)return A(i)+k(i);let r=i.rows,o=i.otherFindings??[];return{title:`ecuc_crosscheck ${e.module}: ${i.summary}`,output:[`${e.module}: ${i.summary} (element kinds: ${i.elementKinds.join(", ")||"none"})`,...r.map((u)=>`${u.element} (${u.at})
39
+ ${u.findings.join(`
40
+ `)}`),...N(o,50),...(i.notes??[]).map((u)=>`NOTE: ${u}`)].join(`
41
+ `)}}}),arxml_edit:S({description:"Structured edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces), including NEW files \u2014 never write "+"that XML by hand. Ops: {op:'new_file', file:'Input/SWC_X.arxml', packages:['/Swcs','/Interfaces']} (empty R25-11 file; later ops in the same call can fill it), {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested} | [list]}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Missing packages along parent_path are created (top level needs `file` when the project has several ARXML files). Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added earlier in the same call), elements are placed and ordered per the AUTOSAR schema, references are re-checked and the file is strictly re-loaded; nothing is written if anything fails. For ECUC values use ecuc_edit; to add a whole signal use autosar_add_signal.",args:{ops:E(d.array(E(d.record(d.string(),d.any()))).min(1)).describe("list of ops"),dry_run:d.boolean().optional(),project_dir:d.string().optional()},async execute(e,a){let i=["arxml-edit","--ops",JSON.stringify(T(e.ops))];if(e.dry_run)i.push("--dry-run");let r=await b(w(a.directory,e.project_dir),i,a.abort),o=r.changes??[];if(!r.ok)return k(r)+(r.problems?.length?`
42
+ ${r.problems.join(`
43
+ `)}`:"")+(o.length?`
39
44
  Rejected diff:
40
- ${t.map((f)=>f.diff).join(`
41
- `)}`:"");let d=i.notes??[],l=i.createdPackages??[];return`${i.written?"Written":"Dry run"}; `+((i.newFiles??[]).length?`new file(s) ${i.newFiles.join(", ")}; `:"")+`created ${JSON.stringify(i.created)}`+(l.length?`; new packages ${l.join(", ")}`:"")+(d.length?`
42
- NOTE: ${d.join(`
45
+ ${o.map((h)=>h.diff).join(`
46
+ `)}`:"");let u=r.notes??[],l=r.createdPackages??[],g="";if(r.written){let h=await b(w(a.directory,e.project_dir),["validate","--no-check-generated","--max","40"],a.abort);if(h.ok!==!1&&h.diagnostics){let p=h.diagnostics.filter((m)=>m.severity==="error"||m.severity==="warning");g=p.length?`
47
+ AUTO-VALIDATE: ${JSON.stringify(h.summary)} -- FIX these before going on:
48
+ ${N(p,25).join(`
49
+ `)}`:`
50
+ AUTO-VALIDATE: clean (no errors or warnings)`}}return`${r.written?"Written":"Dry run"}; `+((r.newFiles??[]).length?`new file(s) ${r.newFiles.join(", ")}; `:"")+`created ${JSON.stringify(r.created)}`+(l.length?`; new packages ${l.join(", ")}`:"")+(u.length?`
51
+ NOTE: ${u.join(`
43
52
  NOTE: `)}`:"")+`
44
- ${t.map((f)=>f.diff).join(`
45
- `)}
46
- Next: ecuc_validate (checks extract/SWC files too).`}}),autosar_add_like:w({description:"Add a new X LIKE an existing Y, end to end, in one checked step: a new message (frame, I-PDU, triggerings, EcuC Pdu, CanIf PDU, PduR route, Com I-PDU), port, runnable with its event and task mapping, Dem event + DTC, NvM block + Fee block, interface... Projects name a thing's elements after it, so the engine clones every element whose SHORT-NAME contains `like` and that is linked to the others by references, across ECUC and System/SWC ARXML, renamed to `name`; lists that contain the template get the clone too; handle IDs get the lowest free value; the template's own members (a message's signals) are left out unless named in `also`. `set` then sets values on the clones by ECUC parameter name, XML tag, or 'Owner:key'. Schema + references are checked and validation runs before/after: any new finding rolls everything back. PREFER THIS over building a chain op by op with arxml_edit/ecuc_edit. Example: like='BCM_DoorStatus', name='BCM_SettingsAck', also={'DrvDoorOpenSts':'SettingsAckCounter'}, set={'IDENTIFIER':'0x4F8','CanIfTxPduCanId':'0x4F8','PduLength':2,'FRAME-LENGTH':2,'I-SIGNAL-I-PDU:LENGTH':2,'ComTxModeTimePeriod':1.0,'SettingsAckCounter:ComBitSize':8}. Pick as template the existing element most like the new one (same direction/bus/kind); dry_run first.",args:{like:c.string().describe("name stem of the template element, e.g. BCM_DoorStatus (an existing message), DoorLockActuatorFault (a Dem event)"),name:c.string().describe("the new stem, e.g. BCM_SettingsAck"),also:A(c.record(c.string(),c.string())).optional().describe("template members to clone and rename too: {template stem: new stem}"),set:A(c.record(c.string(),c.any())).optional().describe("values on the clones: {ParamDefName | XML-TAG | 'Owner:key': value}"),seed:c.string().optional().describe("one element of the template chain, when several separate chains carry the stem"),keep_members:c.boolean().optional().describe("also copy the template's own members (default false)"),dry_run:c.boolean().optional(),project_dir:c.string().optional()},async execute(e,s){let o=["add-like","--like",e.like,"--name",e.name];if(e.also)o.push("--also",JSON.stringify(C(e.also)));if(e.set)o.push("--set",JSON.stringify(C(e.set)));if(e.seed)o.push("--seed",e.seed);if(e.keep_members)o.push("--keep-members");if(e.dry_run)o.push("--dry-run");let i=await y(b(s.directory,e.project_dir),o,s.abort),t=(l)=>i[l]??[],d=[i.ok?`${i.written?"Written":"Dry run OK"}: ${t("created").length} element(s) like ${e.like} as ${e.name}`:`NOT DONE: ${i.error}`,...t("created").length?["Created:",...t("created").map((l)=>` ${l}`)]:[],...t("set").length?["Set:",...t("set").map((l)=>` ${l}`)]:[],...t("ids").length?["Handle IDs:",...t("ids").map((l)=>` ${l}`)]:[],...t("linked").length?["Linked:",...t("linked").map((l)=>` ${l}`)]:[],...t("notCopied").length?[`Not copied (the template's own members): ${t("notCopied").length}`,...t("notCopied").slice(0,12).map((l)=>` ${l}`)]:[],...t("singleValuedNotLinked").length?["Single-valued references to the template NOT changed (add/point your own if the new element needs one):",...t("singleValuedNotLinked").map((l)=>` ${l}`)]:[],...t("notes").map((l)=>`NOTE: ${l}`),...(i.problems??[]).map((l)=>`PROBLEM: ${l}`),...(i.diagnostics??[]).length?j(i.diagnostics,20):[]];if(i.ok&&!i.written)d.push("Next: run again without dry_run.");if(i.ok&&i.written)d.push(String(i.next??""));return d.join(`
47
- `)}}),autosar_add_signal:w({description:"Add one Com signal end to end, consistently: ComSignal (ComHandleId = lowest unused; existing handles never move) + ComIPduSignalRef in the ECUC, SYSTEM-SIGNAL / I-SIGNAL / I-SIGNAL-TO-I-PDU-MAPPING and the rest of the signal chain (triggering, port, extract references) in the System extract, and the DBC SG_ line when a .dbc carries the frame. Everything is derived by following references from an existing signal of the same I-PDU. It refuses on overlap or out-of-bounds bits, picks the first free byte-aligned position when bit_position is omitted, runs ecuc_validate after writing and rolls back on any new finding. Then run autosar_generate, use ComConf_ComSignal_<name> in the C code, and autosar_build.",args:{name:c.string(),ipdu:c.string().describe("ComIPdu AUTOSAR path or SHORT-NAME"),bit_size:c.number().int().min(1).max(64).optional(),type:c.string().optional().describe("ComSignalType, e.g. UINT8 (default)"),bit_position:c.number().int().min(0).optional().describe("omit for the first free byte-aligned position"),endianness:c.enum(["LITTLE_ENDIAN","BIG_ENDIAN","OPAQUE"]).optional(),init:c.union([c.number(),c.string()]).optional(),transfer_property:c.string().optional().describe("e.g. PENDING, TRIGGERED, TRIGGERED_ON_CHANGE"),dry_run:c.boolean().optional(),project_dir:c.string().optional()},async execute(e,s){let o=["add-signal","--name",e.name,"--ipdu",e.ipdu,"--type",e.type??"UINT8"];if(e.bit_size!==void 0)o.push("--bit-size",String(e.bit_size));if(e.bit_position!==void 0)o.push("--bit-position",String(e.bit_position));if(e.endianness)o.push("--endianness",e.endianness);if(e.init!==void 0)o.push("--init",String(e.init));if(e.transfer_property)o.push("--transfer-property",e.transfer_property);if(e.dry_run)o.push("--dry-run");let i=await y(b(s.directory,e.project_dir),o,s.abort);if(!i.ok)return _(i)+(i.diagnostics?.length?`
48
- ${j(i.diagnostics,20).join(`
49
- `)}`:"");let{ok:t,engine:d,ms:l,...f}=i;return JSON.stringify(f,null,1)}}),autosar_audit:w({description:"Integration review \u2014 run this FIRST when reviewing C code that uses BSW services. Deterministically scans "+"C files (default: .hydron/autosar.json `audit` paths, else every .c in the project except generated files, "+"recognised by banner or *_Cfg/*_PBcfg/Rte* names \u2014 folder names do not matter) for Com_Send/ReceiveSignal[Group], "+"NvM_Read/WriteBlock/RestoreBlockDefaults, Dem_SetEventStatus/ReportErrorStatus, PduR_*Transmit and Rte_Write_/Rte_Read_ (resolved through the generated Output/ RTE headers), resolves every handle through the ECUC (symbolic name or raw number), and reports: H2L-API-001 raw numeric handle, -002 unknown handle, -003 direction mismatch (send on a RECEIVE I-PDU signal or receive on SEND), -004 C object width vs ComSignalType, -005 NvM NULL_PTR without a permanent RAM block (NULL_PTR WITH one is valid, SWS_NvM_00900). Each finding has file:line, evidence (configured values + AUTOSAR paths) and a spec citation; calls it cannot check are listed as unchecked, never guessed. Confirm findings with ecuc_read / autosar_trace.",args:{paths:O().describe("C files, directories or globs to scan (default: .hydron/autosar.json audit paths, else every .c under the project except generator output); matching nothing is an ERROR"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=await y(o,["audit",...E(e.paths)??[]],s.abort);if(!i.ok)return S(i)+_(i);let t=(i.notes??[]).map((h)=>`NOTE: ${h}`),{findings:d,unchecked:l,scanned:f}=i,p=[`Scanned ${f.sources.length} C file(s), ${f.calls} BSW call(s): ${d.length?JSON.stringify(i.summary):"no findings"}`,...d.map((h)=>`${h.file}:${h.line} ${h.severity.toUpperCase()} ${h.rule} ${h.call}
50
- ${h.message}
51
- evidence: ${JSON.stringify(h.evidence)}
52
- spec: ${h.cite}${h.fix?`
53
- fix: ${h.fix}`:""}`)];if(l.length)p.push(`Unchecked (${l.length}):`,...l.slice(0,50).map((h)=>` ${h.file}:${h.line} ${h.call} \u2014 ${h.reason}`));let u=i.unreferenced??{};for(let[h,m]of Object.entries(u))p.push(`INFO configured but never referenced in the scanned code \u2014 ${h}: ${m.count} (e.g. ${m.sample.slice(0,5).join(", ")})`);return p.push(...t),{title:`autosar_audit ${JSON.stringify(i.summary)}`,output:S(i)+p.join(`
54
- `)}}}),autosar_verify_rte:w({description:"Verify Rte_* calls in C against the SW-C description (SWS_RTE naming, R25-11). Each call is mapped to its enclosing function, then to every runnable whose entry point (RUNNABLE-ENTITY SYMBOL) reaches it, and classified: OK (the runnable has the access point), NOT-ALLOWED (the API exists for the SW-C but this runnable has no dataSendPoint/dataReceivePoint/serverCallPoint/... for it), INVENTED (no such port, element, runnable, or wrong direction/semantics). For NOT-ALLOWED/INVENTED it returns ready-to-run arxml_edit ops (dry-run checked) that add the access point / port / data element. Run it after writing runnable code and never finish with a failing verify.",args:{paths:O().describe("C files, directories or globs (default: all in-house C)"),diff_against:c.string().optional().describe("git ref: only calls on lines changed since this ref (plus untracked files)"),swc:c.string().optional().describe("restrict to one SW-C (short name or AUTOSAR path)"),runnable:c.string().optional().describe("restrict to one runnable (short name or SYMBOL)"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=["verify-rte",...E(e.paths)??[]];if(e.diff_against)i.push("--diff-against",e.diff_against);if(e.swc)i.push("--swc",e.swc);if(e.runnable)i.push("--runnable",e.runnable);let t=await y(o,i,s.abort);if(!t.ok)return _(t);let{findings:d,counts:l}=t,f=d.filter((g)=>g.status!=="OK"),p=t.scanned,u=[`Verified ${p.calls} Rte_* call(s) in ${p.files.length} file(s) against SW-C(s) ${t.swcs.join(", ")}: OK ${l.OK}, NOT-ALLOWED ${l["NOT-ALLOWED"]}, INVENTED ${l.INVENTED}`+(f.length?" \u2014 FAILING":" \u2014 PASS")];for(let g of f){if(u.push(`${g.at} ${g.status} ${g.call} in ${g.function}() [${g.swc??"?"}${g.runnable?`/${g.runnable}`:""}]`,` ${g.why}`),g.expected)u.push(` expected: ${g.expected} [${g.cite}]`);if(g.hint)u.push(` hint: ${g.hint}`);if(g.arxml_edit?.length)u.push(` fix (arxml_edit ops${g.opsChecked===!0?", dry-run OK":g.opsChecked?`, dry-run FAILED: ${g.opsChecked}`:""}): ${JSON.stringify(g.arxml_edit)}`)}let h=t.notChecked??[];if(h.length)u.push(`Not checked (${h.length}): ${h.map((g)=>`${g.call} @ ${g.at}`).join(", ")}`);let m=t.runnablesWithoutCode??[];if(m.length)u.push(`Runnables without a C definition: ${m.map((g)=>`${g.swc}.${g.runnable} (${g.symbol})`).join(", ")}`);for(let g of t.notes??[])u.push(`NOTE: ${g}`);if(f.length)u.push("Next: rewrite the call with an allowed API (autosar_build_context lists them), or \u2014 only if the behaviour "+"truly needs new data \u2014 apply the ops with arxml_edit, run autosar_generate, then autosar_verify_rte again.");return{title:`autosar_verify_rte ${f.length?"FAIL":"PASS"} ${JSON.stringify(l)}`,output:u.join(`
55
- `),metadata:{counts:l}}}}),autosar_build_context:w({description:"GROUND step: call this FIRST for any change. Two modes. (1) target=<module, container or element> for a configuration change (e.g. an MCAL Can/Fls/Port change): the element's current values with file:line and its param-def schema (allowed params, types, ranges, literals, multiplicity), plus the constraints the ingested specs/manuals give for it. (2) swc(+runnable) for C: assemble the context for implementing or changing a runnable, deterministically (same inputs -> byte-identical text) in a fixed order: 1 task, 2 SW-C definition (ports, interfaces, types, runnables, events), 3 ALLOWED RTE API for the runnable with the exact declarations from Rte_<Swc>.h (else SWS_RTE signatures), 4 the runnable's current C body, 5 functions it calls, 6 related config (Com signals / NvM blocks behind its ports), 7 FUZZY similar functions in other SW-Cs, 8 FUZZY similar past git changes (ARXML delta + C hunks). Fits budget_tokens (~4 chars/token) by dropping 8, 7, then shortening bodies; never the allowed API. Returns the text plus a JSON manifest of every source included. Call it FIRST for any runnable task and use only the allowed APIs it lists. Both modes append DOC FACTS from the selected Hydron project's ingested documents (docs=false skips; the retriever can take minutes, results are cached per session). Plan only from these facts and cite them (F#/D#); a fact you could not ground is an open question, not a guess.",args:{swc:c.string().optional().describe("SW-C short name or AUTOSAR path (runnable/C mode)"),target:c.string().optional().describe("configuration mode: module, container or element (AUTOSAR path, SHORT-NAME or partial path), e.g. 'Can', 'CanController_0'"),docs:c.boolean().optional().describe("add DOC FACTS from the ingested documents (default true)"),runnable:c.string().optional().describe("runnable short name or SYMBOL (default: all runnables)"),task:c.string().describe("the task, verbatim"),budget_tokens:c.number().int().min(500).max(200000).optional().describe("token budget (default 24000)"),use_backend:c.boolean().optional().describe("if the runnable has no local C definition, look it up with the backend trace of the selected workspace"),workspace_id:c.union([c.string(),c.number()]).optional().describe("H2Loop project id or name for use_backend"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=e.docs!==!1;if(!e.swc){if(!e.target)return"ERROR: give target (configuration change) or swc (+runnable, for C code).";let u=await y(o,["read",e.target,"--depth","3","--schema"],s.abort);if(!u.ok)return _(u);let{ok:h,engine:m,ms:g,...T}=u,k=JSON.stringify(T,null,1).split(`
56
- `),pe=k.length>400?`${k.slice(0,400).join(`
53
+ ${o.map((h)=>h.diff).join(`
54
+ `)}`+(g||`
55
+ Next: ecuc_validate (checks extract/SWC files too).`)}}),autosar_add_like:S({description:"Add a new X LIKE an existing Y, end to end, in one checked step: a new message (frame, I-PDU, triggerings, EcuC Pdu, CanIf PDU, PduR route, Com I-PDU), port, runnable with its event and task mapping, Dem event + DTC, NvM block + Fee block, interface... Projects name a thing's elements after it, so the engine clones every element whose SHORT-NAME contains `like` and that is linked to the others by references, across ECUC and System/SWC ARXML, renamed to `name`; lists that contain the template get the clone too; handle IDs get the lowest free value; the template's own members (a message's signals) are left out unless named in `also`. `set` then sets values on the clones by ECUC parameter name, XML tag, or 'Owner:key'. Schema + references are checked and validation runs before/after: any new finding rolls everything back. PREFER THIS over building a chain op by op with arxml_edit/ecuc_edit. Example: like='BCM_DoorStatus', name='BCM_SettingsAck', also={'DrvDoorOpenSts':'SettingsAckCounter'}, set={'IDENTIFIER':'0x4F8','CanIfTxPduCanId':'0x4F8','PduLength':2,'FRAME-LENGTH':2,'I-SIGNAL-I-PDU:LENGTH':2,'ComTxModeTimePeriod':1.0,'SettingsAckCounter:ComBitSize':8}. Pick as template the existing element most like the new one (same direction/bus/kind); dry_run first.",args:{like:d.string().describe("name stem of the template element, e.g. BCM_DoorStatus (an existing message), DoorLockActuatorFault (a Dem event)"),name:d.string().describe("the new stem, e.g. BCM_SettingsAck"),also:E(d.record(d.string(),d.string())).optional().describe("template members to clone and rename too: {template stem: new stem}"),set:E(d.record(d.string(),d.any())).optional().describe("values on the clones: {ParamDefName | XML-TAG | 'Owner:key': value}"),seed:d.string().optional().describe("one element of the template chain, when several separate chains carry the stem"),keep_members:d.boolean().optional().describe("also copy the template's own members (default false)"),dry_run:d.boolean().optional(),project_dir:d.string().optional()},async execute(e,a){let i=["add-like","--like",e.like,"--name",e.name];if(e.also)i.push("--also",JSON.stringify(T(e.also)));if(e.set)i.push("--set",JSON.stringify(T(e.set)));if(e.seed)i.push("--seed",e.seed);if(e.keep_members)i.push("--keep-members");if(e.dry_run)i.push("--dry-run");let r=await b(w(a.directory,e.project_dir),i,a.abort),o=(l)=>r[l]??[],u=[r.ok?`${r.written?"Written":"Dry run OK"}: ${o("created").length} element(s) like ${e.like} as ${e.name}`:`NOT DONE: ${r.error}`,...o("created").length?["Created:",...o("created").map((l)=>` ${l}`)]:[],...o("set").length?["Set:",...o("set").map((l)=>` ${l}`)]:[],...o("ids").length?["Handle IDs:",...o("ids").map((l)=>` ${l}`)]:[],...o("linked").length?["Linked:",...o("linked").map((l)=>` ${l}`)]:[],...o("notCopied").length?[`Not copied (the template's own members): ${o("notCopied").length}`,...o("notCopied").slice(0,12).map((l)=>` ${l}`)]:[],...o("singleValuedNotLinked").length?["Single-valued references to the template NOT changed (add/point your own if the new element needs one):",...o("singleValuedNotLinked").map((l)=>` ${l}`)]:[],...o("notes").map((l)=>`NOTE: ${l}`),...o("todo").length?["STILL TO DO (completeness, not undone):",...o("todo").map((l)=>` ${l}`)]:[],...(r.problems??[]).map((l)=>`PROBLEM: ${l}`),...(r.diagnostics??[]).length?N(r.diagnostics,20):[]];if(r.ok&&!r.written)u.push("Next: run again without dry_run.");if(r.ok&&r.written)u.push(String(r.next??""));return u.join(`
56
+ `)}}),autosar_add_signal:S({description:"Add one Com signal end to end, consistently: ComSignal (ComHandleId = lowest unused; existing handles never move) + ComIPduSignalRef in the ECUC, SYSTEM-SIGNAL / I-SIGNAL / I-SIGNAL-TO-I-PDU-MAPPING and the rest of the signal chain (triggering, port, extract references) in the System extract, and the DBC SG_ line when a .dbc carries the frame. Everything is derived by following references from an existing signal of the same I-PDU. It refuses on overlap or out-of-bounds bits, picks the first free byte-aligned position when bit_position is omitted, runs ecuc_validate after writing and rolls back on any new finding. Then run autosar_generate, use ComConf_ComSignal_<name> in the C code, and autosar_build.",args:{name:d.string(),ipdu:d.string().describe("ComIPdu AUTOSAR path or SHORT-NAME"),bit_size:d.number().int().min(1).max(64).optional(),type:d.string().optional().describe("ComSignalType, e.g. UINT8 (default)"),bit_position:d.number().int().min(0).optional().describe("omit for the first free byte-aligned position"),endianness:d.enum(["LITTLE_ENDIAN","BIG_ENDIAN","OPAQUE"]).optional(),init:d.union([d.number(),d.string()]).optional(),transfer_property:d.string().optional().describe("e.g. PENDING, TRIGGERED, TRIGGERED_ON_CHANGE"),dry_run:d.boolean().optional(),project_dir:d.string().optional()},async execute(e,a){let i=["add-signal","--name",e.name,"--ipdu",e.ipdu,"--type",e.type??"UINT8"];if(e.bit_size!==void 0)i.push("--bit-size",String(e.bit_size));if(e.bit_position!==void 0)i.push("--bit-position",String(e.bit_position));if(e.endianness)i.push("--endianness",e.endianness);if(e.init!==void 0)i.push("--init",String(e.init));if(e.transfer_property)i.push("--transfer-property",e.transfer_property);if(e.dry_run)i.push("--dry-run");let r=await b(w(a.directory,e.project_dir),i,a.abort);if(!r.ok)return k(r)+(r.diagnostics?.length?`
57
+ ${N(r.diagnostics,20).join(`
58
+ `)}`:"");let{ok:o,engine:u,ms:l,...g}=r;return JSON.stringify(g,null,1)}}),autosar_audit:S({description:"Integration review \u2014 run this FIRST when reviewing C code that uses BSW services. Deterministically scans "+"C files (default: .hydron/autosar.json `audit` paths, else every .c in the project except generated files, "+"recognised by banner or *_Cfg/*_PBcfg/Rte* names \u2014 folder names do not matter) for Com_Send/ReceiveSignal[Group], "+"NvM_Read/WriteBlock/RestoreBlockDefaults, Dem_SetEventStatus/ReportErrorStatus, PduR_*Transmit and Rte_Write_/Rte_Read_ (resolved through the generated Output/ RTE headers), resolves every handle through the ECUC (symbolic name or raw number), and reports: H2L-API-001 raw numeric handle, -002 unknown handle, -003 direction mismatch (send on a RECEIVE I-PDU signal or receive on SEND), -004 C object width vs ComSignalType, -005 NvM NULL_PTR without a permanent RAM block (NULL_PTR WITH one is valid, SWS_NvM_00900). Each finding has file:line, evidence (configured values + AUTOSAR paths) and a spec citation; calls it cannot check are listed as unchecked, never guessed. Confirm findings with ecuc_read / autosar_trace.",args:{paths:x().describe("C files, directories or globs to scan (default: .hydron/autosar.json audit paths, else every .c under the project except generator output); matching nothing is an ERROR"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=await b(i,["audit",...C(e.paths)??[]],a.abort);if(!r.ok)return A(r)+k(r);let o=(r.notes??[]).map((m)=>`NOTE: ${m}`),{findings:u,unchecked:l,scanned:g}=r,h=[`Scanned ${g.sources.length} C file(s), ${g.calls} BSW call(s): ${u.length?JSON.stringify(r.summary):"no findings"}`,...u.map((m)=>`${m.file}:${m.line} ${m.severity.toUpperCase()} ${m.rule} ${m.call}
59
+ ${m.message}
60
+ evidence: ${JSON.stringify(m.evidence)}
61
+ spec: ${m.cite}${m.fix?`
62
+ fix: ${m.fix}`:""}`)];if(l.length)h.push(`Unchecked (${l.length}):`,...l.slice(0,50).map((m)=>` ${m.file}:${m.line} ${m.call} \u2014 ${m.reason}`));let p=r.unreferenced??{};for(let[m,_]of Object.entries(p))h.push(`INFO configured but never referenced in the scanned code \u2014 ${m}: ${_.count} (e.g. ${_.sample.slice(0,5).join(", ")})`);return h.push(...o),{title:`autosar_audit ${JSON.stringify(r.summary)}`,output:A(r)+h.join(`
63
+ `)}}}),autosar_verify_rte:S({description:"Verify Rte_* calls in C against the SW-C description (SWS_RTE naming, R25-11). Each call is mapped to its enclosing function, then to every runnable whose entry point (RUNNABLE-ENTITY SYMBOL) reaches it, and classified: OK (the runnable has the access point), NOT-ALLOWED (the API exists for the SW-C but this runnable has no dataSendPoint/dataReceivePoint/serverCallPoint/... for it), INVENTED (no such port, element, runnable, or wrong direction/semantics). For NOT-ALLOWED/INVENTED it returns ready-to-run arxml_edit ops (dry-run checked) that add the access point / port / data element. Run it after writing runnable code and never finish with a failing verify.",args:{paths:x().describe("C files, directories or globs (default: all in-house C)"),diff_against:d.string().optional().describe("git ref: only calls on lines changed since this ref (plus untracked files)"),swc:d.string().optional().describe("restrict to one SW-C (short name or AUTOSAR path)"),runnable:d.string().optional().describe("restrict to one runnable (short name or SYMBOL)"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=["verify-rte",...C(e.paths)??[]];if(e.diff_against)r.push("--diff-against",e.diff_against);if(e.swc)r.push("--swc",e.swc);if(e.runnable)r.push("--runnable",e.runnable);let o=await b(i,r,a.abort);if(!o.ok)return k(o);let{findings:u,counts:l}=o,g=u.filter((y)=>y.status!=="OK"),h=o.scanned,p=[`Verified ${h.calls} Rte_* call(s) in ${h.files.length} file(s) against SW-C(s) ${o.swcs.join(", ")}: OK ${l.OK}, NOT-ALLOWED ${l["NOT-ALLOWED"]}, INVENTED ${l.INVENTED}`+(g.length?" \u2014 FAILING":" \u2014 PASS")];for(let y of g){if(p.push(`${y.at} ${y.status} ${y.call} in ${y.function}() [${y.swc??"?"}${y.runnable?`/${y.runnable}`:""}]`,` ${y.why}`),y.expected)p.push(` expected: ${y.expected} [${y.cite}]`);if(y.hint)p.push(` hint: ${y.hint}`);if(y.arxml_edit?.length)p.push(` fix (arxml_edit ops${y.opsChecked===!0?", dry-run OK":y.opsChecked?`, dry-run FAILED: ${y.opsChecked}`:""}): ${JSON.stringify(y.arxml_edit)}`)}let m=o.notChecked??[];if(m.length)p.push(`Not checked (${m.length}): ${m.map((y)=>`${y.call} @ ${y.at}`).join(", ")}`);let _=o.runnablesWithoutCode??[];if(_.length)p.push(`Runnables without a C definition: ${_.map((y)=>`${y.swc}.${y.runnable} (${y.symbol})`).join(", ")}`);for(let y of o.notes??[])p.push(`NOTE: ${y}`);if(g.length)p.push("Next: rewrite the call with an allowed API (autosar_build_context lists them), or \u2014 only if the behaviour "+"truly needs new data \u2014 apply the ops with arxml_edit, run autosar_generate, then autosar_verify_rte again.");return{title:`autosar_verify_rte ${g.length?"FAIL":"PASS"} ${JSON.stringify(l)}`,output:p.join(`
64
+ `),metadata:{counts:l}}}}),autosar_build_context:S({description:"GROUND step: call this FIRST for any change. Two modes. (1) target=<module, container or element> for a configuration change (e.g. an MCAL Can/Fls/Port change): the element's current values with file:line and its param-def schema (allowed params, types, ranges, literals, multiplicity), plus the constraints the ingested specs/manuals give for it. (2) swc(+runnable) for C: assemble the context for implementing or changing a runnable, deterministically (same inputs -> byte-identical text) in a fixed order: 1 task, 2 SW-C definition (ports, interfaces, types, runnables, events), 3 ALLOWED RTE API for the runnable with the exact declarations from Rte_<Swc>.h (else SWS_RTE signatures), 4 the runnable's current C body, 5 functions it calls, 6 related config (Com signals / NvM blocks behind its ports), 7 FUZZY similar functions in other SW-Cs, 8 FUZZY similar past git changes (ARXML delta + C hunks). Fits budget_tokens (~4 chars/token) by dropping 8, 7, then shortening bodies; never the allowed API. Returns the text plus a JSON manifest of every source included. Call it FIRST for any runnable task and use only the allowed APIs it lists. Both modes append DOC FACTS from the selected Hydron project's ingested documents (docs=false skips; the retriever can take minutes, results are cached per session). Plan only from these facts and cite them (F#/D#); a fact you could not ground is an open question, not a guess.",args:{swc:d.string().optional().describe("SW-C short name or AUTOSAR path (runnable/C mode)"),target:d.string().optional().describe("configuration mode: module, container or element (AUTOSAR path, SHORT-NAME or partial path), e.g. 'Can', 'CanController_0'"),docs:d.boolean().optional().describe("add DOC FACTS from the ingested documents (default true)"),runnable:d.string().optional().describe("runnable short name or SYMBOL (default: all runnables)"),task:d.string().describe("the task, verbatim"),budget_tokens:d.number().int().min(500).max(200000).optional().describe("token budget (default 24000)"),use_backend:d.boolean().optional().describe("if the runnable has no local C definition, look it up with the backend trace of the selected workspace"),workspace_id:d.union([d.string(),d.number()]).optional().describe("H2Loop project id or name for use_backend"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=e.docs!==!1;if(!e.swc){if(!e.target)return"ERROR: give target (configuration change) or swc (+runnable, for C code).";let p=await b(i,["read",e.target,"--depth","3","--schema"],a.abort),{ok:m,engine:_,ms:y,...j}=p,v=p.ok?JSON.stringify(j,null,1).split(`
65
+ `):[`(${e.target} is not an ECUC element this engine can read: ${String(p.error??"").slice(0,300)}; for System/SWC ARXML read the element or its file, and use autosar_config_lookup / ecuc_find)`],me=v.length>400?`${v.slice(0,400).join(`
57
66
  `)}
58
- \u2026 (${k.length-400} more lines: narrow the target)`:k.join(`
59
- `),ge=i?await J(`For this AUTOSAR configuration task: "${e.task}". The element concerned is ${e.target}. From the specifications and manuals in this knowledge base, list every constraint, default, allowed range or literal, multiplicity, dependency on other parameters or modules, and hardware limit of the target microcontroller that applies to it and to what the task changes, each with the document name and section or requirement ID.`,s.sessionID,e.workspace_id,s.abort):"(skipped: docs=false)";return{title:`autosar_build_context target ${e.target}`,output:["# CONTEXT (configuration change)",`## 1. TASK
67
+ \u2026 (${v.length-400} more lines: narrow the target)`:v.join(`
68
+ `),ye=r?await V(`For this AUTOSAR configuration task: "${e.task}". The element concerned is ${e.target}. From the specifications and manuals in this knowledge base, list every constraint, default, allowed range or literal, multiplicity, dependency on other parameters or modules, and hardware limit of the target microcontroller that applies to it and to what the task changes, each with the document name and section or requirement ID.`,a.sessionID,e.workspace_id,a.abort):"(skipped: docs=false)";return{title:`autosar_build_context target ${e.target}`,output:["# CONTEXT (configuration change)",`## 1. TASK
60
69
  ${e.task}`,`## 2. PROJECT FACTS [F]: current values with @at file:line, and the param-def schema (what may be set)
61
- ${pe}`,`## 3. DOC FACTS [D]: from the ingested documents
62
- ${ge}`,`## 4. NEXT
70
+ ${me}`,`## 3. DOC FACTS [D]: from the ingested documents
71
+ ${ye}`,`## 4. NEXT
63
72
  Write the facts you will rely on as a numbered list (F1.. from section 2 with their @at, D1.. from section 3 with their citation). Plan the change from those facts only, in dependency order, citing them. Anything you could not ground: look it up (ecuc_find, ecuc_read, autosar_config_lookup) or ask; never guess a value, id or reference target. Then edit (ecuc_edit / arxml_edit), ecuc_validate until clean, then ecuc_validate docs=true and answer its checklist.`].join(`
64
73
 
65
- `)}}let t=["build-context","--swc",e.swc,"--task",e.task,"--budget-tokens",String(e.budget_tokens??24000)];if(e.runnable)t.push("--runnable",e.runnable);let d=await y(o,t,s.abort);if(!d.ok)return _(d);let l=d.text,f=d.codeMissing??[];if(f.length&&e.use_backend){let u=[];try{let h=await N(e.workspace_id,s.abort);for(let m of f){let g=await x("/api/v1/mcp/autosar/trace",h,{query:m,limit:3},s.abort);u.push(`### backend trace for ${m} (workspace ${h})
66
- ${JSON.stringify(g.matches??[],null,1).slice(0,6000)}`)}}catch(h){u.push(`(backend lookup unavailable: ${h instanceof Error?h.message:String(h)})`)}l+=`
74
+ `)}}let o=["build-context","--swc",e.swc,"--task",e.task,"--budget-tokens",String(e.budget_tokens??24000)];if(e.runnable)o.push("--runnable",e.runnable);let u=await b(i,o,a.abort);if(!u.ok)return k(u);let l=u.text,g=u.codeMissing??[];if(g.length&&e.use_backend){let p=[];try{let m=await I(e.workspace_id,a.abort);for(let _ of g){let y=await D("/api/v1/mcp/autosar/trace",m,{query:_,limit:3},a.abort);p.push(`### backend trace for ${_} (workspace ${m})
75
+ ${JSON.stringify(y.matches??[],null,1).slice(0,6000)}`)}}catch(m){p.push(`(backend lookup unavailable: ${m instanceof Error?m.message:String(m)})`)}l+=`
67
76
  ## 4b. RUNNABLE CODE FROM THE BACKEND (not part of the deterministic context)
68
- ${u.join(`
77
+ ${p.join(`
69
78
  `)}
70
- `}if(i){let u=await J(`For this AUTOSAR task: "${e.task}" in SW-C ${e.swc}${e.runnable?`, runnable ${e.runnable}`:""}. From the specifications and manuals in this knowledge base, list the requirements and constraints that apply (RTE API semantics, timing, data handling, BSW service usage), each with the document name and section or requirement ID.`,s.sessionID,e.workspace_id,s.abort);l+=`
79
+ `}if(r){let p=await V(`For this AUTOSAR task: "${e.task}" in SW-C ${e.swc}${e.runnable?`, runnable ${e.runnable}`:""}. From the specifications and manuals in this knowledge base, list the requirements and constraints that apply (RTE API semantics, timing, data handling, BSW service usage), each with the document name and section or requirement ID.`,a.sessionID,e.workspace_id,a.abort);l+=`
71
80
  ## 9. DOC FACTS [D] from the ingested documents (not part of the deterministic context)
72
- ${u}
73
- `}let p={swc:d.swc,runnables:d.runnables,estimatedTokens:d.estimatedTokens,budgetTokens:d.budgetTokens,reductions:d.reductions,overBudget:d.overBudget??!1,sha1:d.sha1,sections:d.sections,sources:d.manifest};return{title:`autosar_build_context ${d.swc}${d.runnables.length===1?`/${d.runnables[0]}`:""} ~${d.estimatedTokens} tok`,output:`${l}
81
+ ${p}
82
+ `}let h={swc:u.swc,runnables:u.runnables,estimatedTokens:u.estimatedTokens,budgetTokens:u.budgetTokens,reductions:u.reductions,overBudget:u.overBudget??!1,sha1:u.sha1,sections:u.sections,sources:u.manifest};return{title:`autosar_build_context ${u.swc}${u.runnables.length===1?`/${u.runnables[0]}`:""} ~${u.estimatedTokens} tok`,output:`${l}
74
83
  ---
75
- MANIFEST ${JSON.stringify(p)}`,metadata:{sha1:d.sha1,estimatedTokens:d.estimatedTokens}}}}),autosar_codegen:w({description:"Deterministic BSW configuration code generation from the ECUC ARXML + param defs (no LLM output: the same ARXML always gives the same C). Modules: NvM, MemIf, Fee, Ea, Dem, PduR, CanIf, Port, Dio (any other configured module can be named; it then gets the generic def-driven emission only). Emits <Mip>_Cfg.h (pre-compile #defines, STD_ON/STD_OFF, <MIP>_CFG_* version macros, TPS_ECUC_02108 symbols <Mip>Conf_<ContainerDef>_<Name>), <Mip>_Cfg.c / _Lcfg.c / _PBcfg.c by each parameter's config class, const tables for multiplicity-n containers, MemMap-wrapped, SchM_<Mip>.h and <Mip>_MemMap.h. Static-code struct layouts (the SIP's NvM_BlockDescriptorType, PduR_RoutingPathType, ...) come from a binding selected by the project's headers. Missing handle IDs are assigned deterministically and written back into the ECUC ARXML in place (TPS_ECUC_02107) unless write_back=false. Writes to the scratch dir .hydron/codegen/out unless out_dir is given ('project' = the project's Output dir). Follow with autosar_codegen_diff against the existing Output/, then autosar_build.",args:{modules:O().describe("modules to generate (default: every supported module that is configured)"),out_dir:c.string().optional().describe("output dir, relative to the project or absolute; 'project' = codegen.outDir / Output. Default: scratch .hydron/codegen/out"),write_back:c.boolean().optional().describe("write assigned handle IDs into the ECUC ARXML (default true)"),binding:c.string().optional().describe("force a static-code binding (e.g. 'sip-standin'), or 'none' for generic tables only"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=["codegen"];for(let u of E(e.modules)??[])i.push("--module",u);if(e.out_dir)i.push("--out",e.out_dir);if(e.write_back===!1)i.push("--no-write-back");if(e.binding)i.push("--binding",e.binding);let t=await y(o,i,s.abort);if(!t.ok){let u=t.handleErrors??[];return S(t)+_(t)+(u.length?`
76
- ${u.map((h)=>` ${h}`).join(`
77
- `)}`:"")}let{modules:d,handles:l,timings:f}=t,p=[`Generated ${Object.keys(d).length} module(s) into ${t.outDir} in ${f.total_ms} ms (AUTOSAR R${t.release}):`,...Object.entries(d).map(([u,h])=>` ${u} (${h.variant}${h.binding?`, binding ${h.binding}`:", generic tables"}): ${h.files.join(", ")}; ${h.symbols} symbol(s)`+h.notes.map((m)=>`
78
- NOTE: ${m}`).join("")),l.assigned.length?`Assigned ${l.assigned.length} handle ID(s) (${l.writtenBack?"written back into the ECUC ARXML":"in memory only, NOT written back"}):
79
- `+l.assigned.slice(0,40).map((u)=>` ${u.path}/${u.param} = ${u.value} (hex ${he(u.value)})`).join(`
84
+ MANIFEST ${JSON.stringify(h)}`,metadata:{sha1:u.sha1,estimatedTokens:u.estimatedTokens}}}}),autosar_codegen:S({description:"Deterministic BSW configuration code generation from the ECUC ARXML + param defs (no LLM output: the same ARXML always gives the same C). Modules: NvM, MemIf, Fee, Ea, Dem, PduR, CanIf, Port, Dio (any other configured module can be named; it then gets the generic def-driven emission only). Emits <Mip>_Cfg.h (pre-compile #defines, STD_ON/STD_OFF, <MIP>_CFG_* version macros, TPS_ECUC_02108 symbols <Mip>Conf_<ContainerDef>_<Name>), <Mip>_Cfg.c / _Lcfg.c / _PBcfg.c by each parameter's config class, const tables for multiplicity-n containers, MemMap-wrapped, SchM_<Mip>.h and <Mip>_MemMap.h. Static-code struct layouts (the SIP's NvM_BlockDescriptorType, PduR_RoutingPathType, ...) come from a binding selected by the project's headers. Missing handle IDs are assigned deterministically and written back into the ECUC ARXML in place (TPS_ECUC_02107) unless write_back=false. Writes to the scratch dir .hydron/codegen/out unless out_dir is given ('project' = the project's Output dir). Follow with autosar_codegen_diff against the existing Output/, then autosar_build.",args:{modules:x().describe("modules to generate (default: every supported module that is configured)"),out_dir:d.string().optional().describe("output dir, relative to the project or absolute; 'project' = codegen.outDir / Output. Default: scratch .hydron/codegen/out"),write_back:d.boolean().optional().describe("write assigned handle IDs into the ECUC ARXML (default true)"),binding:d.string().optional().describe("force a static-code binding (e.g. 'sip-standin'), or 'none' for generic tables only"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=["codegen"];for(let p of C(e.modules)??[])r.push("--module",p);if(e.out_dir)r.push("--out",e.out_dir);if(e.write_back===!1)r.push("--no-write-back");if(e.binding)r.push("--binding",e.binding);let o=await b(i,r,a.abort);if(!o.ok){let p=o.handleErrors??[];return A(o)+k(o)+(p.length?`
85
+ ${p.map((m)=>` ${m}`).join(`
86
+ `)}`:"")}let{modules:u,handles:l,timings:g}=o,h=[`Generated ${Object.keys(u).length} module(s) into ${o.outDir} in ${g.total_ms} ms (AUTOSAR R${o.release}):`,...Object.entries(u).map(([p,m])=>` ${p} (${m.variant}${m.binding?`, binding ${m.binding}`:", generic tables"}): ${m.files.join(", ")}; ${m.symbols} symbol(s)`+m.notes.map((_)=>`
87
+ NOTE: ${_}`).join("")),l.assigned.length?`Assigned ${l.assigned.length} handle ID(s) (${l.writtenBack?"written back into the ECUC ARXML":"in memory only, NOT written back"}):
88
+ `+l.assigned.slice(0,40).map((p)=>` ${p.path}/${p.param} = ${p.value} (hex ${ge(p.value)})`).join(`
80
89
  `)+(l.changes.length?`
81
- ${l.changes.map((u)=>u.diff).join(`
82
- `)}`:""):"Handle IDs: all already present in the ECUC (nothing assigned).",...(t.notes??[]).map((u)=>`NOTE: ${u}`)];return{title:`autosar_codegen ${Object.keys(d).join(", ")}`,output:S(t)+p.join(`
83
- `)}}}),autosar_codegen_diff:w({description:"Semantic diff of generated C configuration code against a reference dir (e.g. the vendor-generated Output/): compares #define names and values and the contents of every initialised table/struct element by element, plus table ordering, after normalising numbers (0x2A0u == 672), casts, parentheses and macro references. Ignores formatting, comments, banners and include order. Reports per module: matching symbols, missing (in reference only), extra (generated only), value mismatches with both values.",args:{reference_dir:c.string().describe("reference code dir, e.g. Output (relative to the project or absolute)"),generated_dir:c.string().optional().describe("generated code dir (default: the autosar_codegen scratch dir)"),modules:O().describe("only these modules"),project_dir:c.string().optional().describe("project root (default: session directory)")},async execute(e,s){let o=b(s.directory,e.project_dir),i=["codegen-diff",e.reference_dir];if(e.generated_dir)i.push(e.generated_dir);for(let f of E(e.modules)??[])i.push("--module",f);let t=await y(o,i,s.abort);if(!t.ok)return _(t);let d=t.modules,l=[`Semantic diff ${t.reference} (reference) vs ${t.generated} (generated): ${JSON.stringify(t.total)}`,`Not generated (reference only): ${t.notGenerated.join(", ")||"-"}; not in reference: ${t.notInReference.join(", ")||"-"}`];for(let[f,p]of Object.entries(d)){if(l.push(`== ${f}: ${p.summary.matching} matching, ${p.summary.missing} missing, ${p.summary.extra} extra, ${p.summary.mismatches} mismatch(es); table elements ${p.elements.match} equal / ${p.elements.mismatch} different`),p.defines.missing.length)l.push(` missing #define: ${p.defines.missing.slice(0,30).join(", ")}`);for(let u of p.defines.mismatch.slice(0,30))l.push(` #define ${u.name}: reference ${u.reference} | generated ${u.generated}`);if(p.objects.missing.length)l.push(` missing object: ${p.objects.missing.join(", ")}`);if(p.objects.extra.length)l.push(` extra object: ${p.objects.extra.join(", ")}`);for(let u of p.objects.order)l.push(` order differs: ${u.name} (same rows)`);for(let u of p.objects.mismatch)l.push(` ${u.name}: ${u.count} element(s) differ`,...u.elements.slice(0,12).map((h)=>` ${h.element}: reference ${h.reference} | generated ${h.generated}`));if(p.defines.extra.length)l.push(` extra #define (${p.defines.extra.length}): ${p.defines.extra.slice(0,12).join(", ")}${p.defines.extra.length>12?", \u2026":""}`)}return{title:`autosar_codegen_diff ${JSON.stringify(t.total)}`,output:l.join(`
84
- `)}}}),autosar_generate:a("generate"),autosar_build:a("build")}),n)}},Ke=Ye;export{Ke as Autosar};
90
+ ${l.changes.map((p)=>p.diff).join(`
91
+ `)}`:""):"Handle IDs: all already present in the ECUC (nothing assigned).",...(o.notes??[]).map((p)=>`NOTE: ${p}`)];return{title:`autosar_codegen ${Object.keys(u).join(", ")}`,output:A(o)+h.join(`
92
+ `)}}}),autosar_codegen_diff:S({description:"Semantic diff of generated C configuration code against a reference dir (e.g. the vendor-generated Output/): compares #define names and values and the contents of every initialised table/struct element by element, plus table ordering, after normalising numbers (0x2A0u == 672), casts, parentheses and macro references. Ignores formatting, comments, banners and include order. Reports per module: matching symbols, missing (in reference only), extra (generated only), value mismatches with both values.",args:{reference_dir:d.string().describe("reference code dir, e.g. Output (relative to the project or absolute)"),generated_dir:d.string().optional().describe("generated code dir (default: the autosar_codegen scratch dir)"),modules:x().describe("only these modules"),project_dir:d.string().optional().describe("project root (default: session directory)")},async execute(e,a){let i=w(a.directory,e.project_dir),r=["codegen-diff",e.reference_dir];if(e.generated_dir)r.push(e.generated_dir);for(let g of C(e.modules)??[])r.push("--module",g);let o=await b(i,r,a.abort);if(!o.ok)return k(o);let u=o.modules,l=[`Semantic diff ${o.reference} (reference) vs ${o.generated} (generated): ${JSON.stringify(o.total)}`,`Not generated (reference only): ${o.notGenerated.join(", ")||"-"}; not in reference: ${o.notInReference.join(", ")||"-"}`];for(let[g,h]of Object.entries(u)){if(l.push(`== ${g}: ${h.summary.matching} matching, ${h.summary.missing} missing, ${h.summary.extra} extra, ${h.summary.mismatches} mismatch(es); table elements ${h.elements.match} equal / ${h.elements.mismatch} different`),h.defines.missing.length)l.push(` missing #define: ${h.defines.missing.slice(0,30).join(", ")}`);for(let p of h.defines.mismatch.slice(0,30))l.push(` #define ${p.name}: reference ${p.reference} | generated ${p.generated}`);if(h.objects.missing.length)l.push(` missing object: ${h.objects.missing.join(", ")}`);if(h.objects.extra.length)l.push(` extra object: ${h.objects.extra.join(", ")}`);for(let p of h.objects.order)l.push(` order differs: ${p.name} (same rows)`);for(let p of h.objects.mismatch)l.push(` ${p.name}: ${p.count} element(s) differ`,...p.elements.slice(0,12).map((m)=>` ${m.element}: reference ${m.reference} | generated ${m.generated}`));if(h.defines.extra.length)l.push(` extra #define (${h.defines.extra.length}): ${h.defines.extra.slice(0,12).join(", ")}${h.defines.extra.length>12?", \u2026":""}`)}return{title:`autosar_codegen_diff ${JSON.stringify(o.total)}`,output:l.join(`
93
+ `)}}}),autosar_generate:s("generate"),autosar_build:s("build")}),t)}},Qe=Ze;export{Qe as Autosar};
@@ -306,8 +306,11 @@ def add_like(project: Project, like: str, name: str, seed: Optional[str] = None,
306
306
  with open(f, "wb") as fh:
307
307
  fh.write(b)
308
308
  after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
309
- fresh = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
310
- and _dkey(d) not in before]
309
+ fresh_all = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning") and _dkey(d) not in before]
310
+ # completeness warnings (H2L-CHAIN-*: e.g. the new signal is not mapped to a port YET) are follow-up work, not a
311
+ # reason to undo the step; errors (a duplicated CAN id, a broken route) still roll everything back
312
+ fresh = [d for d in fresh_all if d["severity"] == "error" or not d["rule"].startswith("H2L-CHAIN")]
313
+ todo = [d for d in fresh_all if d not in fresh]
311
314
  if fresh:
312
315
  for f, b in originals.items():
313
316
  with open(f, "wb") as fh:
@@ -315,6 +318,7 @@ def add_like(project: Project, like: str, name: str, seed: Optional[str] = None,
315
318
  return {**res, "ok": False, "written": False, "error": "validation found new problems; all changes rolled back",
316
319
  "diagnostics": fresh[:20]}
317
320
  return {**res, "ok": True, "written": True, "validation": after["summary"],
321
+ "todo": [f"{d['rule']} {d['path']}: {d['message'][:300]}" for d in todo],
318
322
  "next": "check each created element against the request (ecuc_read / the diff), set what is still the "
319
323
  "template's (ecuc_edit / arxml_edit), add the new signals (autosar_add_signal), then regenerate "
320
324
  "and build"}
@@ -208,8 +208,11 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
208
208
  fh.write(b)
209
209
  # same inputs as `before` (explicit --defs/--config included), so only the edit's own findings count as new
210
210
  after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
211
- new = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
212
- and _dkey(d) not in before]
211
+ new_all = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning") and _dkey(d) not in before]
212
+ # completeness warnings (H2L-CHAIN-*: e.g. the new signal is not mapped to a port YET) are follow-up work, not a
213
+ # reason to undo the step; errors (a duplicated CAN id, a broken route) still roll everything back
214
+ new = [d for d in new_all if d["severity"] == "error" or not d["rule"].startswith("H2L-CHAIN")]
215
+ todo = [d for d in new_all if d not in new]
213
216
  if new:
214
217
  for f, b in originals.items():
215
218
  with open(f, "wb") as fh:
@@ -217,6 +220,7 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
217
220
  return {**res, "ok": False, "written": False, "error": "validation found new problems; all changes rolled back",
218
221
  "diagnostics": new[:20]}
219
222
  return {**res, "ok": True, "written": True, "validation": after["summary"],
223
+ "todo": [f"{d['rule']} {d['path']}: {d['message'][:300]}" for d in todo],
220
224
  "next": "regenerate (autosar_generate), use the generated symbol "
221
225
  f"ComConf_ComSignal_{name} in the C code, build (autosar_build)"}
222
226
 
@@ -74,7 +74,7 @@ class RuleContext:
74
74
 
75
75
 
76
76
  def run_rules(project: Project) -> Iterable[dict]:
77
- from . import com, handles, mem, nvm, pdur, system # noqa: F401 (registers the packs)
77
+ from . import chain, com, handles, mem, nvm, pdur, system # noqa: F401 (registers the packs)
78
78
  ctx = RuleContext(project)
79
79
  for r in RULES:
80
80
  for item in r.fn(ctx):
@@ -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)
@@ -111,7 +111,13 @@ Do not decide what to change from memory or from the prompt alone. Each change g
111
111
  3. **Change** with the structured tools only (`ecuc_edit`, `arxml_edit` incl. `new_file`, `autosar_add_signal`). Prefer
112
112
  one call per coherent change; nothing is written if any op fails.
113
113
  4. **Verify in two steps → fix → verify, until clean.**
114
- - 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.
115
121
  `autosar_generate` + `autosar_codegen_diff` / `autosar_build` and `autosar_verify_rte` where the project has them.
116
122
  - Validation 2 (against the documents): `ecuc_validate docs=true` checks every element changed in this session
117
123
  against the ingested specs/manuals and returns a checklist; answer it line by line (OK / VIOLATED / NOT COVERED,
@@ -119,7 +125,8 @@ Do not decide what to change from memory or from the prompt alone. Each change g
119
125
  On any problem: read it, go back to step 1 for that fact, fix with the structured tools, verify again. Repeat until
120
126
  every check is clean. Then re-read the request and tick each todo against the evidence. Never report done with a
121
127
  failing or unrun check; if something cannot be made clean, say exactly what and why.
122
- 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
123
130
  `autosar_build_context` (or `autosar_audit`) first, and the system prompt lists every open verification item
124
131
  (unvalidated edits, failing validation, unverified C, changed elements not yet checked against the documents)
125
132
  until it is resolved.