@h2loop/autosar 1.2.0 → 1.3.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.2.0",
3
+ "version": "1.3.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,58 +1,58 @@
1
1
  // @bun
2
- import{isAbsolute as de,join as Y,resolve as le}from"path";import{tool as b}from"@kilocode/plugin";import{homedir as X}from"os";import{join as S}from"path";var z=()=>process.env.HYDRON_TEST_HOME||X(),T=(r,o)=>process.env[r]||S(z(),o),K=()=>S(T("XDG_CONFIG_HOME",".config"),"hydron-cli"),Z=()=>S(T("XDG_DATA_HOME",".local/share"),"hydron-cli"),Q=()=>S(T("XDG_STATE_HOME",".local/state"),"hydron-cli");async function I(r){let o=Bun.file(r);if(!await o.exists())return;return o.json().catch(()=>{return})}async function x(){let r=process.env.KILO_API_URL?.trim();if(r)return r.replace(/\/+$/,"");let o=await I(S(K(),"server-config.json")),e=typeof o?.apiBaseUrl==="string"?o.apiBaseUrl:void 0;if(!e)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return e.replace(/\/+$/,"")}function ee(r){let o=r?.split(".")[1];if(!o)return"";try{let e=JSON.parse(Buffer.from(o.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof e.sub==="string"?e.sub:""}catch{return""}}async function te(r){let o=ee(r.access);if(o)return o;if(!r.refresh)return"";return new Bun.CryptoHasher("sha256").update(r.refresh).digest("hex").slice(0,12)}async function F(){let o=(await I(S(Z(),"auth.json")))?.hydron;if(!o||o.type!=="oauth"||!o.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return o}async function H(){let r=await F();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 P(r){let o=await fetch(`${await x()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await H()}`},signal:r?AbortSignal.any([r,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(o.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!o.ok)throw Error(`could not list H2Loop projects: HTTP ${o.status}`);let e=await o.json();return Array.isArray(e)?e:e?.items??[]}var M=()=>S(Q(),"h2loop-project.json");async function A(r,o){let e=r===void 0||r===null?"":String(r).trim();if(/^\d+$/.test(e))return e;if(e){let s=await P(o),d=s.filter((l)=>l.name===e),f=d.length?d:s.filter((l)=>l.name?.toLowerCase()===e.toLowerCase());if(f.length===1)return String(f[0].id);if(f.length>1)throw Error(`workspace "${e}" is ambiguous: ${f.map((l)=>`${l.name} (id ${l.id})`).join(", ")}; pass the numeric id`);let p=s.filter((l)=>l.name?.toLowerCase().includes(e.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${e}"`+(p.length?`; similar: ${p.map((l)=>`${l.name} (id ${l.id})`).join(", ")}`:`; you have ${s.length} project(s)`))}let t=await I(M());if(!t)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let c=await F();if(t.project&&typeof t.project==="object"){let s=t.project;if(typeof t.account==="string"&&t.account!==await te(c))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(s.id!==void 0)return String(s.id)}let n=t;if(n.id===void 0)throw Error(`${M()} holds no project id; select a project again or pass workspace_id.`);if(!(await P(o)).some((s)=>String(s.id)===String(n.id)))throw Error(`The selected project ${n.name??""} (id ${n.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(n.id)}async function O(r,o,e,t){let c=`${await x()}${r}?workspace_id=${encodeURIComponent(o)}`,n=await fetch(c,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await H()}`},body:JSON.stringify(e),signal:t?AbortSignal.any([t,AbortSignal.timeout(120000)]):AbortSignal.timeout(120000)}),i=await n.text();if(n.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(n.status===404&&i.includes("Not Found"))throw Error(`${r} is not available on ${await x()} (backend predates the AUTOSAR endpoints).`);if(!n.ok)throw Error(`H2Loop ${r} failed: HTTP ${n.status} ${i.slice(0,800)}`);try{return JSON.parse(i)}catch(s){throw Error(`H2Loop ${r} returned non-JSON (${String(s)}): ${i.slice(0,300)}`)}}var D=/\.arxml\b/i,U="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.";function J(r,o){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!o)return;let e=typeof o.filePath==="string"?o.filePath:typeof o.file_path==="string"?o.file_path:"";if(["edit","write","multiedit"].includes(r)){let t=Array.isArray(o.edits)?o.edits.some((c)=>typeof c?.filePath==="string"&&D.test(c.filePath)):!1;if(D.test(e)||t)return`${U} (${r} on ${e||"an .arxml file"})`;return}if(r==="apply_patch"||r==="patch"){let t=String(o.patchText??o.patch??""),c=t.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??t.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return c?`${U} (${r} on ${c[1].trim()})`:void 0}if(r==="bash"){let t=String(o.command??"");if(!D.test(t))return;let c=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(t)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(t),n=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(t)||/\btee\b[^|;&]*\.arxml\b/i.test(t),i=/open\([^)]*\.arxml[^)]*,\s*(?:mode\s*=\s*)?['"][wa+]/i.test(t)||/\.arxml['"]?\s*\)?\s*\.write_(?:text|bytes)\b/i.test(t)||/write_(?:text|bytes)\([^)]*\)[^;\n]*\.arxml/i.test(t)||/\bwriteFile(?:Sync)?\([^)]*\.arxml/i.test(t)||/\.write\(\s*['"][^'"]*\.arxml/i.test(t)||/\b(?:awk|gawk)\b[^|;&]*-i\s+inplace/.test(t)||/\bed\b\s+-s\s+[^|;&]*\.arxml/.test(t);if(c||n||i)return`${U} (bash: ${t.slice(0,120)})`}return}import{existsSync as ne}from"fs";import{dirname as ie,join as L}from"path";import{fileURLToPath as re}from"url";var oe="autosar-data==0.17.0";function se(){let r=ie(re(import.meta.url)),o=[L(r,"..","python"),L(r,"..","..","python")].find((e)=>ne(L(e,"ecuc_engine","__main__.py")));if(!o)throw Error(`ECUC engine not found next to ${r} (expected ../python/ecuc_engine)`);return o}async function ae(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 j={};async function ce(){if(j.cmd!==void 0)return j.cmd;let r=process.env.AUTOSAR_PYTHON?.trim(),e=r?[[r]]:[["uv","run","--no-project","--quiet","--with",oe,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let t of e)if(await ae(t))return j.cmd=t,t;return j.cmd=null,null}async function y(r,o,e){let t=await ce();if(!t)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 c=se(),n=Bun.spawn([...t,"-m","ecuc_engine","--root",r,...o,"--json"],{cwd:r,env:{...process.env,PYTHONPATH:[c,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:e}),[i,s]=await Promise.all([new Response(n.stdout).text(),new Response(n.stderr).text()]);await n.exited;let d=i.trim().split(`
3
- `).filter(Boolean).at(-1);if(!d)return{ok:!1,error:`engine produced no output (exit ${n.exitCode}): ${s.trim().slice(-2000)}`};try{return JSON.parse(d)}catch(f){return{ok:!1,error:`engine output was not JSON (${String(f)}): ${d.slice(0,500)} ${s.slice(-500)}`}}}var a=b.schema,ue=["# AUTOSAR pack active","Load the `autosar` skill. Config: ecuc_read/ecuc_find, not grep. Edit via ecuc_edit/arxml_edit/","autosar_add_signal (.arxml hand edits blocked), then ecuc_validate + ecuc_crosscheck per module.","C review: autosar_audit. Runnables: autosar_build_context, autosar_verify_rte, autosar_build.","Cite @path/@def/file:line/hex as tools print; req IDs only if tool/spec text shows them."].join(`
4
- `);function N(r){if(typeof r!=="string")return r;let o=r.trim();if(!o.startsWith("[")&&!o.startsWith("{")&&!o.startsWith('"'))return r;try{return N(JSON.parse(o))}catch{return r}}var k=(r)=>a.preprocess(N,r);function R(r){let o=N(r);if(o===void 0||o===null||o==="")return;if(typeof o==="string")return[o];if(Array.isArray(o))return o.map(String).filter((e)=>e.trim());return[String(o)]}var E=()=>a.preprocess(R,a.array(a.string()).optional()),v=" 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 C(r,o=200){let e=r.slice(0,o).map((t)=>`${t.file}:${t.line} ${t.severity.toUpperCase()} ${t.rule} ${t.path} \u2014 ${t.message}${t.defRef?` [def ${t.defRef}]`:""}`);if(r.length>o)e.push(`\u2026 ${r.length-o} more`);return e}function w(r){let o=r.didYouMean?`
5
- Did you mean: ${r.didYouMean.join(", ")}`:"",e=r.schema?`
6
- Schema errors: ${JSON.stringify(r.schema)}`:"";return`ERROR: ${r.error}${o}${e}`}function _(r){let o=r.warnings??[];return o.length?o.map((e)=>`WARNING: ${e}`).join(`
2
+ import{readFileSync as le}from"fs";import{isAbsolute as ue,join as M,resolve as fe}from"path";import{tool as b}from"@kilocode/plugin";import{homedir as z}from"os";import{join as R}from"path";var K=()=>process.env.HYDRON_TEST_HOME||z(),I=(i,r)=>process.env[i]||R(K(),r),Z=()=>R(I("XDG_CONFIG_HOME",".config"),"hydron-cli"),Q=()=>R(I("XDG_DATA_HOME",".local/share"),"hydron-cli"),ee=()=>R(I("XDG_STATE_HOME",".local/state"),"hydron-cli");async function D(i){let r=Bun.file(i);if(!await r.exists())return;return r.json().catch(()=>{return})}async function x(){let i=process.env.KILO_API_URL?.trim();if(i)return i.replace(/\/+$/,"");let r=await D(R(Z(),"server-config.json")),c=typeof r?.apiBaseUrl==="string"?r.apiBaseUrl:void 0;if(!c)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return c.replace(/\/+$/,"")}function te(i){let r=i?.split(".")[1];if(!r)return"";try{let c=JSON.parse(Buffer.from(r.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof c.sub==="string"?c.sub:""}catch{return""}}async function ne(i){let r=te(i.access);if(r)return r;if(!i.refresh)return"";return new Bun.CryptoHasher("sha256").update(i.refresh).digest("hex").slice(0,12)}async function J(){let r=(await D(R(Q(),"auth.json")))?.hydron;if(!r||r.type!=="oauth"||!r.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return r}async function B(){let i=await J();if(typeof i.expires==="number"&&i.expires>0&&Date.now()>=i.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 i.access}async function F(i){let r=await fetch(`${await x()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await B()}`},signal:i?AbortSignal.any([i,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(r.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!r.ok)throw Error(`could not list H2Loop projects: HTTP ${r.status}`);let c=await r.json();return Array.isArray(c)?c:c?.items??[]}var H=()=>R(ee(),"h2loop-project.json");async function O(i,r){let c=i===void 0||i===null?"":String(i).trim();if(/^\d+$/.test(c))return c;if(c){let t=await F(r),d=t.filter((f)=>f.name===c),l=d.length?d:t.filter((f)=>f.name?.toLowerCase()===c.toLowerCase());if(l.length===1)return String(l[0].id);if(l.length>1)throw Error(`workspace "${c}" is ambiguous: ${l.map((f)=>`${f.name} (id ${f.id})`).join(", ")}; pass the numeric id`);let h=t.filter((f)=>f.name?.toLowerCase().includes(c.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${c}"`+(h.length?`; similar: ${h.map((f)=>`${f.name} (id ${f.id})`).join(", ")}`:`; you have ${t.length} project(s)`))}let e=await D(H());if(!e)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let s=await J();if(e.project&&typeof e.project==="object"){let t=e.project;if(typeof e.account==="string"&&e.account!==await ne(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 a=e;if(a.id===void 0)throw Error(`${H()} holds no project id; select a project again or pass workspace_id.`);if(!(await F(r)).some((t)=>String(t.id)===String(a.id)))throw Error(`The selected project ${a.name??""} (id ${a.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(a.id)}async function j(i,r,c,e){let s=`${await x()}${i}?workspace_id=${encodeURIComponent(r)}`,a=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await B()}`},body:JSON.stringify(c),signal:e?AbortSignal.any([e,AbortSignal.timeout(120000)]):AbortSignal.timeout(120000)}),n=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&&n.includes("Not Found"))throw Error(`${i} is not available on ${await x()} (backend predates the AUTOSAR endpoints).`);if(!a.ok)throw Error(`H2Loop ${i} failed: HTTP ${a.status} ${n.slice(0,800)}`);try{return JSON.parse(n)}catch(t){throw Error(`H2Loop ${i} returned non-JSON (${String(t)}): ${n.slice(0,300)}`)}}var U=/\.arxml\b/i,L="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.";function W(i,r){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!r)return;let c=typeof r.filePath==="string"?r.filePath:typeof r.file_path==="string"?r.file_path:"";if(["edit","write","multiedit"].includes(i)){let e=Array.isArray(r.edits)?r.edits.some((s)=>typeof s?.filePath==="string"&&U.test(s.filePath)):!1;if(U.test(c)||e)return`${L} (${i} on ${c||"an .arxml file"})`;return}if(i==="apply_patch"||i==="patch"){let e=String(r.patchText??r.patch??""),s=e.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??e.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return s?`${L} (${i} on ${s[1].trim()})`:void 0}if(i==="bash"){let e=String(r.command??"");if(!U.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),a=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(e)||/\btee\b[^|;&]*\.arxml\b/i.test(e),n=/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||a||n)return`${L} (bash: ${e.slice(0,120)})`}return}import{existsSync as ie}from"fs";import{dirname as re,join as P}from"path";import{fileURLToPath as oe}from"url";var se="autosar-data==0.17.0";function ae(){let i=re(oe(import.meta.url)),r=[P(i,"..","python"),P(i,"..","..","python")].find((c)=>ie(P(c,"ecuc_engine","__main__.py")));if(!r)throw Error(`ECUC engine not found next to ${i} (expected ../python/ecuc_engine)`);return r}async function ce(i){try{return await Bun.spawn([...i,"-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 C={};async function de(){if(C.cmd!==void 0)return C.cmd;let i=process.env.AUTOSAR_PYTHON?.trim(),c=i?[[i]]:[["uv","run","--no-project","--quiet","--with",se,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let e of c)if(await ce(e))return C.cmd=e,e;return C.cmd=null,null}async function y(i,r,c){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=ae(),a=Bun.spawn([...e,"-m","ecuc_engine","--root",i,...r,"--json"],{cwd:i,env:{...process.env,PYTHONPATH:[s,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:c}),[n,t]=await Promise.all([new Response(a.stdout).text(),new Response(a.stderr).text()]);await a.exited;let d=n.trim().split(`
3
+ `).filter(Boolean).at(-1);if(!d)return{ok:!1,error:`engine produced no output (exit ${a.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 o=b.schema,pe=["# AUTOSAR pack active","Load the `autosar` skill. Config: ecuc_read/ecuc_find, not grep. Edit via ecuc_edit/arxml_edit/","autosar_add_signal (.arxml hand edits blocked), then ecuc_validate + ecuc_crosscheck per module.","C review: autosar_audit. Runnables: autosar_build_context, autosar_verify_rte, autosar_build.","Cite @path/@def/file:line/hex as tools print; req IDs only if tool/spec text shows them."].join(`
4
+ `);function T(i){if(typeof i!=="string")return i;let r=i.trim();if(!r.startsWith("[")&&!r.startsWith("{")&&!r.startsWith('"'))return i;try{return T(JSON.parse(r))}catch{return i}}var A=(i)=>o.preprocess(T,i);function E(i){let r=T(i);if(r===void 0||r===null||r==="")return;if(typeof r==="string")return[r];if(Array.isArray(r))return r.map(String).filter((c)=>c.trim());return[String(r)]}var k=()=>o.preprocess(E,o.array(o.string()).optional()),v=" 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(i,r=200){let c=i.slice(0,r).map((e)=>`${e.file}:${e.line} ${e.severity.toUpperCase()} ${e.rule} ${e.path} \u2014 ${e.message}${e.defRef?` [def ${e.defRef}]`:""}`);if(i.length>r)c.push(`\u2026 ${i.length-r} more`);return c}function w(i){let r=i.didYouMean?`
5
+ Did you mean: ${i.didYouMean.join(", ")}`:"",c=i.schema?`
6
+ Schema errors: ${JSON.stringify(i.schema)}`:"";return`ERROR: ${i.error}${r}${c}`}function S(i){let r=i.warnings??[];return r.length?r.map((c)=>`WARNING: ${c}`).join(`
7
7
  `)+`
8
- `:""}function g(r,o){if(!o)return r;return de(o)?o:le(r,o)}async function fe(r){let o=Bun.file(Y(r,".hydron","autosar.json"));if(!await o.exists())return{};return o.json().catch((e)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(e)}`)})}async function pe(r,o){let e=(await fe(r))[o];if(typeof e==="string")return{command:e};if(e&&typeof e==="object"&&typeof e.command==="string")return e;return}function B(r,o=150){let e=r.split(`
9
- `);return e.length>o?[`\u2026 (${e.length-o} earlier lines omitted)`,...e.slice(-o)].join(`
10
- `):r}async function he(r,o,e,t){let c=process.platform==="win32"?["cmd","/d","/s","/c",r]:["sh","-c",r],n=Date.now(),i=Bun.spawn(c,{cwd:o,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([t,AbortSignal.timeout(e*1000)])}),[s,d]=await Promise.all([new Response(i.stdout).text(),new Response(i.stderr).text()]);return{code:await i.exited,out:s,err:d,secs:Math.round((Date.now()-n)/1000)}}var me=(r)=>(r??[]).map((o)=>`CAN ID ${o.canIdHex??"?"} (${o.canId??"?"}) ${o.direction} via ${o.canIfPdu}, controller ${o.controller??"?"}, network ${(o.network??[]).join(", ")||"?"}`).join("; ");async function W(r,o,e){let t=o.filter((s)=>s?.path&&/\/Com((Group)?Signal|IPdu)$/.test(String(s.definition_ref??""))&&s.ipdu===void 0&&s.direction===void 0);if(!t.length)return[];let c=await y(r,["signal-owners",...new Set(t.map((s)=>s.path))],e);if(!c.ok)return[`NOTE: owning I-PDU not computed locally (${c.error})`];let n=c.owners,i=[];for(let s of t){let d=n[s.path];if(d===null||d===void 0){i.push(`NOTE: ${s.path} is not in the local checkout (${r}); owning I-PDU/direction unknown`);continue}s.ipdu_local=d,i.push(d.length?`${s.path}: owning I-PDU ${d.map((f)=>`${f.ipdu} (ComIPduDirection=${f.direction??"?"}${f.can?.length?`; ${me(f.can)}`:""})`).join(", ")} [local ECUC]`:`${s.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return i}var ge=(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 o=r.trim(),e=o.startsWith("-")?-Number(o.slice(1)):Number(o);return Number.isSafeInteger(e)?e:null}return null},q=(r)=>r<0?`-0x${(-r).toString(16).toUpperCase()}`:`0x${r.toString(16).toUpperCase()}`;async function G(r,o,e){let t=o.filter((s)=>typeof s?.path==="string");if(!t.length)return[];let c=await y(r,["value-sites",...new Set(t.map((s)=>s.path))],e),n=c.ok?c.sites:{},i=[];if(!c.ok)i.push(`NOTE: value lines not computed locally (${c.error})`);for(let s of t){let d=n[s.path];if(d){s.values_local=d.values,i.push(`VALUES ${s.path} (container ${d.at}) [local ECUC; cite these lines]:`);for(let[l,u]of Object.entries(d.values))for(let m of Array.isArray(u)?u:[u])i.push(` ${l} = ${JSON.stringify(m.value)}${m.hex?` (dec ${m.dec}, hex ${m.hex})`:""} @ ${m.at}`);continue}let f=s.values&&typeof s.values==="object"?s.values:{},p={};for(let[l,u]of Object.entries(f)){let m=ge(u);if(m!==null)p[l]=`dec ${m}, hex ${q(m)}`}if(Object.keys(p).length)s.values_int=p;if(c.ok)i.push(`NOTE: ${s.path} is not in the local checkout (${r}): its file:line${s.file?` ${s.file}:${s.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 V=(r)=>{let o=r.notes??[],e=r.errors??[];return[...o.map((t)=>`NOTE: ${typeof t==="string"?t:JSON.stringify(t)}`),...e.map((t)=>`BACKEND ERROR: ${typeof t==="string"?t:JSON.stringify(t)}`)]};async function be(r,o){try{let e=await o(),t=typeof e==="string"?e:e.output;if(t&&t.trim())return e;return`${r}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(e){return`ERROR (${r}): ${e instanceof Error?e.message:String(e)}`}}function ye(r){for(let[o,e]of Object.entries(r)){let t=e.execute;e.execute=(c,n)=>be(o,()=>t(c,n))}return r}var we=async(r)=>{let o=(e)=>b({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:a.string().optional().describe("module name substituted for {module} in the command, if used"),args:a.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(t,c){let n=g(c.directory,t.project_dir),i=await pe(n,e);if(!i)return`No \`${e}\` command configured. Add it to ${Y(n,".hydron","autosar.json")}, e.g.
11
- {"${e}": {"command": "${e==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let s=t.args??"",d=i.command.replaceAll("{module}",t.module??""),f=d.includes("{args}")?d.replaceAll("{args}",s):`${d}${s?` ${s}`:""}`,p=g(n,i.cwd);await c.ask({permission:`autosar_${e}`,patterns:[f],always:[i.command],metadata:{command:f,cwd:p}}),c.metadata({title:`${e}: ${f}`});let l=await he(f,p,i.timeoutSec??3600,c.abort),u=l.code===0?"OK":`FAILED (exit ${l.code})`;return{title:`autosar_${e} ${u}`,output:[`$ ${f} (cwd ${p}, ${l.secs}s) -> ${u}`,l.out.trim()?`--- stdout (tail) ---
12
- ${B(l.out)}`:"",l.err.trim()?`--- stderr (tail) ---
13
- ${B(l.err,80)}`:""].filter(Boolean).join(`
14
- `),metadata:{exit:l.code}}}});return{"tool.execute.before":async(e,t)=>{let c=J(e.tool,t.args);if(c)throw Error(c)},"experimental.chat.system.transform":async(e,t)=>{t.system.push(ue)},tool:ye({ecuc_read:b({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."+v,args:{path:a.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:a.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:a.boolean().optional().describe("include the param-def summary for the element"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=["read"];if(e.path)c.push(e.path);if(c.push("--depth",String(e.depth??2)),e.schema)c.push("--schema");let n=await y(g(t.directory,e.project_dir),c,t.abort);if(!n.ok)return _(n)+w(n);let{ok:i,engine:s,ms:d,warnings:f,...p}=n;return _(n)+JSON.stringify(p,null,1)}}),ecuc_edit:b({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."+v,args:{ops:k(a.array(k(a.object({op:a.enum(["set","add","remove"]),path:a.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:a.string().optional().describe("parameter/reference def short-name"),value:a.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:a.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:k(a.record(a.string(),a.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:a.boolean().optional().describe("compute and show the diff without writing"),force:a.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)c.push("--dry-run");if(e.force)c.push("--force");let n=await y(g(t.directory,e.project_dir),c,t.abort);if(!n.ok){let f=n.changes??[];return _(n)+w(n)+(f.length?`
8
+ `:""}function g(i,r){if(!r)return i;return ue(r)?r:fe(i,r)}async function he(i){let r=Bun.file(M(i,".hydron","autosar.json"));if(!await r.exists())return{};return r.json().catch((c)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(c)}`)})}async function me(i,r){let c=(await he(i))[r];if(typeof c==="string")return{command:c};if(c&&typeof c==="object"&&typeof c.command==="string")return c;return}function G(i,r=150){let c=i.split(`
9
+ `);return c.length>r?[`\u2026 (${c.length-r} earlier lines omitted)`,...c.slice(-r)].join(`
10
+ `):i}async function ge(i,r,c,e){let s=process.platform==="win32"?["cmd","/d","/s","/c",i]:["sh","-c",i],a=Date.now(),n=Bun.spawn(s,{cwd:r,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([e,AbortSignal.timeout(c*1000)])}),[t,d]=await Promise.all([new Response(n.stdout).text(),new Response(n.stderr).text()]);return{code:await n.exited,out:t,err:d,secs:Math.round((Date.now()-a)/1000)}}var be=(i)=>(i??[]).map((r)=>`CAN ID ${r.canIdHex??"?"} (${r.canId??"?"}) ${r.direction} via ${r.canIfPdu}, controller ${r.controller??"?"}, network ${(r.network??[]).join(", ")||"?"}`).join("; ");async function V(i,r,c){let e=r.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(i,["signal-owners",...new Set(e.map((t)=>t.path))],c);if(!s.ok)return[`NOTE: owning I-PDU not computed locally (${s.error})`];let a=s.owners,n=[];for(let t of e){let d=a[t.path];if(d===null||d===void 0){n.push(`NOTE: ${t.path} is not in the local checkout (${i}); owning I-PDU/direction unknown`);continue}t.ipdu_local=d,n.push(d.length?`${t.path}: owning I-PDU ${d.map((l)=>`${l.ipdu} (ComIPduDirection=${l.direction??"?"}${l.can?.length?`; ${be(l.can)}`:""})`).join(", ")} [local ECUC]`:`${t.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return n}var ye=(i)=>{if(typeof i==="number"&&Number.isSafeInteger(i))return i;if(typeof i==="string"&&/^\s*-?(0x[0-9a-f]+|\d+)\s*$/i.test(i)){let r=i.trim(),c=r.startsWith("-")?-Number(r.slice(1)):Number(r);return Number.isSafeInteger(c)?c:null}return null},X=(i)=>i<0?`-0x${(-i).toString(16).toUpperCase()}`:`0x${i.toString(16).toUpperCase()}`;async function Y(i,r,c){let e=r.filter((t)=>typeof t?.path==="string");if(!e.length)return[];let s=await y(i,["value-sites",...new Set(e.map((t)=>t.path))],c),a=s.ok?s.sites:{},n=[];if(!s.ok)n.push(`NOTE: value lines not computed locally (${s.error})`);for(let t of e){let d=a[t.path];if(d){t.values_local=d.values,n.push(`VALUES ${t.path} (container ${d.at}) [local ECUC; cite these lines]:`);for(let[f,p]of Object.entries(d.values))for(let u of Array.isArray(p)?p:[p])n.push(` ${f} = ${JSON.stringify(u.value)}${u.hex?` (dec ${u.dec}, hex ${u.hex})`:""} @ ${u.at}`);continue}let l=t.values&&typeof t.values==="object"?t.values:{},h={};for(let[f,p]of Object.entries(l)){let u=ye(p);if(u!==null)h[f]=`dec ${u}, hex ${X(u)}`}if(Object.keys(h).length)t.values_int=h;if(s.ok)n.push(`NOTE: ${t.path} is not in the local checkout (${i}): 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 n}var q=(i)=>{let r=i.notes??[],c=i.errors??[];return[...r.map((e)=>`NOTE: ${typeof e==="string"?e:JSON.stringify(e)}`),...c.map((e)=>`BACKEND ERROR: ${typeof e==="string"?e:JSON.stringify(e)}`)]};async function we(i,r){try{let c=await r(),e=typeof c==="string"?c:c.output;if(e&&e.trim())return c;return`${i}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(c){return`ERROR (${i}): ${c instanceof Error?c.message:String(c)}`}}function _e(i){for(let[r,c]of Object.entries(i)){let e=c.execute;c.execute=(s,a)=>we(r,()=>e(s,a))}return i}function $e(i,r=process.env){let c=new Set;for(let e of(r.AUTOSAR_DISABLE_TOOLS??"").split(/[\s,]+/))if(e)c.add(e);if(i)try{let e=JSON.parse(le(M(i,".hydron","autosar.json"),"utf8"));if(Array.isArray(e.disableTools)){for(let s of e.disableTools)if(typeof s==="string"&&s)c.add(s)}}catch{}return c}function Se(i,r){if(r.size===0)return i;return Object.fromEntries(Object.entries(i).filter(([c])=>!r.has(c)))}var Re=async(i)=>{let r=$e(i?.directory),c=(e)=>b({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:o.string().optional().describe("module name substituted for {module} in the command, if used"),args:o.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(s,a){let n=g(a.directory,s.project_dir),t=await me(n,e);if(!t)return`No \`${e}\` command configured. Add it to ${M(n,".hydron","autosar.json")}, e.g.
11
+ {"${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??""),h=l.includes("{args}")?l.replaceAll("{args}",d):`${l}${d?` ${d}`:""}`,f=g(n,t.cwd);await a.ask({permission:`autosar_${e}`,patterns:[h],always:[t.command],metadata:{command:h,cwd:f}}),a.metadata({title:`${e}: ${h}`});let p=await ge(h,f,t.timeoutSec??3600,a.abort),u=p.code===0?"OK":`FAILED (exit ${p.code})`;return{title:`autosar_${e} ${u}`,output:[`$ ${h} (cwd ${f}, ${p.secs}s) -> ${u}`,p.out.trim()?`--- stdout (tail) ---
12
+ ${G(p.out)}`:"",p.err.trim()?`--- stderr (tail) ---
13
+ ${G(p.err,80)}`:""].filter(Boolean).join(`
14
+ `),metadata:{exit:p.code}}}});return{"tool.execute.before":async(e,s)=>{let a=W(e.tool,s.args);if(a)throw Error(a)},"experimental.chat.system.transform":async(e,s)=>{s.system.push(pe)},tool:Se(_e({ecuc_read:b({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."+v,args:{path:o.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:o.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:o.boolean().optional().describe("include the param-def summary for the element"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=["read"];if(e.path)a.push(e.path);if(a.push("--depth",String(e.depth??2)),e.schema)a.push("--schema");let n=await y(g(s.directory,e.project_dir),a,s.abort);if(!n.ok)return S(n)+w(n);let{ok:t,engine:d,ms:l,warnings:h,...f}=n;return S(n)+JSON.stringify(f,null,1)}}),ecuc_edit:b({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."+v,args:{ops:A(o.array(A(o.object({op:o.enum(["set","add","remove"]),path:o.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:o.string().optional().describe("parameter/reference def short-name"),value:o.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:o.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:A(o.record(o.string(),o.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:o.boolean().optional().describe("compute and show the diff without writing"),force:o.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)a.push("--dry-run");if(e.force)a.push("--force");let n=await y(g(s.directory,e.project_dir),a,s.abort);if(!n.ok){let h=n.changes??[];return S(n)+w(n)+(h.length?`
15
15
  Rejected diff:
16
- ${f.map((p)=>p.diff).join(`
17
- `)}`:"")}let i=n.changes??[],s=n.validation,d=[`${n.written?"Written":"Dry run (nothing written)"}: ${n.applied} op(s), ${i.length} file(s) changed.`,...(n.notes??[]).map((f)=>`NOTE: ${f}`),...i.map((f)=>f.diff)];if(s)d.push(s.validated?`Validation of touched files: ${JSON.stringify(s.summary)}`:"Validation skipped: no param defs loaded.",...C(s.diagnostics,50));return{title:`ecuc_edit ${i.map((f)=>f.file).join(", ")}`,output:_(n)+d.join(`
18
- `)}}}),ecuc_validate:b({description:"Validate 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. Returns LSP-like diagnostics "+"file:line severity rule path \u2014 message [def DEFINITION-REF]. Run after EVERY ecuc_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."+v,args:{files:E().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:a.string().optional().describe("only validate this module (e.g. Com); an unknown module is an ERROR, not clean"),max:a.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),checkGenerated:a.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:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=["validate"];for(let u of R(e.files)??[])n.push("--file",u);if(e.module)n.push("--module",e.module);if(e.checkGenerated===!0)n.push("--check-generated");if(e.checkGenerated===!1)n.push("--no-check-generated");let i=await y(c,n,t.abort);if(!i.ok)return _(i)+w(i);let{diagnostics:s,scope:d}=i,f=s.filter((u)=>u.rule!=="H2L-DISCOVERY"),p=i.validated?`Validated ${i.modules.join(", ")||"(no modules)"} against ${i.defsLoaded} module def(s): ${f.length?JSON.stringify(i.summary):"no findings"}`:`Param definitions NOT loaded: only the H2L-* semantic rules ran. ${f.length?JSON.stringify(i.summary):"no findings"}`,l=[`Loaded ${d.configFiles} config file(s)`+(d.filter?`; files filter ${JSON.stringify(d.filter.files)} matched ${d.filter.matchedCount}`:"")+(d.module?`; module filter ${d.module}`:"")+":",...d.files.slice(0,30).map((u)=>` ${u.file} (${u.modules.join(", ")})`),...d.files.length>30||d.filesTruncated?[` \u2026 ${d.files.length-30+(d.filesTruncated??0)} more`]:[]];return{title:`ecuc_validate ${JSON.stringify(i.summary)}`,output:_(i)+[p,...C(s,e.max??200),...l].join(`
19
- `)}}}),arxml_diff:b({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:a.string().describe("old: file, directory (recursive) or glob, relative to the project root or absolute"),b:a.string().describe("new: file, directory (recursive) or glob, relative to the project root or absolute"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=await y(c,["diff",e.a,e.b],t.abort);if(!n.ok)return w(n);let{ok:i,engine:s,ms:d,...f}=n;return JSON.stringify(f,null,1)}}),autosar_trace:b({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."+v,args:{query:a.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:a.number().int().optional().describe("restrict to one code data source"),limit:a.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:a.union([a.string(),a.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:a.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,t){let c=await A(e.workspace_id,t.abort),n={query:e.query};if(e.data_source_id!==void 0)n.data_source_id=e.data_source_id;if(e.limit!==void 0)n.limit=e.limit;let i=await O("/api/v1/mcp/autosar/trace",c,n,t.abort),s=i.matches??[],d=g(t.directory,e.project_dir),f=s.map((m)=>m.arxml).filter((m)=>!!m),[p,l]=await Promise.all([W(d,f,t.abort),G(d,f,t.abort)]),u=[`${s.length} match(es) for "${e.query}" in workspace ${c}`+(i.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(i.data_sources_queried)})`:""),...V(i),...p,...l,JSON.stringify(s,null,1)];if(!s.length)u.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:u.join(`
20
- `)}}}),autosar_config_lookup:b({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."+v,args:{path:a.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:a.number().int().optional().describe("restrict to one code data source"),workspace_id:a.union([a.string(),a.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:a.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,t){let c=await A(e.workspace_id,t.abort),n={path:e.path};if(e.data_source_id!==void 0)n.data_source_id=e.data_source_id;let i=await O("/api/v1/mcp/autosar/config-lookup",c,n,t.abort),s=i.matches??[],d=g(t.directory,e.project_dir),f=s.map((u)=>u.element).filter((u)=>!!u),[p,l]=await Promise.all([W(d,f,t.abort),G(d,f,t.abort)]);return{title:`autosar_config_lookup ${e.path}`,output:[`${s.length} match(es) for ${e.path} in workspace ${c}`,...V(i),...p,...l,JSON.stringify(s,null,1)].join(`
21
- `)}}}),ecuc_find:b({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>".`+v,args:{module:a.string().optional(),container_def:a.string().optional().describe("container definition short-name"),where:a.union([a.string(),a.record(a.string(),a.any())]).optional().describe("conditions on parameter values"),ref_to:a.string().optional().describe("AUTOSAR path / SHORT-NAME that the containers must reference"),referenced_by:a.string().optional().describe("container whose references define the result set"),limit:a.number().int().min(1).max(500).optional(),project_dir:a.string().optional()},async execute(e,t){let c=["find"];if(e.module)c.push("--module",e.module);if(e.container_def)c.push("--container-def",e.container_def);if(e.where)c.push("--where",typeof e.where==="string"?e.where:JSON.stringify(e.where));if(e.ref_to)c.push("--ref-to",e.ref_to);if(e.referenced_by)c.push("--referenced-by",e.referenced_by);c.push("--limit",String(e.limit??50));let n=await y(g(t.directory,e.project_dir),c,t.abort);if(!n.ok)return _(n)+w(n);let{ok:i,engine:s,ms:d,warnings:f,...p}=n;return _(n)+JSON.stringify(p,null,1)}}),autosar_spec_id_check:b({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:E().describe("requirement IDs to check (a list, a JSON-encoded list, or one ID)"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=R(e.ids)??[];if(!c.length)return'ERROR: pass ids, e.g. ["SWS_Com_00197"]';let n=await y(g(t.directory,e.project_dir),["spec-ids",...c],t.abort);if(!n.ok)return w(n);let i=n.results;return{title:`autosar_spec_id_check ${JSON.stringify(n.counts)}`,output:[`Checked ${i.length} ID(s) against ${n.docs} spec text file(s) (${n.docsOrigin}; ${n.indexedIds} IDs indexed): ${JSON.stringify(n.counts)}`,...i.map((s)=>`${s.id} ${s.status}`+(s.doc?` \u2014 ${s.doc}:${s.line}`:"")+(s.didYouMean?` (did you mean ${s.didYouMean}?)`:"")+(s.text?`
22
- ${s.text}`:"")),i.some((s)=>s.status!=="DEFINED")?"Do not cite IDs that are not DEFINED; state the rule without an ID instead.":""].filter(Boolean).join(`
23
- `)}}}),ecuc_crosscheck:b({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."+v,args:{module:a.string().describe("module name, e.g. PduR"),project_dir:a.string().optional()},async execute(e,t){let c=await y(g(t.directory,e.project_dir),["crosscheck",e.module],t.abort);if(!c.ok)return _(c)+w(c);let n=c.rows,i=c.otherFindings??[];return{title:`ecuc_crosscheck ${e.module}: ${c.summary}`,output:[`${e.module}: ${c.summary} (element kinds: ${c.elementKinds.join(", ")||"none"})`,...n.map((s)=>`${s.element} (${s.at})
24
- ${s.findings.join(`
25
- `)}`),...C(i,50),...(c.notes??[]).map((s)=>`NOTE: ${s}`)].join(`
26
- `)}}}),arxml_edit:b({description:"Structured in-place edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces) \u2014 never write that XML by "+"hand. Ops: {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested}}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added 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:k(a.array(k(a.record(a.string(),a.any()))).min(1)).describe("list of ops"),dry_run:a.boolean().optional(),project_dir:a.string().optional()},async execute(e,t){let c=["arxml-edit","--ops",JSON.stringify(N(e.ops))];if(e.dry_run)c.push("--dry-run");let n=await y(g(t.directory,e.project_dir),c,t.abort),i=n.changes??[];if(!n.ok)return w(n)+(n.problems?.length?`
16
+ ${h.map((f)=>f.diff).join(`
17
+ `)}`:"")}let t=n.changes??[],d=n.validation,l=[`${n.written?"Written":"Dry run (nothing written)"}: ${n.applied} op(s), ${t.length} file(s) changed.`,...(n.notes??[]).map((h)=>`NOTE: ${h}`),...t.map((h)=>h.diff)];if(d)l.push(d.validated?`Validation of touched files: ${JSON.stringify(d.summary)}`:"Validation skipped: no param defs loaded.",...N(d.diagnostics,50));return{title:`ecuc_edit ${t.map((h)=>h.file).join(", ")}`,output:S(n)+l.join(`
18
+ `)}}}),ecuc_validate:b({description:"Validate 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. Returns LSP-like diagnostics "+"file:line severity rule path \u2014 message [def DEFINITION-REF]. Run after EVERY ecuc_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."+v,args:{files:k().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:o.string().optional().describe("only validate this module (e.g. Com); an unknown module is an ERROR, not clean"),max:o.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),checkGenerated:o.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:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=["validate"];for(let u of E(e.files)??[])n.push("--file",u);if(e.module)n.push("--module",e.module);if(e.checkGenerated===!0)n.push("--check-generated");if(e.checkGenerated===!1)n.push("--no-check-generated");let t=await y(a,n,s.abort);if(!t.ok)return S(t)+w(t);let{diagnostics:d,scope:l}=t,h=d.filter((u)=>u.rule!=="H2L-DISCOVERY"),f=t.validated?`Validated ${t.modules.join(", ")||"(no modules)"} against ${t.defsLoaded} module def(s): ${h.length?JSON.stringify(t.summary):"no findings"}`:`Param definitions NOT loaded: only the H2L-* semantic rules ran. ${h.length?JSON.stringify(t.summary):"no findings"}`,p=[`Loaded ${l.configFiles} config 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((u)=>` ${u.file} (${u.modules.join(", ")})`),...l.files.length>30||l.filesTruncated?[` \u2026 ${l.files.length-30+(l.filesTruncated??0)} more`]:[]];return{title:`ecuc_validate ${JSON.stringify(t.summary)}`,output:S(t)+[f,...N(d,e.max??200),...p].join(`
19
+ `)}}}),arxml_diff:b({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:o.string().describe("old: file, directory (recursive) or glob, relative to the project root or absolute"),b:o.string().describe("new: file, directory (recursive) or glob, relative to the project root or absolute"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=await y(a,["diff",e.a,e.b],s.abort);if(!n.ok)return w(n);let{ok:t,engine:d,ms:l,...h}=n;return JSON.stringify(h,null,1)}}),autosar_trace:b({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."+v,args:{query:o.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:o.number().int().optional().describe("restrict to one code data source"),limit:o.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:o.union([o.string(),o.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:o.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,s){let a=await O(e.workspace_id,s.abort),n={query:e.query};if(e.data_source_id!==void 0)n.data_source_id=e.data_source_id;if(e.limit!==void 0)n.limit=e.limit;let t=await j("/api/v1/mcp/autosar/trace",a,n,s.abort),d=t.matches??[],l=g(s.directory,e.project_dir),h=d.map((_)=>_.arxml).filter((_)=>!!_),[f,p]=await Promise.all([V(l,h,s.abort),Y(l,h,s.abort)]),u=[`${d.length} match(es) for "${e.query}" in workspace ${a}`+(t.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(t.data_sources_queried)})`:""),...q(t),...f,...p,JSON.stringify(d,null,1)];if(!d.length)u.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:u.join(`
20
+ `)}}}),autosar_config_lookup:b({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."+v,args:{path:o.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:o.number().int().optional().describe("restrict to one code data source"),workspace_id:o.union([o.string(),o.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:o.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(e,s){let a=await O(e.workspace_id,s.abort),n={path:e.path};if(e.data_source_id!==void 0)n.data_source_id=e.data_source_id;let t=await j("/api/v1/mcp/autosar/config-lookup",a,n,s.abort),d=t.matches??[],l=g(s.directory,e.project_dir),h=d.map((u)=>u.element).filter((u)=>!!u),[f,p]=await Promise.all([V(l,h,s.abort),Y(l,h,s.abort)]);return{title:`autosar_config_lookup ${e.path}`,output:[`${d.length} match(es) for ${e.path} in workspace ${a}`,...q(t),...f,...p,JSON.stringify(d,null,1)].join(`
21
+ `)}}}),ecuc_find:b({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>".`+v,args:{module:o.string().optional(),container_def:o.string().optional().describe("container definition short-name"),where:o.union([o.string(),o.record(o.string(),o.any())]).optional().describe("conditions on parameter values"),ref_to:o.string().optional().describe("AUTOSAR path / SHORT-NAME that the containers must reference"),referenced_by:o.string().optional().describe("container whose references define the result set"),limit:o.number().int().min(1).max(500).optional(),project_dir:o.string().optional()},async execute(e,s){let a=["find"];if(e.module)a.push("--module",e.module);if(e.container_def)a.push("--container-def",e.container_def);if(e.where)a.push("--where",typeof e.where==="string"?e.where:JSON.stringify(e.where));if(e.ref_to)a.push("--ref-to",e.ref_to);if(e.referenced_by)a.push("--referenced-by",e.referenced_by);a.push("--limit",String(e.limit??50));let n=await y(g(s.directory,e.project_dir),a,s.abort);if(!n.ok)return S(n)+w(n);let{ok:t,engine:d,ms:l,warnings:h,...f}=n;return S(n)+JSON.stringify(f,null,1)}}),autosar_spec_id_check:b({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:k().describe("requirement IDs to check (a list, a JSON-encoded list, or one ID)"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=E(e.ids)??[];if(!a.length)return'ERROR: pass ids, e.g. ["SWS_Com_00197"]';let n=await y(g(s.directory,e.project_dir),["spec-ids",...a],s.abort);if(!n.ok)return w(n);let t=n.results;return{title:`autosar_spec_id_check ${JSON.stringify(n.counts)}`,output:[`Checked ${t.length} ID(s) against ${n.docs} spec text file(s) (${n.docsOrigin}; ${n.indexedIds} IDs indexed): ${JSON.stringify(n.counts)}`,...t.map((d)=>`${d.id} ${d.status}`+(d.doc?` \u2014 ${d.doc}:${d.line}`:"")+(d.didYouMean?` (did you mean ${d.didYouMean}?)`:"")+(d.text?`
22
+ ${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(`
23
+ `)}}}),ecuc_crosscheck:b({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."+v,args:{module:o.string().describe("module name, e.g. PduR"),project_dir:o.string().optional()},async execute(e,s){let a=await y(g(s.directory,e.project_dir),["crosscheck",e.module],s.abort);if(!a.ok)return S(a)+w(a);let n=a.rows,t=a.otherFindings??[];return{title:`ecuc_crosscheck ${e.module}: ${a.summary}`,output:[`${e.module}: ${a.summary} (element kinds: ${a.elementKinds.join(", ")||"none"})`,...n.map((d)=>`${d.element} (${d.at})
24
+ ${d.findings.join(`
25
+ `)}`),...N(t,50),...(a.notes??[]).map((d)=>`NOTE: ${d}`)].join(`
26
+ `)}}}),arxml_edit:b({description:"Structured in-place edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces) \u2014 never write that XML by "+"hand. Ops: {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested}}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added 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(o.array(A(o.record(o.string(),o.any()))).min(1)).describe("list of ops"),dry_run:o.boolean().optional(),project_dir:o.string().optional()},async execute(e,s){let a=["arxml-edit","--ops",JSON.stringify(T(e.ops))];if(e.dry_run)a.push("--dry-run");let n=await y(g(s.directory,e.project_dir),a,s.abort),t=n.changes??[];if(!n.ok)return w(n)+(n.problems?.length?`
27
27
  ${n.problems.join(`
28
- `)}`:"")+(i.length?`
28
+ `)}`:"")+(t.length?`
29
29
  Rejected diff:
30
- ${i.map((s)=>s.diff).join(`
30
+ ${t.map((d)=>d.diff).join(`
31
31
  `)}`:"");return`${n.written?"Written":"Dry run"}; created ${JSON.stringify(n.created)}
32
- ${i.map((s)=>s.diff).join(`
32
+ ${t.map((d)=>d.diff).join(`
33
33
  `)}
34
- Next: ecuc_validate.`}}),autosar_add_signal:b({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:a.string(),ipdu:a.string().describe("ComIPdu AUTOSAR path or SHORT-NAME"),bit_size:a.number().int().min(1).max(64).optional(),type:a.string().optional().describe("ComSignalType, e.g. UINT8 (default)"),bit_position:a.number().int().min(0).optional().describe("omit for the first free byte-aligned position"),endianness:a.enum(["LITTLE_ENDIAN","BIG_ENDIAN","OPAQUE"]).optional(),init:a.union([a.number(),a.string()]).optional(),transfer_property:a.string().optional().describe("e.g. PENDING, TRIGGERED, TRIGGERED_ON_CHANGE"),dry_run:a.boolean().optional(),project_dir:a.string().optional()},async execute(e,t){let c=["add-signal","--name",e.name,"--ipdu",e.ipdu,"--type",e.type??"UINT8"];if(e.bit_size!==void 0)c.push("--bit-size",String(e.bit_size));if(e.bit_position!==void 0)c.push("--bit-position",String(e.bit_position));if(e.endianness)c.push("--endianness",e.endianness);if(e.init!==void 0)c.push("--init",String(e.init));if(e.transfer_property)c.push("--transfer-property",e.transfer_property);if(e.dry_run)c.push("--dry-run");let n=await y(g(t.directory,e.project_dir),c,t.abort);if(!n.ok)return w(n)+(n.diagnostics?.length?`
35
- ${C(n.diagnostics,20).join(`
36
- `)}`:"");let{ok:i,engine:s,ms:d,...f}=n;return JSON.stringify(f,null,1)}}),autosar_audit:b({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:E().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:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=await y(c,["audit",...R(e.paths)??[]],t.abort);if(!n.ok)return _(n)+w(n);let i=(n.notes??[]).map((u)=>`NOTE: ${u}`),{findings:s,unchecked:d,scanned:f}=n,p=[`Scanned ${f.sources.length} C file(s), ${f.calls} BSW call(s): ${s.length?JSON.stringify(n.summary):"no findings"}`,...s.map((u)=>`${u.file}:${u.line} ${u.severity.toUpperCase()} ${u.rule} ${u.call}
34
+ Next: ecuc_validate.`}}),autosar_add_signal:b({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:o.string(),ipdu:o.string().describe("ComIPdu AUTOSAR path or SHORT-NAME"),bit_size:o.number().int().min(1).max(64).optional(),type:o.string().optional().describe("ComSignalType, e.g. UINT8 (default)"),bit_position:o.number().int().min(0).optional().describe("omit for the first free byte-aligned position"),endianness:o.enum(["LITTLE_ENDIAN","BIG_ENDIAN","OPAQUE"]).optional(),init:o.union([o.number(),o.string()]).optional(),transfer_property:o.string().optional().describe("e.g. PENDING, TRIGGERED, TRIGGERED_ON_CHANGE"),dry_run:o.boolean().optional(),project_dir:o.string().optional()},async execute(e,s){let a=["add-signal","--name",e.name,"--ipdu",e.ipdu,"--type",e.type??"UINT8"];if(e.bit_size!==void 0)a.push("--bit-size",String(e.bit_size));if(e.bit_position!==void 0)a.push("--bit-position",String(e.bit_position));if(e.endianness)a.push("--endianness",e.endianness);if(e.init!==void 0)a.push("--init",String(e.init));if(e.transfer_property)a.push("--transfer-property",e.transfer_property);if(e.dry_run)a.push("--dry-run");let n=await y(g(s.directory,e.project_dir),a,s.abort);if(!n.ok)return w(n)+(n.diagnostics?.length?`
35
+ ${N(n.diagnostics,20).join(`
36
+ `)}`:"");let{ok:t,engine:d,ms:l,...h}=n;return JSON.stringify(h,null,1)}}),autosar_audit:b({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:k().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:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=await y(a,["audit",...E(e.paths)??[]],s.abort);if(!n.ok)return S(n)+w(n);let t=(n.notes??[]).map((u)=>`NOTE: ${u}`),{findings:d,unchecked:l,scanned:h}=n,f=[`Scanned ${h.sources.length} C file(s), ${h.calls} BSW call(s): ${d.length?JSON.stringify(n.summary):"no findings"}`,...d.map((u)=>`${u.file}:${u.line} ${u.severity.toUpperCase()} ${u.rule} ${u.call}
37
37
  ${u.message}
38
38
  evidence: ${JSON.stringify(u.evidence)}
39
39
  spec: ${u.cite}${u.fix?`
40
- fix: ${u.fix}`:""}`)];if(d.length)p.push(`Unchecked (${d.length}):`,...d.slice(0,50).map((u)=>` ${u.file}:${u.line} ${u.call} \u2014 ${u.reason}`));let l=n.unreferenced??{};for(let[u,m]of Object.entries(l))p.push(`INFO configured but never referenced in the scanned code \u2014 ${u}: ${m.count} (e.g. ${m.sample.slice(0,5).join(", ")})`);return p.push(...i),{title:`autosar_audit ${JSON.stringify(n.summary)}`,output:_(n)+p.join(`
41
- `)}}}),autosar_verify_rte:b({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:E().describe("C files, directories or globs (default: all in-house C)"),diff_against:a.string().optional().describe("git ref: only calls on lines changed since this ref (plus untracked files)"),swc:a.string().optional().describe("restrict to one SW-C (short name or AUTOSAR path)"),runnable:a.string().optional().describe("restrict to one runnable (short name or SYMBOL)"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=["verify-rte",...R(e.paths)??[]];if(e.diff_against)n.push("--diff-against",e.diff_against);if(e.swc)n.push("--swc",e.swc);if(e.runnable)n.push("--runnable",e.runnable);let i=await y(c,n,t.abort);if(!i.ok)return w(i);let{findings:s,counts:d}=i,f=s.filter((h)=>h.status!=="OK"),p=i.scanned,l=[`Verified ${p.calls} Rte_* call(s) in ${p.files.length} file(s) against SW-C(s) ${i.swcs.join(", ")}: OK ${d.OK}, NOT-ALLOWED ${d["NOT-ALLOWED"]}, INVENTED ${d.INVENTED}`+(f.length?" \u2014 FAILING":" \u2014 PASS")];for(let h of f){if(l.push(`${h.at} ${h.status} ${h.call} in ${h.function}() [${h.swc??"?"}${h.runnable?`/${h.runnable}`:""}]`,` ${h.why}`),h.expected)l.push(` expected: ${h.expected} [${h.cite}]`);if(h.hint)l.push(` hint: ${h.hint}`);if(h.arxml_edit?.length)l.push(` fix (arxml_edit ops${h.opsChecked===!0?", dry-run OK":h.opsChecked?`, dry-run FAILED: ${h.opsChecked}`:""}): ${JSON.stringify(h.arxml_edit)}`)}let u=i.notChecked??[];if(u.length)l.push(`Not checked (${u.length}): ${u.map((h)=>`${h.call} @ ${h.at}`).join(", ")}`);let m=i.runnablesWithoutCode??[];if(m.length)l.push(`Runnables without a C definition: ${m.map((h)=>`${h.swc}.${h.runnable} (${h.symbol})`).join(", ")}`);for(let h of i.notes??[])l.push(`NOTE: ${h}`);if(f.length)l.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(d)}`,output:l.join(`
42
- `),metadata:{counts:d}}}}),autosar_build_context:b({description:"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.",args:{swc:a.string().describe("SW-C short name or AUTOSAR path"),runnable:a.string().optional().describe("runnable short name or SYMBOL (default: all runnables)"),task:a.string().describe("the task, verbatim"),budget_tokens:a.number().int().min(500).max(200000).optional().describe("token budget (default 24000)"),use_backend:a.boolean().optional().describe("if the runnable has no local C definition, look it up with the backend trace of the selected workspace"),workspace_id:a.union([a.string(),a.number()]).optional().describe("H2Loop project id or name for use_backend"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=["build-context","--swc",e.swc,"--task",e.task,"--budget-tokens",String(e.budget_tokens??24000)];if(e.runnable)n.push("--runnable",e.runnable);let i=await y(c,n,t.abort);if(!i.ok)return w(i);let s=i.text,d=i.codeMissing??[];if(d.length&&e.use_backend){let p=[];try{let l=await A(e.workspace_id,t.abort);for(let u of d){let m=await O("/api/v1/mcp/autosar/trace",l,{query:u,limit:3},t.abort);p.push(`### backend trace for ${u} (workspace ${l})
43
- ${JSON.stringify(m.matches??[],null,1).slice(0,6000)}`)}}catch(l){p.push(`(backend lookup unavailable: ${l instanceof Error?l.message:String(l)})`)}s+=`
40
+ fix: ${u.fix}`:""}`)];if(l.length)f.push(`Unchecked (${l.length}):`,...l.slice(0,50).map((u)=>` ${u.file}:${u.line} ${u.call} \u2014 ${u.reason}`));let p=n.unreferenced??{};for(let[u,_]of Object.entries(p))f.push(`INFO configured but never referenced in the scanned code \u2014 ${u}: ${_.count} (e.g. ${_.sample.slice(0,5).join(", ")})`);return f.push(...t),{title:`autosar_audit ${JSON.stringify(n.summary)}`,output:S(n)+f.join(`
41
+ `)}}}),autosar_verify_rte:b({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:k().describe("C files, directories or globs (default: all in-house C)"),diff_against:o.string().optional().describe("git ref: only calls on lines changed since this ref (plus untracked files)"),swc:o.string().optional().describe("restrict to one SW-C (short name or AUTOSAR path)"),runnable:o.string().optional().describe("restrict to one runnable (short name or SYMBOL)"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=["verify-rte",...E(e.paths)??[]];if(e.diff_against)n.push("--diff-against",e.diff_against);if(e.swc)n.push("--swc",e.swc);if(e.runnable)n.push("--runnable",e.runnable);let t=await y(a,n,s.abort);if(!t.ok)return w(t);let{findings:d,counts:l}=t,h=d.filter((m)=>m.status!=="OK"),f=t.scanned,p=[`Verified ${f.calls} Rte_* call(s) in ${f.files.length} file(s) against SW-C(s) ${t.swcs.join(", ")}: OK ${l.OK}, NOT-ALLOWED ${l["NOT-ALLOWED"]}, INVENTED ${l.INVENTED}`+(h.length?" \u2014 FAILING":" \u2014 PASS")];for(let m of h){if(p.push(`${m.at} ${m.status} ${m.call} in ${m.function}() [${m.swc??"?"}${m.runnable?`/${m.runnable}`:""}]`,` ${m.why}`),m.expected)p.push(` expected: ${m.expected} [${m.cite}]`);if(m.hint)p.push(` hint: ${m.hint}`);if(m.arxml_edit?.length)p.push(` fix (arxml_edit ops${m.opsChecked===!0?", dry-run OK":m.opsChecked?`, dry-run FAILED: ${m.opsChecked}`:""}): ${JSON.stringify(m.arxml_edit)}`)}let u=t.notChecked??[];if(u.length)p.push(`Not checked (${u.length}): ${u.map((m)=>`${m.call} @ ${m.at}`).join(", ")}`);let _=t.runnablesWithoutCode??[];if(_.length)p.push(`Runnables without a C definition: ${_.map((m)=>`${m.swc}.${m.runnable} (${m.symbol})`).join(", ")}`);for(let m of t.notes??[])p.push(`NOTE: ${m}`);if(h.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 ${h.length?"FAIL":"PASS"} ${JSON.stringify(l)}`,output:p.join(`
42
+ `),metadata:{counts:l}}}}),autosar_build_context:b({description:"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.",args:{swc:o.string().describe("SW-C short name or AUTOSAR path"),runnable:o.string().optional().describe("runnable short name or SYMBOL (default: all runnables)"),task:o.string().describe("the task, verbatim"),budget_tokens:o.number().int().min(500).max(200000).optional().describe("token budget (default 24000)"),use_backend:o.boolean().optional().describe("if the runnable has no local C definition, look it up with the backend trace of the selected workspace"),workspace_id:o.union([o.string(),o.number()]).optional().describe("H2Loop project id or name for use_backend"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=["build-context","--swc",e.swc,"--task",e.task,"--budget-tokens",String(e.budget_tokens??24000)];if(e.runnable)n.push("--runnable",e.runnable);let t=await y(a,n,s.abort);if(!t.ok)return w(t);let d=t.text,l=t.codeMissing??[];if(l.length&&e.use_backend){let f=[];try{let p=await O(e.workspace_id,s.abort);for(let u of l){let _=await j("/api/v1/mcp/autosar/trace",p,{query:u,limit:3},s.abort);f.push(`### backend trace for ${u} (workspace ${p})
43
+ ${JSON.stringify(_.matches??[],null,1).slice(0,6000)}`)}}catch(p){f.push(`(backend lookup unavailable: ${p instanceof Error?p.message:String(p)})`)}d+=`
44
44
  ## 4b. RUNNABLE CODE FROM THE BACKEND (not part of the deterministic context)
45
- ${p.join(`
45
+ ${f.join(`
46
46
  `)}
47
- `}let f={swc:i.swc,runnables:i.runnables,estimatedTokens:i.estimatedTokens,budgetTokens:i.budgetTokens,reductions:i.reductions,overBudget:i.overBudget??!1,sha1:i.sha1,sections:i.sections,sources:i.manifest};return{title:`autosar_build_context ${i.swc}${i.runnables.length===1?`/${i.runnables[0]}`:""} ~${i.estimatedTokens} tok`,output:`${s}
47
+ `}let h={swc:t.swc,runnables:t.runnables,estimatedTokens:t.estimatedTokens,budgetTokens:t.budgetTokens,reductions:t.reductions,overBudget:t.overBudget??!1,sha1:t.sha1,sections:t.sections,sources:t.manifest};return{title:`autosar_build_context ${t.swc}${t.runnables.length===1?`/${t.runnables[0]}`:""} ~${t.estimatedTokens} tok`,output:`${d}
48
48
  ---
49
- MANIFEST ${JSON.stringify(f)}`,metadata:{sha1:i.sha1,estimatedTokens:i.estimatedTokens}}}}),autosar_codegen:b({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:E().describe("modules to generate (default: every supported module that is configured)"),out_dir:a.string().optional().describe("output dir, relative to the project or absolute; 'project' = codegen.outDir / Output. Default: scratch .hydron/codegen/out"),write_back:a.boolean().optional().describe("write assigned handle IDs into the ECUC ARXML (default true)"),binding:a.string().optional().describe("force a static-code binding (e.g. 'sip-standin'), or 'none' for generic tables only"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=["codegen"];for(let l of R(e.modules)??[])n.push("--module",l);if(e.out_dir)n.push("--out",e.out_dir);if(e.write_back===!1)n.push("--no-write-back");if(e.binding)n.push("--binding",e.binding);let i=await y(c,n,t.abort);if(!i.ok){let l=i.handleErrors??[];return _(i)+w(i)+(l.length?`
50
- ${l.map((u)=>` ${u}`).join(`
51
- `)}`:"")}let{modules:s,handles:d,timings:f}=i,p=[`Generated ${Object.keys(s).length} module(s) into ${i.outDir} in ${f.total_ms} ms (AUTOSAR R${i.release}):`,...Object.entries(s).map(([l,u])=>` ${l} (${u.variant}${u.binding?`, binding ${u.binding}`:", generic tables"}): ${u.files.join(", ")}; ${u.symbols} symbol(s)`+u.notes.map((m)=>`
52
- NOTE: ${m}`).join("")),d.assigned.length?`Assigned ${d.assigned.length} handle ID(s) (${d.writtenBack?"written back into the ECUC ARXML":"in memory only, NOT written back"}):
53
- `+d.assigned.slice(0,40).map((l)=>` ${l.path}/${l.param} = ${l.value} (hex ${q(l.value)})`).join(`
54
- `)+(d.changes.length?`
55
- ${d.changes.map((l)=>l.diff).join(`
56
- `)}`:""):"Handle IDs: all already present in the ECUC (nothing assigned).",...(i.notes??[]).map((l)=>`NOTE: ${l}`)];return{title:`autosar_codegen ${Object.keys(s).join(", ")}`,output:_(i)+p.join(`
57
- `)}}}),autosar_codegen_diff:b({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:a.string().describe("reference code dir, e.g. Output (relative to the project or absolute)"),generated_dir:a.string().optional().describe("generated code dir (default: the autosar_codegen scratch dir)"),modules:E().describe("only these modules"),project_dir:a.string().optional().describe("project root (default: session directory)")},async execute(e,t){let c=g(t.directory,e.project_dir),n=["codegen-diff",e.reference_dir];if(e.generated_dir)n.push(e.generated_dir);for(let f of R(e.modules)??[])n.push("--module",f);let i=await y(c,n,t.abort);if(!i.ok)return w(i);let s=i.modules,d=[`Semantic diff ${i.reference} (reference) vs ${i.generated} (generated): ${JSON.stringify(i.total)}`,`Not generated (reference only): ${i.notGenerated.join(", ")||"-"}; not in reference: ${i.notInReference.join(", ")||"-"}`];for(let[f,p]of Object.entries(s)){if(d.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)d.push(` missing #define: ${p.defines.missing.slice(0,30).join(", ")}`);for(let l of p.defines.mismatch.slice(0,30))d.push(` #define ${l.name}: reference ${l.reference} | generated ${l.generated}`);if(p.objects.missing.length)d.push(` missing object: ${p.objects.missing.join(", ")}`);if(p.objects.extra.length)d.push(` extra object: ${p.objects.extra.join(", ")}`);for(let l of p.objects.order)d.push(` order differs: ${l.name} (same rows)`);for(let l of p.objects.mismatch)d.push(` ${l.name}: ${l.count} element(s) differ`,...l.elements.slice(0,12).map((u)=>` ${u.element}: reference ${u.reference} | generated ${u.generated}`));if(p.defines.extra.length)d.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(i.total)}`,output:d.join(`
58
- `)}}}),autosar_generate:o("generate"),autosar_build:o("build")})}},_e=we;export{_e as Autosar};
49
+ MANIFEST ${JSON.stringify(h)}`,metadata:{sha1:t.sha1,estimatedTokens:t.estimatedTokens}}}}),autosar_codegen:b({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:k().describe("modules to generate (default: every supported module that is configured)"),out_dir:o.string().optional().describe("output dir, relative to the project or absolute; 'project' = codegen.outDir / Output. Default: scratch .hydron/codegen/out"),write_back:o.boolean().optional().describe("write assigned handle IDs into the ECUC ARXML (default true)"),binding:o.string().optional().describe("force a static-code binding (e.g. 'sip-standin'), or 'none' for generic tables only"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=["codegen"];for(let p of E(e.modules)??[])n.push("--module",p);if(e.out_dir)n.push("--out",e.out_dir);if(e.write_back===!1)n.push("--no-write-back");if(e.binding)n.push("--binding",e.binding);let t=await y(a,n,s.abort);if(!t.ok){let p=t.handleErrors??[];return S(t)+w(t)+(p.length?`
50
+ ${p.map((u)=>` ${u}`).join(`
51
+ `)}`:"")}let{modules:d,handles:l,timings:h}=t,f=[`Generated ${Object.keys(d).length} module(s) into ${t.outDir} in ${h.total_ms} ms (AUTOSAR R${t.release}):`,...Object.entries(d).map(([p,u])=>` ${p} (${u.variant}${u.binding?`, binding ${u.binding}`:", generic tables"}): ${u.files.join(", ")}; ${u.symbols} symbol(s)`+u.notes.map((_)=>`
52
+ 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"}):
53
+ `+l.assigned.slice(0,40).map((p)=>` ${p.path}/${p.param} = ${p.value} (hex ${X(p.value)})`).join(`
54
+ `)+(l.changes.length?`
55
+ ${l.changes.map((p)=>p.diff).join(`
56
+ `)}`:""):"Handle IDs: all already present in the ECUC (nothing assigned).",...(t.notes??[]).map((p)=>`NOTE: ${p}`)];return{title:`autosar_codegen ${Object.keys(d).join(", ")}`,output:S(t)+f.join(`
57
+ `)}}}),autosar_codegen_diff:b({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:o.string().describe("reference code dir, e.g. Output (relative to the project or absolute)"),generated_dir:o.string().optional().describe("generated code dir (default: the autosar_codegen scratch dir)"),modules:k().describe("only these modules"),project_dir:o.string().optional().describe("project root (default: session directory)")},async execute(e,s){let a=g(s.directory,e.project_dir),n=["codegen-diff",e.reference_dir];if(e.generated_dir)n.push(e.generated_dir);for(let h of E(e.modules)??[])n.push("--module",h);let t=await y(a,n,s.abort);if(!t.ok)return w(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[h,f]of Object.entries(d)){if(l.push(`== ${h}: ${f.summary.matching} matching, ${f.summary.missing} missing, ${f.summary.extra} extra, ${f.summary.mismatches} mismatch(es); table elements ${f.elements.match} equal / ${f.elements.mismatch} different`),f.defines.missing.length)l.push(` missing #define: ${f.defines.missing.slice(0,30).join(", ")}`);for(let p of f.defines.mismatch.slice(0,30))l.push(` #define ${p.name}: reference ${p.reference} | generated ${p.generated}`);if(f.objects.missing.length)l.push(` missing object: ${f.objects.missing.join(", ")}`);if(f.objects.extra.length)l.push(` extra object: ${f.objects.extra.join(", ")}`);for(let p of f.objects.order)l.push(` order differs: ${p.name} (same rows)`);for(let p of f.objects.mismatch)l.push(` ${p.name}: ${p.count} element(s) differ`,...p.elements.slice(0,12).map((u)=>` ${u.element}: reference ${u.reference} | generated ${u.generated}`));if(f.defines.extra.length)l.push(` extra #define (${f.defines.extra.length}): ${f.defines.extra.slice(0,12).join(", ")}${f.defines.extra.length>12?", \u2026":""}`)}return{title:`autosar_codegen_diff ${JSON.stringify(t.total)}`,output:l.join(`
58
+ `)}}}),autosar_generate:c("generate"),autosar_build:c("build")}),r)}},ve=Re;export{ve as Autosar};
@@ -18,7 +18,7 @@ import re
18
18
  from typing import Optional
19
19
 
20
20
  from . import paths
21
- from .arxml_edit import ArxmlEditError, ArxmlEditor
21
+ from .arxml_edit import ArxmlEditError, ArxmlEditor, _ad, _etype, _version
22
22
  from .model import Project
23
23
  from .rules import RuleContext
24
24
  from .rules.com import _layout, bits
@@ -102,11 +102,11 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
102
102
  # ---------- system extract ----------
103
103
  sysi = project.system
104
104
  ref_pv = next((pv for g in project.find(sib.path) for pv in g.params if pv.is_ref and sysi.get(pv.value or "")), None)
105
- sys_writes, new_mapping = {}, None
105
+ sys_writes, new_mapping, not_cloned = {}, None, []
106
106
  if ref_pv is not None and sysi.by_path:
107
107
  ed = ArxmlEditor(project)
108
108
  try:
109
- new_mapping = _clone_system_chain(project, ed, sysi.get(ref_pv.value), sib.name, name)
109
+ new_mapping = _clone_system_chain(project, ed, sysi.get(ref_pv.value), sib.name, name, not_cloned)
110
110
  m_path = new_mapping
111
111
  ed.op_set({"path": m_path, "element": "START-POSITION", "value": pos})
112
112
  ed.op_set({"path": m_path, "element": "PACKING-BYTE-ORDER", "value": ORDER.get(endianness, endianness)})
@@ -127,6 +127,11 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
127
127
  if probs:
128
128
  raise AddSignalError("system extract edit would leave: " + "; ".join(probs[:5]))
129
129
  sys_writes = {f: ft.data for f, ft in ed.files.items() if ft.data != ed.original[f]}
130
+ for sk in not_cloned:
131
+ notes.append(f"not copied from {sib.name}: its {sk['ref']} in {sk['element']} ({sk['holder']}) is a "
132
+ f"single-valued reference of the template's own relation. If {name} needs the same kind of "
133
+ f"relation (e.g. a SWC data element mapped to it), add a new {sk['element']} with "
134
+ f"{sk['ref']} = {sk['new']} (arxml_edit)")
130
135
  else:
131
136
  notes.append("no system-extract link on the template signal; extract not changed")
132
137
 
@@ -188,7 +193,8 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
188
193
  ecuc_writes = {f: project.files[f].data for f in ced.original if project.files[f].data != ced.original[f]}
189
194
 
190
195
  # ---------- DBC ----------
191
- dbc_writes = _dbc_line(project, pdu, name, pos, size, endianness, signal_type, notes)
196
+ dbc_writes = _dbc_line(project, pdu, name, pos, size, endianness, signal_type, notes,
197
+ {m.name for m in members if m is not None})
192
198
 
193
199
  writes = {**sys_writes, **ecuc_writes, **dbc_writes}
194
200
  originals = {f: open(f, "rb").read() for f in writes}
@@ -200,7 +206,8 @@ def add_signal(project: Project, name: str, ipdu: str, bit_size: Optional[int] =
200
206
  for f, b in writes.items():
201
207
  with open(f, "wb") as fh:
202
208
  fh.write(b)
203
- after = validate(Project(project.root), check_generated=False)
209
+ # same inputs as `before` (explicit --defs/--config included), so only the edit's own findings count as new
210
+ after = validate(Project(project.root, project.sources.config, project.sources.defs), check_generated=False)
204
211
  new = [d for d in after["diagnostics"] if d["severity"] in ("error", "warning")
205
212
  and (d["rule"], d["path"], d["message"]) not in before]
206
213
  if new:
@@ -247,8 +254,24 @@ def _renamed(path, old: str, new: str) -> Optional[str]:
247
254
  return f"{head}/{last.replace(old, new)}" if old in last else None
248
255
 
249
256
 
250
- def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, new: str) -> str:
251
- """Clone the sibling's extract chain under the new name; return the new mapping path."""
257
+ def _repeatable(ft, wrap) -> bool:
258
+ """May wrap's parent hold another element like wrap? Schema content mode when autosar-data is available
259
+ (Bag = collection, Sequence/Choice = single slots); otherwise AUTOSAR's plural-wrapper naming or an existing
260
+ repetition of the same tag."""
261
+ parent = wrap.parent
262
+ if parent is None:
263
+ return False
264
+ et = _etype(ft.chain(parent), _version(ft.data))
265
+ if et is not None:
266
+ return et.content_mode == _ad().ContentMode.Bag
267
+ return parent.tag.endswith("S") or sum(c.tag == wrap.tag for c in parent.children) > 1
268
+
269
+
270
+ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, new: str,
271
+ skipped: Optional[list] = None) -> str:
272
+ """Clone the sibling's extract chain under the new name; return the new mapping path. Reference entries
273
+ that cannot be duplicated (single-valued slots) are appended to `skipped` instead."""
274
+ skipped = [] if skipped is None else skipped
252
275
  sysi = project.system
253
276
  if mapping is None or mapping.tag != "I-SIGNAL-TO-I-PDU-MAPPING":
254
277
  raise ArxmlEditError("template signal's system reference is not an I-SIGNAL-TO-I-PDU-MAPPING")
@@ -293,7 +316,10 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
293
316
  ed.patch(ft, node.end, node.end, ft.nl + ind + text.encode())
294
317
  if p == mapping.path:
295
318
  mapping_new = subst[p]
296
- # reference entries held by shared elements (not renamed): clone the wrapper next to the original
319
+ # reference entries held by shared elements (not renamed): clone the wrapper next to the original, but only
320
+ # where the holder is a collection (FIBEX-ELEMENTS, I-SIGNAL-TRIGGERINGS, ...). A reference in a single-valued
321
+ # slot (SYSTEM-SIGNAL-REF of a SENDER-RECEIVER-TO-SIGNAL-MAPPING) belongs to the sibling's own relation, e.g.
322
+ # its SWC data element; duplicating it is a schema error and the relation does not carry over to a new signal.
297
323
  for p in chain:
298
324
  for r in sysi.refs_to(p):
299
325
  if r.path in chain:
@@ -306,6 +332,10 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
306
332
  while wrap.parent is not None and wrap.parent is not node and len(wrap.parent.children) == 1 \
307
333
  and not wrap.parent.tag.endswith("S"):
308
334
  wrap = wrap.parent
335
+ if not _repeatable(ft, wrap):
336
+ skipped.append({"holder": r.path, "element": wrap.parent.tag if wrap.parent is not None else node.tag,
337
+ "ref": leaf.tag, "template": p, "new": subst[p]})
338
+ continue
309
339
  text = ft.data[wrap.start:wrap.end].decode().replace(f">{p}<", f">{subst[p]}<")
310
340
  ind = line_indent(ft.data, wrap.start) or b""
311
341
  ed.patch(ft, wrap.end, wrap.end, ft.nl + ind + text.encode())
@@ -314,7 +344,15 @@ def _clone_system_chain(project: Project, ed: ArxmlEditor, mapping, old: str, ne
314
344
  return mapping_new
315
345
 
316
346
 
317
- def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes) -> dict:
347
+ def _frame_signals(text: str, m) -> set:
348
+ body = text[m.end():].split("\n\n", 1)[0]
349
+ return set(re.findall(r"^\s*SG_ (\w+)", body, re.M))
350
+
351
+
352
+ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes, members: set = frozenset()) -> dict:
353
+ """Add an SG_ line to the DBC frame of the I-PDU. CAN IDs are unique per bus only, so when several DBC files
354
+ have a frame with the ID, the frame that already carries the I-PDU's signals is the one; a frame that carries
355
+ none of them while another does belongs to another bus and is left alone."""
318
356
  from .audit import can_info
319
357
  infos = can_info(project, pdu.path if pdu is not None else None)
320
358
  dbcs = paths.walk(project.root, lambda n: n.lower().endswith(".dbc")).files
@@ -325,11 +363,21 @@ def _dbc_line(project: Project, pdu, name, pos, size, endian, typ, notes) -> dic
325
363
  cid = info.get("canId")
326
364
  if cid is None:
327
365
  continue
366
+ cands = []
328
367
  for f in dbcs:
329
368
  text = open(f, encoding="utf-8", errors="replace").read()
330
369
  m = re.search(rf"^BO_ ({cid}|{cid | 0x80000000}) (\w+)\s*:.*$", text, re.M)
331
- if not m:
332
- continue
370
+ if m:
371
+ cands.append((f, text, m))
372
+ owning = [c for c in cands if members & _frame_signals(c[1], c[2])]
373
+ if owning:
374
+ cands = owning
375
+ elif len(cands) > 1:
376
+ notes.append(f"DBC not changed: {len(cands)} frames have CAN ID {cid} "
377
+ f"({', '.join(project.rel(c[0]) + ':' + c[2].group(2) for c in cands)}) and none carries "
378
+ f"the I-PDU's signals")
379
+ cands = []
380
+ for f, text, m in cands:
333
381
  block_end = m.end()
334
382
  sg = list(re.finditer(r"^\s*SG_ .*$", text[m.end():].split("\n\n", 1)[0], re.M))
335
383
  recv = "Vector__XXX"
@@ -19,6 +19,9 @@ Path language (a list of steps, evaluated left to right from the row container):
19
19
  finds its neighbour's PDU handle (TPS_ECUC 3.4.1.3, via EcucPduCollection/Pdu)
20
20
  call:<name> a module generator's resolver (Generator.path_steps()) for what the defs cannot
21
21
  express as a path, e.g. Com's notification callouts (terminal, token)
22
+ child the one chosen sub-container of a choice container (any def)
23
+ def def short name of the current container (terminal), e.g. which choice is configured
24
+ count:<Def> number of direct sub-containers of def <Def> (terminal)
22
25
  sym TPS_ECUC_02108 symbol of the current container (terminal)
23
26
  module | name | index the current container's module name / SHORT-NAME / table index (terminal)
24
27
  Macro values: count:<Def> | count:@<Set> | max:<Def>/<Param>, optionally followed by +N or -N.
@@ -29,7 +32,12 @@ Module binding keys besides tables/objects/macros:
29
32
  vars RAM objects per row that tables point to (e.g. I-PDU buffers), sized by a path
30
33
  require [{"rows": "<Def>", "path": <path>, "in": [...], "why": "..."}]: what the static code supports;
31
34
  a configuration outside it is an error, never silently mis-generated
32
- Field keys: path, default, enum (literal -> C token), fmt ("hex" | "int" | template with {} / {MIP} / {Mip}),
35
+ Table keys besides name/type/rows/fields: order (sort param), contiguous (that param must be exactly
36
+ 0..n-1 after sorting, e.g. a position index), designated, per (<ParentDef>: one table per parent instance,
37
+ rows = its direct <Def> children, "{}" in name = parent SHORT-NAME; a parent with no rows is an error,
38
+ C has no empty arrays). unsupported entries may set "max": N (more than N containers is the error).
39
+ Field keys: path, default, enum (literal or token -> C token), scale (multiply, must give an integer:
40
+ seconds -> ms), fmt ("hex" | "int" | template with {} / {MIP} / {Mip}),
33
41
  divide (a path from the row whose value divides this one, rounded: seconds -> main-function ticks),
34
42
  agg ({"over": <multi-ref>, "params": [...], "op": "min_nonzero" | "max", "skipIf": <param>}).
35
43
  """
@@ -147,6 +155,16 @@ class Eval:
147
155
  (not wdef or c.def_name == wdef)]
148
156
  hits.sort(key=lambda c: (c.module.name, self.cg.by_path_index(c)))
149
157
  cur = hits[0] if hits else None
158
+ elif op == "child":
159
+ kids = list(cur.children)
160
+ if len(kids) > 1:
161
+ raise BindingError(f"{cur.path}: path step child: {len(kids)} sub-containers, expected the one "
162
+ "chosen sub-container of a choice container")
163
+ cur = kids[0] if kids else None
164
+ elif op == "def":
165
+ return ("token", cur.def_name)
166
+ elif op == "count":
167
+ return ("token", W.c_uint(len(cur.sub(arg))))
150
168
  elif op == "sym":
151
169
  s = self.cg.symbol(cur)
152
170
  if s and self.ctx is not None:
@@ -231,6 +249,12 @@ class Eval:
231
249
  s = self.ctx.xsym(v, self.mc, self.used_mips)
232
250
  return (s, "")
233
251
  if kind == "token":
252
+ if "enum" in spec:
253
+ if v not in spec["enum"]:
254
+ raise BindingError(f"{row.path}: {spec['path']} = {v} is not supported by the static code "
255
+ f"(the {self.mc.name} binding maps only {', '.join(spec['enum'])})")
256
+ e = spec["enum"][v]
257
+ return (W.c_uint(e) if isinstance(e, int) else str(e), v)
234
258
  if fmt and fmt not in ("hex", "int"):
235
259
  return (fmt.replace("{MIP}", v.upper()).replace("{Mip}", v).replace("{}", v), "")
236
260
  return (v, "")
@@ -244,6 +268,8 @@ class Eval:
244
268
  return (W.c_uint(e) if isinstance(e, int) else str(e), raw.strip())
245
269
  if "divide" in spec:
246
270
  return (self._ticks(row, raw, spec), "")
271
+ if "scale" in spec:
272
+ return (self._scaled(row, raw, spec), "")
247
273
  if fmt == "int": # a numeric value held in a STRING param (e.g. ComSignalInitValue)
248
274
  n = parse_int(raw.strip())
249
275
  if n is None:
@@ -261,6 +287,20 @@ class Eval:
261
287
  raise BindingError(f"{row.path}: cannot convert {spec.get('name')} = {raw} to ticks of {spec['divide']} = {base}")
262
288
  return W.c_uint(int(round(v / b)))
263
289
 
290
+ def _scaled(self, row: Container, raw, spec: dict) -> str:
291
+ """value * scale as an integer (e.g. LinIfDelay seconds -> ms); a value that is not a whole multiple is
292
+ an error, never rounded silently."""
293
+ from ..values import parse_float
294
+ v = parse_float(str(raw).strip())
295
+ if v is None:
296
+ raise BindingError(f"{row.path}: {spec.get('name')} = {raw} is not a number")
297
+ x = v * spec["scale"]
298
+ n = int(round(x))
299
+ if abs(x - n) > 1e-6 * max(1.0, abs(x)):
300
+ raise BindingError(f"{row.path}: {spec['path']} = {str(raw).strip()} is not a whole multiple of "
301
+ f"1/{spec['scale']}; the static code's {spec.get('name')} field holds an integer")
302
+ return W.c_uint(n)
303
+
264
304
  def _agg(self, row: Container, spec: dict) -> tuple:
265
305
  """Aggregate params over the targets of a multi-valued reference of the row (e.g. the smallest non-zero
266
306
  timeout of an I-PDU's signals, SWS_Com_00291)."""
@@ -373,6 +373,124 @@
373
373
  "init": ["Com_TxSignalInfo", "Com_RxSignalInfo", "Com_TxIPduInfo", "Com_RxIPduInfo"]
374
374
  }
375
375
  ]
376
+ },
377
+ "LinIf": {
378
+ "detect": {"header": "LinIf.h", "contains": "LinIf_FrameType"},
379
+ "sourceIncludes": ["LinIf.h"],
380
+ "sets": {"TxPdus": {"rows": "LinIfTxPdu"}},
381
+ "handleLayout": [{"param": "LinIfTxPduId", "sets": ["TxPdus"]}],
382
+ "require": [
383
+ {
384
+ "rows": "LinIfFrame",
385
+ "path": "param:LinIfFrameType",
386
+ "in": ["UNCONDITIONAL"],
387
+ "why": "LinIf_FrameType has no frame type: LinIf.c sends every slot as an unconditional frame (no sporadic, event-triggered, diagnostic MRF/SRF or node configuration frames)"
388
+ },
389
+ {
390
+ "rows": "LinIfFrame",
391
+ "path": ["sub:LinIfPduDirection", "child", "def"],
392
+ "in": ["LinIfTxPdu", "LinIfRxPdu"],
393
+ "why": "LinIf_FrameType.Direction is LINIF_DIR_TX or LINIF_DIR_RX only (no slave-to-slave or internal PDUs)"
394
+ },
395
+ {
396
+ "rows": "LinIfScheduleTable",
397
+ "path": "param:LinIfRunMode",
398
+ "in": ["RUN_CONTINUOUS"],
399
+ "why": "LinIf_ScheduleType has no run mode: LinIf_MainFunction cycles its entries continuously"
400
+ }
401
+ ],
402
+ "unsupported": [
403
+ {
404
+ "rows": "LinIfChannel",
405
+ "max": 1,
406
+ "why": "LinIf_ConfigType has one frame and one schedule list and no channel field"
407
+ },
408
+ {"rows": "LinIfSlave", "why": "LinIf.h is a master-node LinIf (LinIf_MainFunction runs the schedule)"},
409
+ {"rows": "LinIfSubstitutionFrames", "why": "LinIf_FrameType has no substitution frame list"},
410
+ {"rows": "LinIfFixedFrameSdu", "why": "LinIf_FrameType has no fixed response SDU"}
411
+ ],
412
+ "macros": [
413
+ {"name": "LINIF_NUMBER_OF_CHANNELS", "value": "count:LinIfChannel"},
414
+ {"name": "LINIF_NUMBER_OF_TX_PDUS", "value": "count:LinIfTxPdu"},
415
+ {"name": "LINIF_NUMBER_OF_FRAMES", "value": "count:LinIfFrame"},
416
+ {"name": "LINIF_NUMBER_OF_SCHEDULES", "value": "count:LinIfScheduleTable"}
417
+ ],
418
+ "tables": [
419
+ {
420
+ "name": "LinIf_Frames",
421
+ "type": "LinIf_FrameType",
422
+ "qualifier": "LINIF_CONST",
423
+ "static": true,
424
+ "rows": "LinIfFrame",
425
+ "size": "LINIF_NUMBER_OF_FRAMES",
426
+ "doc": "LinIfFrame in configuration order; LinIf_EntryType.FrameIndex indexes this table",
427
+ "fields": [
428
+ {"name": "FrameId", "path": "param:LinIfFrameId", "fmt": "hex"},
429
+ {
430
+ "name": "Length",
431
+ "path": [
432
+ ["sub:LinIfPduDirection", "sub:LinIfTxPdu", "ref:LinIfTxPduRef", "param:PduLength"],
433
+ ["sub:LinIfPduDirection", "sub:LinIfRxPdu", "ref:LinIfRxPduRef", "param:PduLength"]
434
+ ]
435
+ },
436
+ {
437
+ "name": "Checksum",
438
+ "path": "param:LinIfChecksumType",
439
+ "enum": {"CLASSIC": "LINIF_CHECKSUM_CLASSIC", "ENHANCED": "LINIF_CHECKSUM_ENHANCED"}
440
+ },
441
+ {
442
+ "name": "Direction",
443
+ "path": ["sub:LinIfPduDirection", "child", "def"],
444
+ "enum": {"LinIfTxPdu": "LINIF_DIR_TX", "LinIfRxPdu": "LINIF_DIR_RX"}
445
+ },
446
+ {
447
+ "name": "PduRPduId",
448
+ "path": [
449
+ ["sub:LinIfPduDirection", "sub:LinIfTxPdu", "peer:LinIfTxPduRef@PduR/PduRDestPdu", "sym"],
450
+ ["sub:LinIfPduDirection", "sub:LinIfRxPdu", "peer:LinIfRxPduRef@PduR/PduRSrcPdu", "sym"]
451
+ ]
452
+ }
453
+ ]
454
+ },
455
+ {
456
+ "name": "LinIf_Entries_{}",
457
+ "type": "LinIf_EntryType",
458
+ "qualifier": "LINIF_CONST",
459
+ "static": true,
460
+ "per": "LinIfScheduleTable",
461
+ "rows": "LinIfEntry",
462
+ "order": "LinIfEntryIndex",
463
+ "contiguous": true,
464
+ "doc": "{} slots in LinIfEntryIndex order (ECUC_LinIf_00011: position in the schedule table)",
465
+ "fields": [
466
+ {"name": "FrameIndex", "path": ["ref:LinIfFrameRef", "index"]},
467
+ {"name": "DelayMs", "path": "param:LinIfDelay", "scale": 1000}
468
+ ]
469
+ },
470
+ {
471
+ "name": "LinIf_Schedules",
472
+ "type": "LinIf_ScheduleType",
473
+ "qualifier": "LINIF_CONST",
474
+ "static": true,
475
+ "rows": "LinIfScheduleTable",
476
+ "size": "LINIF_NUMBER_OF_SCHEDULES",
477
+ "designated": true,
478
+ "doc": "indexed by LinIfScheduleTableIndex",
479
+ "comment": "name",
480
+ "fields": [
481
+ {"name": "Entries", "path": "name", "fmt": "LinIf_Entries_{}"},
482
+ {"name": "NumEntries", "path": "count:LinIfEntry"}
483
+ ]
484
+ }
485
+ ],
486
+ "objects": [
487
+ {
488
+ "name": "LinIf_Config",
489
+ "type": "LinIf_ConfigType",
490
+ "qualifier": "LINIF_CONST",
491
+ "init": ["LinIf_Frames", "LinIf_Schedules", "LINIF_NUMBER_OF_FRAMES", "LINIF_NUMBER_OF_SCHEDULES"]
492
+ }
493
+ ]
376
494
  }
377
495
  }
378
496
  }
@@ -0,0 +1,53 @@
1
+ """LinIf: LinIf_Cfg.h / LinIf_PBcfg.c.
2
+
3
+ Def-driven (generic emitter): the LinIfGeneral switches and the TPS_ECUC_02108 / SWS_BSW_00200 symbols
4
+ LinIfConf_LinIfTxPdu_<Pdu> (LinIfTxPduId, the handle PduR passes to LinIf_Transmit) and
5
+ LinIfConf_LinIfScheduleTable_<Table> (LinIfScheduleTableIndex, used with LinIf_ScheduleRequest).
6
+
7
+ The static LinIf's own layout (frame table, one entry list per schedule table, schedule table list,
8
+ root config object) comes from its binding (bindings/*.json): channels, frames with their EcuC PDU
9
+ length and PduR handle (via the shared EcucPduCollection/Pdu), schedule entries in LinIfEntryIndex
10
+ order with LinIfDelay, schedule tables by LinIfScheduleTableIndex. Configuration the binding cannot
11
+ represent (non-unconditional frames, slave nodes, more channels than the static code has, ...) is
12
+ listed in the binding's `require` / `unsupported` and fails generation.
13
+
14
+ This module adds only the SWS_LinIf consistency rules that hold for any static code:
15
+ * a schedule entry (LinIfEntry/LinIfFrameRef) must reference a frame of the schedule table's own
16
+ channel (ECUC_LinIf_00016; schedule tables are per channel, ECUC_LinIf_00365);
17
+ * the LinIfFrameId of the unconditional frames of a channel must be unique (one publisher per
18
+ frame identifier on a LIN cluster).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ from ..registry import Generator, register
23
+
24
+
25
+ def _channel(c):
26
+ while c is not None and c.def_name != "LinIfChannel":
27
+ c = c.parent
28
+ return c
29
+
30
+
31
+ @register("LinIf")
32
+ class LinIf(Generator):
33
+ std_header = "ComStack_Types.h"
34
+
35
+ def specific(self, mo):
36
+ for e in self.cg.instances(self.mc, "LinIfEntry"):
37
+ fr = self.cg.target(e.value("LinIfFrameRef"))
38
+ if fr is None:
39
+ mo.errors.append(f"{e.path}: LinIfFrameRef is not set or does not resolve to a LinIfFrame")
40
+ elif _channel(fr) is not _channel(e):
41
+ mo.errors.append(f"{e.path}: LinIfFrameRef points to {fr.path}, a frame of another channel "
42
+ f"(schedule tables are per channel, ECUC_LinIf_00365)")
43
+ for ch in self.cg.instances(self.mc, "LinIfChannel"):
44
+ seen: dict = {}
45
+ for fr in ch.sub("LinIfFrame"):
46
+ if (fr.text("LinIfFrameType") or "").strip() != "UNCONDITIONAL":
47
+ continue
48
+ fid = (fr.text("LinIfFrameId") or "").strip()
49
+ if fid in seen:
50
+ mo.errors.append(f"{fr.path}: LinIfFrameId {fid} is also used by {seen[fid]} on channel "
51
+ f"{ch.name}; unconditional frame identifiers must be unique per channel")
52
+ else:
53
+ seen[fid] = fr.name
@@ -98,10 +98,11 @@ def apply_binding(cg, mo: E.ModuleOutput, mb: dict, ext: Optional[dict] = None):
98
98
  f"{', '.join('(not configured)' if x is None else str(x) for x in req['in'])}: {req.get('why', '')}")
99
99
  for un in mb.get("unsupported", []):
100
100
  rows = cg.instances(mo.mc, un["rows"])
101
- if rows:
101
+ mx = un.get("max", 0)
102
+ if len(rows) > mx:
102
103
  mo.errors.append(f"{len(rows)} {un['rows']} container(s) configured ({', '.join(r.name for r in rows[:5])}"
103
104
  f"{', ...' if len(rows) > 5 else ''}), which the {mb['_binding']} static code cannot "
104
- f"represent: {un.get('why', '')}")
105
+ f"represent{f' (at most {mx})' if mx else ''}: {un.get('why', '')}")
105
106
  for inc in mb.get("sourceIncludes", []):
106
107
  mo.include(inc)
107
108
  macros = []
@@ -121,41 +122,9 @@ def apply_binding(cg, mo: E.ModuleOutput, mb: dict, ext: Optional[dict] = None):
121
122
  name = vs["name"].replace("{}", r.name)
122
123
  lines.append(f"{'static ' if vs.get('static') else ''}VAR({vs['type']}, {vs['qualifier']}) {name}[{size}];")
123
124
  pending.append(("var", ev.rank, vs.get("section", "VAR_NOINIT_UNSPECIFIED"), "\n".join(lines)))
124
- for t in mb.get("tables", []):
125
- ev.rank = 1
126
- rows = ev.rows(t["rows"], sets)
127
- if t.get("order"):
128
- from ..values import parse_int
129
- rows.sort(key=lambda r: (parse_int(r.text(t["order"]) or "") is None, parse_int(r.text(t["order"]) or "") or 0))
130
- lines, externs = [], []
131
- des = t.get("designated")
132
- if des:
133
- _check_index(cg, mo, t, rows, des, values)
134
- for r in rows:
135
- vals = []
136
- for f in t["fields"]:
137
- v, _ = ev.field(r, f)
138
- vals.append(v)
139
- if f.get("extern") and v not in ("NULL_PTR", "0", "0u") and not v.startswith('"'):
140
- m = re.search(r"[A-Za-z_]\w*", v)
141
- if m:
142
- ext_ = "extern " + f["extern"].replace("{}", m.group(0)) + ";"
143
- if ext_ not in externs:
144
- externs.append(ext_)
145
- lead = ""
146
- if des:
147
- lead = f"[{des.replace('{sym}', cg.symbol(r)) if isinstance(des, str) else cg.symbol(r)}] = "
148
- comment = t.get("comment", "name")
149
- cm = {"name": r.name, "sym": cg.symbol(r) or r.name}.get(comment, r.name)
150
- if t.get("scalar"):
151
- lines.append(f" {lead}{vals[0]} /* {cm} */")
152
- else:
153
- lines.append(f" {lead}{{ {', '.join(vals)} }} /* {cm} */")
154
- st = "static " if t.get("static") else ""
155
- size = t.get("size", "")
156
- body = (f"/* {t.get('doc', t['rows'])} */\n{st}CONST({t['type']}, {t['qualifier']}) {t['name']}[{size}] = {{\n" +
157
- ",\n".join(lines) + "\n};")
158
- pending.append(("table", ev.rank, body, externs))
125
+ for t0 in mb.get("tables", []):
126
+ for t, rows in _table_instances(cg, mo, ev, t0, sets):
127
+ _emit_table(cg, mo, ev, t, rows, values, pending)
159
128
  objects = mb.get("objects", [])
160
129
  if objects:
161
130
  # a root config object points at the (static) tables and buffers, so they all live in its file:
@@ -173,6 +142,67 @@ def apply_binding(cg, mo: E.ModuleOutput, mb: dict, ext: Optional[dict] = None):
173
142
  mo.used_mips |= ev.used_mips
174
143
 
175
144
 
145
+ def _table_instances(cg, mo, ev, t: dict, sets: dict) -> list:
146
+ """[(table spec, rows)]: one table, or one per parent instance for a "per" table."""
147
+ from ..values import parse_int
148
+ num = lambda r: parse_int(r.text(t["order"]) or "") # noqa: E731
149
+ out = []
150
+ if t.get("per"):
151
+ specs = []
152
+ for par in cg.sorted_instances(mo.mc, t["per"]):
153
+ rows = par.sub(t["rows"])
154
+ if not rows:
155
+ raise B.BindingError(f"{par.path}: no {t['rows']} configured; {t['name'].replace('{}', par.name)} "
156
+ "would be an empty array, which C does not allow")
157
+ specs.append((dict(t, name=t["name"].replace("{}", par.name),
158
+ doc=t.get("doc", t["rows"]).replace("{}", par.name)), rows))
159
+ else:
160
+ specs = [(t, ev.rows(t["rows"], sets))]
161
+ for spec, rows in specs:
162
+ if t.get("order"):
163
+ rows = sorted(rows, key=lambda r: (num(r) is None, num(r) or 0))
164
+ if t.get("contiguous"):
165
+ got = [num(r) for r in rows]
166
+ if got != list(range(len(rows))):
167
+ raise B.BindingError(f"{spec['name']}: {t['order']} of the {len(rows)} {t['rows']} rows must be exactly "
168
+ f"0..{len(rows) - 1} (table position), got {', '.join(map(str, got))}")
169
+ out.append((spec, rows))
170
+ return out
171
+
172
+
173
+ def _emit_table(cg, mo, ev, t: dict, rows: list, values: dict, pending: list):
174
+ ev.rank = 1
175
+ lines, externs = [], []
176
+ des = t.get("designated")
177
+ if des:
178
+ _check_index(cg, mo, t, rows, des, values)
179
+ for r in rows:
180
+ vals = []
181
+ for f in t["fields"]:
182
+ v, _ = ev.field(r, f)
183
+ vals.append(v)
184
+ if f.get("extern") and v not in ("NULL_PTR", "0", "0u") and not v.startswith('"'):
185
+ m = re.search(r"[A-Za-z_]\w*", v)
186
+ if m:
187
+ ext_ = "extern " + f["extern"].replace("{}", m.group(0)) + ";"
188
+ if ext_ not in externs:
189
+ externs.append(ext_)
190
+ lead = ""
191
+ if des:
192
+ lead = f"[{des.replace('{sym}', cg.symbol(r)) if isinstance(des, str) else cg.symbol(r)}] = "
193
+ comment = t.get("comment", "name")
194
+ cm = {"name": r.name, "sym": cg.symbol(r) or r.name}.get(comment, r.name)
195
+ if t.get("scalar"):
196
+ lines.append(f" {lead}{vals[0]} /* {cm} */")
197
+ else:
198
+ lines.append(f" {lead}{{ {', '.join(vals)} }} /* {cm} */")
199
+ st = "static " if t.get("static") else ""
200
+ size = t.get("size", "")
201
+ body = (f"/* {t.get('doc', t['rows'])} */\n{st}CONST({t['type']}, {t['qualifier']}) {t['name']}[{size}] = {{\n" +
202
+ ",\n".join(lines) + "\n};")
203
+ pending.append(("table", ev.rank, body, externs))
204
+
205
+
176
206
  def _check_index(cg, mo, t: dict, rows: list, des, values: dict):
177
207
  """A designated table is indexed by handle (minus an offset): every row must land in [0, size) exactly once,
178
208
  or the static code's table lookup would be wrong. Reported as an error, never renumbered here."""
@@ -37,12 +37,30 @@ ComUserCallback of type COM_RX_ACK / COM_TX_ACK; `call:comUserCallback`), or, fo
37
37
  callbacks, the RTE's `Rte_COMCbk_<sn>()` / `Rte_COMCbkTAck_<sn>()` when the RTE's Rte_Cbk.h declares it
38
38
  (`call:rteSignalCallback`). Nothing is bound from guesses: no declaration, no callback.
39
39
 
40
+ ## LinIf
41
+ `LinIf_Cfg.h`: LinIfGeneral switches, `LinIfConf_LinIfTxPdu_*` (LinIfTxPduId, the handle PduR passes to `LinIf_Transmit`) and
42
+ `LinIfConf_LinIfScheduleTable_*` (LinIfScheduleTableIndex) symbols (TPS_ECUC_02108 / SWS_BSW_00200), `LINIF_NUMBER_OF_*`.
43
+ `LinIf_PBcfg.c` (binding): frame table in configuration order (LinIfFrameId, length from the EcuC Pdu, checksum, direction
44
+ from the LinIfPduDirection choice, PduR dest (Tx) / src (Rx) handle via the shared EcuC Pdu), one entry list per schedule
45
+ table (`per` table, LinIfEntryIndex order, which must be 0..n-1; FrameIndex = position of LinIfFrameRef's frame;
46
+ LinIfDelay s -> ms, a non-whole ms is an error), schedule list indexed by LinIfScheduleTableIndex, root `LinIf_Config`.
47
+ Tx PDU handles are laid out 0..n-1 (handleLayout; `LinIf_Transmit` range-checks them).
48
+ Errors, never dropped: frame types other than UNCONDITIONAL, PDU directions other than Tx/Rx, RUN_ONCE schedules, more
49
+ than one channel, slave nodes, substitution frames, fixed frame SDUs (stand-in binding); an entry whose frame belongs to
50
+ another channel, duplicate unconditional LinIfFrameIds on a channel (any binding).
51
+
52
+ Binding path/table additions used by LinIf: `child` (chosen sub-container of a choice), `def` (its def name, mapped with
53
+ `enum`), `count:<Def>`, field `scale`, table `per` / `contiguous`, `unsupported[].max`.
54
+
40
55
  ## Known differences from the sample/large fixture oracle (all explained)
41
56
  | Item | Oracle | Ours | Why |
42
57
  |---|---|---|---|
43
58
  | `NvM_BlockDescriptorTable[*].RamBlockDataAddress` | `X` | `&X` (as configured) | ECUC_NvM_00482 value is the address expression; kept verbatim, same address for arrays |
44
59
  | Rx I-PDU `TimeoutTicks` with mixed signal timeouts | largest ComTimeout | smallest non-zero | SWS_Com_00291 |
45
60
  | `<MIP>_CFG_*` version macros, generic ComGeneral/NvMCommon/... switches, Dem/Fee tables | absent | extra | def-driven output the oracle does not emit |
61
+ | LinIf entry list name | `LinIf_Sched_<Table>` | `LinIf_Entries_<Table>` | `static` table, internal linkage: not part of the LinIf.h contract |
62
+ | `LinSMConf_LinSMSchedule_*` in `LinIf_Cfg.h` | present | absent (in `LinSM_Cfg.h`) | a `LinSMConf_` symbol belongs to the LinSM config header (SWS_BSW_00200) |
63
+ | `LINIF_NUMBER_OF_SCHEDULES`, LinIfGeneral switches, version macros | absent | extra | def/binding-driven output the oracle does not emit |
46
64
  | SchM main function names | `Com_MainFunctionTx` | same | SWS_Com says `Com_MainFunctionTx_<shortName>`; the instance named like its def keeps the SIP's legacy name |
47
65
 
48
66
  ## Reading a diff