@anik-2/autosar 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -13,6 +13,7 @@ Deterministic tools for AUTOSAR Classic integration work, so the agent never han
13
13
  | `arxml_diff` | semantic diff by AUTOSAR path (ignores UUIDs, order, whitespace, file splits) | local |
14
14
  | `autosar_trace` | config ↔ generated C symbol ↔ in-house callers, via the H2Loop code graph | H2Loop backend |
15
15
  | `autosar_config_lookup` | element + children + refs in/out + links from the code graph | H2Loop backend |
16
+ | `autosar_audit` | integration review of C code: BSW API handles, direction, widths, NvM NULL_PTR vs ECUC | local |
16
17
  | `autosar_generate` / `autosar_build` | run the project's configured generator / build command (asks permission) | local |
17
18
 
18
19
  Plus the `autosar` skill (ECUC model, symbolic-name rule, config classes, project layout, the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anik-2/autosar",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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,21 +1,26 @@
1
1
  // @bun
2
- import{isAbsolute as q_,join as h,resolve as H_}from"path";import{tool as P}from"@kilocode/plugin";import{homedir as x}from"os";import{join as X}from"path";var d=()=>process.env.HYDRON_TEST_HOME||x(),C=(q,H)=>process.env[q]||X(d(),H),c=()=>X(C("XDG_CONFIG_HOME",".config"),"hydron-cli"),l=()=>X(C("XDG_DATA_HOME",".local/share"),"hydron-cli"),n=()=>X(C("XDG_STATE_HOME",".local/state"),"hydron-cli");async function L(q){let H=Bun.file(q);if(!await H.exists())return;return H.json().catch(()=>{return})}async function O(){let q=process.env.KILO_API_URL?.trim();if(q)return q.replace(/\/+$/,"");let H=await L(X(c(),"server-config.json")),_=typeof H?.apiBaseUrl==="string"?H.apiBaseUrl:void 0;if(!_)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return _.replace(/\/+$/,"")}function i(q){let H=q?.split(".")[1];if(!H)return"";try{let _=JSON.parse(Buffer.from(H.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof _.sub==="string"?_.sub:""}catch{return""}}async function g(q){let H=i(q.access);if(H)return H;if(!q.refresh)return"";return new Bun.CryptoHasher("sha256").update(q.refresh).digest("hex").slice(0,12)}async function v(){let H=(await L(X(l(),"auth.json")))?.hydron;if(!H||H.type!=="oauth"||!H.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return H}async function u(){let q=await v();if(typeof q.expires==="number"&&q.expires>0&&Date.now()>=q.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 q.access}async function f(q){let H=await fetch(`${await O()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await u()}`},signal:q?AbortSignal.any([q,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(H.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!H.ok)throw Error(`could not list H2Loop projects: HTTP ${H.status}`);let _=await H.json();return Array.isArray(_)?_:_?.items??[]}var k=()=>X(n(),"h2loop-project.json");async function S(q,H){let _=q===void 0||q===null?"":String(q).trim();if(/^\d+$/.test(_))return _;if(_){let $=await f(H),D=$.filter((F)=>F.name===_),B=D.length?D:$.filter((F)=>F.name?.toLowerCase()===_.toLowerCase());if(B.length===1)return String(B[0].id);if(B.length>1)throw Error(`workspace "${_}" is ambiguous: ${B.map((F)=>`${F.name} (id ${F.id})`).join(", ")}; pass the numeric id`);let W=$.filter((F)=>F.name?.toLowerCase().includes(_.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${_}"`+(W.length?`; similar: ${W.map((F)=>`${F.name} (id ${F.id})`).join(", ")}`:`; you have ${$.length} project(s)`))}let J=await L(k());if(!J)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let V=await v();if(J.project&&typeof J.project==="object"){let $=J.project;if(typeof J.account==="string"&&J.account!==await g(V))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if($.id!==void 0)return String($.id)}let Q=J;if(Q.id===void 0)throw Error(`${k()} holds no project id; select a project again or pass workspace_id.`);if(!(await f(H)).some(($)=>String($.id)===String(Q.id)))throw Error(`The selected project ${Q.name??""} (id ${Q.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(Q.id)}async function T(q,H,_,J){let V=`${await O()}${q}?workspace_id=${encodeURIComponent(H)}`,Q=await fetch(V,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await u()}`},body:JSON.stringify(_),signal:J?AbortSignal.any([J,AbortSignal.timeout(120000)]):AbortSignal.timeout(120000)}),Y=await Q.text();if(Q.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(Q.status===404&&Y.includes("Not Found"))throw Error(`${q} is not available on ${await O()} (backend predates the AUTOSAR endpoints).`);if(!Q.ok)throw Error(`H2Loop ${q} failed: HTTP ${Q.status} ${Y.slice(0,800)}`);try{return JSON.parse(Y)}catch($){throw Error(`H2Loop ${q} returned non-JSON (${String($)}): ${Y.slice(0,300)}`)}}var A=/\.arxml\b/i,E="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit with ops by AUTOSAR path (read paths and allowed params with ecuc_read schema=true), then ecuc_validate.";function z(q,H){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!H)return;let _=typeof H.filePath==="string"?H.filePath:typeof H.file_path==="string"?H.file_path:"";if(["edit","write","multiedit"].includes(q)){let J=Array.isArray(H.edits)?H.edits.some((V)=>typeof V?.filePath==="string"&&A.test(V.filePath)):!1;if(A.test(_)||J)return`${E} (${q} on ${_||"an .arxml file"})`;return}if(q==="apply_patch"||q==="patch"){let J=String(H.patchText??H.patch??""),V=J.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??J.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return V?`${E} (${q} on ${V[1].trim()})`:void 0}if(q==="bash"){let J=String(H.command??"");if(!A.test(J))return;let V=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(J)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(J),Q=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(J)||/\btee\b[^|;&]*\.arxml\b/i.test(J);if(V||Q)return`${E} (bash: ${J.slice(0,120)})`}return}import{existsSync as o}from"fs";import{dirname as t,join as b}from"path";import{fileURLToPath as a}from"url";var s="autosar-data==0.17.0";function e(){let q=t(a(import.meta.url)),H=[b(q,"..","python"),b(q,"..","..","python")].find((_)=>o(b(_,"ecuc_engine","__main__.py")));if(!H)throw Error(`ECUC engine not found next to ${q} (expected ../python/ecuc_engine)`);return H}async function r(q){try{return await Bun.spawn([...q,"-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 I={};async function __(){if(I.cmd!==void 0)return I.cmd;let q=process.env.AUTOSAR_PYTHON?.trim(),_=q?[[q]]:[["uv","run","--no-project","--quiet","--with",s,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let J of _)if(await r(J))return I.cmd=J,J;return I.cmd=null,null}async function G(q,H,_){let J=await __();if(!J)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 V=e(),Q=Bun.spawn([...J,"-m","ecuc_engine","--root",q,...H,"--json"],{cwd:q,env:{...process.env,PYTHONPATH:[V,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:_}),[Y,$]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text()]);await Q.exited;let D=Y.trim().split(`
3
- `).filter(Boolean).at(-1);if(!D)return{ok:!1,error:`engine produced no output (exit ${Q.exitCode}): ${$.trim().slice(-2000)}`};try{return JSON.parse(D)}catch(B){return{ok:!1,error:`engine output was not JSON (${String(B)}): ${D.slice(0,500)} ${$.slice(-500)}`}}}var Z=P.schema,J_=["# AUTOSAR pack active","Load the `autosar` skill for ARXML/ECUC work. Configured values: use ecuc_read (accepts a SHORT-NAME) or","autosar_config_lookup, not grep/read. Change config only with ecuc_edit (hand edits of .arxml are blocked),","then ecuc_validate. Cite the value's @path (its AUTOSAR path) and its DEFINITION-REF (@def) separately."].join(`
4
- `);function p(q){if(typeof q!=="string")return q;let H=q.trim();if(!H.startsWith("[")&&!H.startsWith("{")&&!H.startsWith('"'))return q;try{return p(JSON.parse(H))}catch{return q}}var M=(q)=>Z.preprocess(p,q),U=" 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.";function w(q,H=200){let _=q.slice(0,H).map((J)=>`${J.file}:${J.line} ${J.severity.toUpperCase()} ${J.rule} ${J.path} \u2014 ${J.message}${J.defRef?` [def ${J.defRef}]`:""}`);if(q.length>H)_.push(`\u2026 ${q.length-H} more`);return _}function R(q){let H=q.didYouMean?`
2
+ import{isAbsolute as H_,join as w,resolve as J_}from"path";import{tool as P}from"@kilocode/plugin";import{homedir as d}from"os";import{join as N}from"path";var l=()=>process.env.HYDRON_TEST_HOME||d(),C=(q,J)=>process.env[q]||N(l(),J),c=()=>N(C("XDG_CONFIG_HOME",".config"),"hydron-cli"),i=()=>N(C("XDG_DATA_HOME",".local/share"),"hydron-cli"),g=()=>N(C("XDG_STATE_HOME",".local/state"),"hydron-cli");async function L(q){let J=Bun.file(q);if(!await J.exists())return;return J.json().catch(()=>{return})}async function O(){let q=process.env.KILO_API_URL?.trim();if(q)return q.replace(/\/+$/,"");let J=await L(N(c(),"server-config.json")),_=typeof J?.apiBaseUrl==="string"?J.apiBaseUrl:void 0;if(!_)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return _.replace(/\/+$/,"")}function n(q){let J=q?.split(".")[1];if(!J)return"";try{let _=JSON.parse(Buffer.from(J.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof _.sub==="string"?_.sub:""}catch{return""}}async function o(q){let J=n(q.access);if(J)return J;if(!q.refresh)return"";return new Bun.CryptoHasher("sha256").update(q.refresh).digest("hex").slice(0,12)}async function k(){let J=(await L(N(i(),"auth.json")))?.hydron;if(!J||J.type!=="oauth"||!J.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return J}async function z(){let q=await k();if(typeof q.expires==="number"&&q.expires>0&&Date.now()>=q.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 q.access}async function v(q){let J=await fetch(`${await O()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await z()}`},signal:q?AbortSignal.any([q,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(J.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!J.ok)throw Error(`could not list H2Loop projects: HTTP ${J.status}`);let _=await J.json();return Array.isArray(_)?_:_?.items??[]}var f=()=>N(g(),"h2loop-project.json");async function S(q,J){let _=q===void 0||q===null?"":String(q).trim();if(/^\d+$/.test(_))return _;if(_){let V=await v(J),D=V.filter((W)=>W.name===_),B=D.length?D:V.filter((W)=>W.name?.toLowerCase()===_.toLowerCase());if(B.length===1)return String(B[0].id);if(B.length>1)throw Error(`workspace "${_}" is ambiguous: ${B.map((W)=>`${W.name} (id ${W.id})`).join(", ")}; pass the numeric id`);let F=V.filter((W)=>W.name?.toLowerCase().includes(_.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${_}"`+(F.length?`; similar: ${F.map((W)=>`${W.name} (id ${W.id})`).join(", ")}`:`; you have ${V.length} project(s)`))}let H=await L(f());if(!H)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let Y=await k();if(H.project&&typeof H.project==="object"){let V=H.project;if(typeof H.account==="string"&&H.account!==await o(Y))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(V.id!==void 0)return String(V.id)}let Q=H;if(Q.id===void 0)throw Error(`${f()} holds no project id; select a project again or pass workspace_id.`);if(!(await v(J)).some((V)=>String(V.id)===String(Q.id)))throw Error(`The selected project ${Q.name??""} (id ${Q.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(Q.id)}async function T(q,J,_,H){let Y=`${await O()}${q}?workspace_id=${encodeURIComponent(J)}`,Q=await fetch(Y,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await z()}`},body:JSON.stringify(_),signal:H?AbortSignal.any([H,AbortSignal.timeout(120000)]):AbortSignal.timeout(120000)}),Z=await Q.text();if(Q.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(Q.status===404&&Z.includes("Not Found"))throw Error(`${q} is not available on ${await O()} (backend predates the AUTOSAR endpoints).`);if(!Q.ok)throw Error(`H2Loop ${q} failed: HTTP ${Q.status} ${Z.slice(0,800)}`);try{return JSON.parse(Z)}catch(V){throw Error(`H2Loop ${q} returned non-JSON (${String(V)}): ${Z.slice(0,300)}`)}}var A=/\.arxml\b/i,b="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit with ops by AUTOSAR path (read paths and allowed params with ecuc_read schema=true), then ecuc_validate.";function y(q,J){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!J)return;let _=typeof J.filePath==="string"?J.filePath:typeof J.file_path==="string"?J.file_path:"";if(["edit","write","multiedit"].includes(q)){let H=Array.isArray(J.edits)?J.edits.some((Y)=>typeof Y?.filePath==="string"&&A.test(Y.filePath)):!1;if(A.test(_)||H)return`${b} (${q} on ${_||"an .arxml file"})`;return}if(q==="apply_patch"||q==="patch"){let H=String(J.patchText??J.patch??""),Y=H.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??H.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return Y?`${b} (${q} on ${Y[1].trim()})`:void 0}if(q==="bash"){let H=String(J.command??"");if(!A.test(H))return;let Y=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(H)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(H),Q=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(H)||/\btee\b[^|;&]*\.arxml\b/i.test(H);if(Y||Q)return`${b} (bash: ${H.slice(0,120)})`}return}import{existsSync as a}from"fs";import{dirname as t,join as E}from"path";import{fileURLToPath as s}from"url";var e="autosar-data==0.17.0";function r(){let q=t(s(import.meta.url)),J=[E(q,"..","python"),E(q,"..","..","python")].find((_)=>a(E(_,"ecuc_engine","__main__.py")));if(!J)throw Error(`ECUC engine not found next to ${q} (expected ../python/ecuc_engine)`);return J}async function __(q){try{return await Bun.spawn([...q,"-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 R={};async function q_(){if(R.cmd!==void 0)return R.cmd;let q=process.env.AUTOSAR_PYTHON?.trim(),_=q?[[q]]:[["uv","run","--no-project","--quiet","--with",e,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let H of _)if(await __(H))return R.cmd=H,H;return R.cmd=null,null}async function G(q,J,_){let H=await q_();if(!H)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 Y=r(),Q=Bun.spawn([...H,"-m","ecuc_engine","--root",q,...J,"--json"],{cwd:q,env:{...process.env,PYTHONPATH:[Y,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:_}),[Z,V]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text()]);await Q.exited;let D=Z.trim().split(`
3
+ `).filter(Boolean).at(-1);if(!D)return{ok:!1,error:`engine produced no output (exit ${Q.exitCode}): ${V.trim().slice(-2000)}`};try{return JSON.parse(D)}catch(B){return{ok:!1,error:`engine output was not JSON (${String(B)}): ${D.slice(0,500)} ${V.slice(-500)}`}}}var $=P.schema,Q_=["# AUTOSAR pack active","Load the `autosar` skill for ARXML/ECUC work. Configured values: use ecuc_read (accepts a SHORT-NAME) or","autosar_config_lookup, not grep/read. Change config only with ecuc_edit (hand edits of .arxml are blocked),","then ecuc_validate. Reviewing C code using BSW APIs: run autosar_audit first. Cite @path (AUTOSAR path) and","@def (DEFINITION-REF) separately."].join(`
4
+ `);function x(q){if(typeof q!=="string")return q;let J=q.trim();if(!J.startsWith("[")&&!J.startsWith("{")&&!J.startsWith('"'))return q;try{return x(JSON.parse(J))}catch{return q}}var U=(q)=>$.preprocess(x,q),I=" 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.";function p(q,J=200){let _=q.slice(0,J).map((H)=>`${H.file}:${H.line} ${H.severity.toUpperCase()} ${H.rule} ${H.path} \u2014 ${H.message}${H.defRef?` [def ${H.defRef}]`:""}`);if(q.length>J)_.push(`\u2026 ${q.length-J} more`);return _}function M(q){let J=q.didYouMean?`
5
5
  Did you mean: ${q.didYouMean.join(", ")}`:"",_=q.schema?`
6
- Schema errors: ${JSON.stringify(q.schema)}`:"";return`ERROR: ${q.error}${H}${_}`}function N(q){let H=q.warnings??[];return H.length?H.map((_)=>`WARNING: ${_}`).join(`
6
+ Schema errors: ${JSON.stringify(q.schema)}`:"";return`ERROR: ${q.error}${J}${_}`}function X(q){let J=q.warnings??[];return J.length?J.map((_)=>`WARNING: ${_}`).join(`
7
7
  `)+`
8
- `:""}function K(q,H){if(!H)return q;return q_(H)?H:H_(q,H)}async function Q_(q){let H=Bun.file(h(q,".hydron","autosar.json"));if(!await H.exists())return{};return H.json().catch((_)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(_)}`)})}async function V_(q,H){let _=(await Q_(q))[H];if(typeof _==="string")return{command:_};if(_&&typeof _==="object"&&typeof _.command==="string")return _;return}function m(q,H=150){let _=q.split(`
9
- `);return _.length>H?[`\u2026 (${_.length-H} earlier lines omitted)`,..._.slice(-H)].join(`
10
- `):q}async function Y_(q,H,_,J){let V=process.platform==="win32"?["cmd","/d","/s","/c",q]:["sh","-c",q],Q=Date.now(),Y=Bun.spawn(V,{cwd:H,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([J,AbortSignal.timeout(_*1000)])}),[$,D]=await Promise.all([new Response(Y.stdout).text(),new Response(Y.stderr).text()]);return{code:await Y.exited,out:$,err:D,secs:Math.round((Date.now()-Q)/1000)}}var y=(q)=>{let H=q.notes??[],_=q.errors??[];return[...H.map((J)=>`NOTE: ${typeof J==="string"?J:JSON.stringify(J)}`),..._.map((J)=>`BACKEND ERROR: ${typeof J==="string"?J:JSON.stringify(J)}`)]};async function Z_(q,H){try{let _=await H(),J=typeof _==="string"?_:_.output;if(J&&J.trim())return _;return`${q}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(_){return`ERROR (${q}): ${_ instanceof Error?_.message:String(_)}`}}function $_(q){for(let[H,_]of Object.entries(q)){let J=_.execute;_.execute=(V,Q)=>Z_(H,()=>J(V,Q))}return q}var B_=async(q)=>{let H=(_)=>P({description:_==="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:Z.string().optional().describe("module name substituted for {module} in the command, if used"),args:Z.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(J,V){let Q=K(V.directory,J.project_dir),Y=await V_(Q,_);if(!Y)return`No \`${_}\` command configured. Add it to ${h(Q,".hydron","autosar.json")}, e.g.
11
- {"${_}": {"command": "${_==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let $=J.args??"",D=Y.command.replaceAll("{module}",J.module??""),B=D.includes("{args}")?D.replaceAll("{args}",$):`${D}${$?` ${$}`:""}`,W=K(Q,Y.cwd);await V.ask({permission:`autosar_${_}`,patterns:[B],always:[Y.command],metadata:{command:B,cwd:W}}),V.metadata({title:`${_}: ${B}`});let F=await Y_(B,W,Y.timeoutSec??3600,V.abort),j=F.code===0?"OK":`FAILED (exit ${F.code})`;return{title:`autosar_${_} ${j}`,output:[`$ ${B} (cwd ${W}, ${F.secs}s) -> ${j}`,F.out.trim()?`--- stdout (tail) ---
12
- ${m(F.out)}`:"",F.err.trim()?`--- stderr (tail) ---
13
- ${m(F.err,80)}`:""].filter(Boolean).join(`
14
- `),metadata:{exit:F.code}}}});return{"tool.execute.before":async(_,J)=>{let V=z(_.tool,J.args);if(V)throw Error(V)},"experimental.chat.system.transform":async(_,J)=>{J.system.push(J_)},tool:$_({ecuc_read:P({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, and @def = its DEFINITION-REF). 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."+U,args:{path:Z.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:Z.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:Z.boolean().optional().describe("include the param-def summary for the element"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=["read"];if(_.path)V.push(_.path);if(V.push("--depth",String(_.depth??2)),_.schema)V.push("--schema");let Q=await G(K(J.directory,_.project_dir),V,J.abort);if(!Q.ok)return N(Q)+R(Q);let{ok:Y,engine:$,ms:D,warnings:B,...W}=Q;return N(Q)+JSON.stringify(W,null,1)}}),ecuc_edit:P({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."+U,args:{ops:M(Z.array(M(Z.object({op:Z.enum(["set","add","remove"]),path:Z.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:Z.string().optional().describe("parameter/reference def short-name"),value:Z.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:Z.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:M(Z.record(Z.string(),Z.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:Z.boolean().optional().describe("compute and show the diff without writing"),force:Z.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=["edit","--ops",JSON.stringify(_.ops)];if(_.dry_run)V.push("--dry-run");if(_.force)V.push("--force");let Q=await G(K(J.directory,_.project_dir),V,J.abort);if(!Q.ok){let B=Q.changes??[];return N(Q)+R(Q)+(B.length?`
8
+ `:""}function K(q,J){if(!J)return q;return H_(J)?J:J_(q,J)}async function V_(q){let J=Bun.file(w(q,".hydron","autosar.json"));if(!await J.exists())return{};return J.json().catch((_)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(_)}`)})}async function Y_(q,J){let _=(await V_(q))[J];if(typeof _==="string")return{command:_};if(_&&typeof _==="object"&&typeof _.command==="string")return _;return}function m(q,J=150){let _=q.split(`
9
+ `);return _.length>J?[`\u2026 (${_.length-J} earlier lines omitted)`,..._.slice(-J)].join(`
10
+ `):q}async function Z_(q,J,_,H){let Y=process.platform==="win32"?["cmd","/d","/s","/c",q]:["sh","-c",q],Q=Date.now(),Z=Bun.spawn(Y,{cwd:J,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([H,AbortSignal.timeout(_*1000)])}),[V,D]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text()]);return{code:await Z.exited,out:V,err:D,secs:Math.round((Date.now()-Q)/1000)}}async function u(q,J,_){let H=J.filter((V)=>V?.path&&/\/Com(Group)?Signal$/.test(String(V.definition_ref??""))&&V.ipdu===void 0&&V.direction===void 0);if(!H.length)return[];let Y=await G(q,["signal-owners",...new Set(H.map((V)=>V.path))],_);if(!Y.ok)return[`NOTE: owning I-PDU not computed locally (${Y.error})`];let Q=Y.owners,Z=[];for(let V of H){let D=Q[V.path];if(D===null||D===void 0){Z.push(`NOTE: ${V.path} is not in the local checkout (${q}); owning I-PDU/direction unknown`);continue}V.ipdu_local=D,Z.push(D.length?`${V.path}: owning I-PDU ${D.map((B)=>`${B.ipdu} (ComIPduDirection=${B.direction??"?"})`).join(", ")} [local ECUC]`:`${V.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return Z}var h=(q)=>{let J=q.notes??[],_=q.errors??[];return[...J.map((H)=>`NOTE: ${typeof H==="string"?H:JSON.stringify(H)}`),..._.map((H)=>`BACKEND ERROR: ${typeof H==="string"?H:JSON.stringify(H)}`)]};async function $_(q,J){try{let _=await J(),H=typeof _==="string"?_:_.output;if(H&&H.trim())return _;return`${q}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(_){return`ERROR (${q}): ${_ instanceof Error?_.message:String(_)}`}}function B_(q){for(let[J,_]of Object.entries(q)){let H=_.execute;_.execute=(Y,Q)=>$_(J,()=>H(Y,Q))}return q}var D_=async(q)=>{let J=(_)=>P({description:_==="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:$.string().optional().describe("module name substituted for {module} in the command, if used"),args:$.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(H,Y){let Q=K(Y.directory,H.project_dir),Z=await Y_(Q,_);if(!Z)return`No \`${_}\` command configured. Add it to ${w(Q,".hydron","autosar.json")}, e.g.
11
+ {"${_}": {"command": "${_==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let V=H.args??"",D=Z.command.replaceAll("{module}",H.module??""),B=D.includes("{args}")?D.replaceAll("{args}",V):`${D}${V?` ${V}`:""}`,F=K(Q,Z.cwd);await Y.ask({permission:`autosar_${_}`,patterns:[B],always:[Z.command],metadata:{command:B,cwd:F}}),Y.metadata({title:`${_}: ${B}`});let W=await Z_(B,F,Z.timeoutSec??3600,Y.abort),j=W.code===0?"OK":`FAILED (exit ${W.code})`;return{title:`autosar_${_} ${j}`,output:[`$ ${B} (cwd ${F}, ${W.secs}s) -> ${j}`,W.out.trim()?`--- stdout (tail) ---
12
+ ${m(W.out)}`:"",W.err.trim()?`--- stderr (tail) ---
13
+ ${m(W.err,80)}`:""].filter(Boolean).join(`
14
+ `),metadata:{exit:W.code}}}});return{"tool.execute.before":async(_,H)=>{let Y=y(_.tool,H.args);if(Y)throw Error(Y)},"experimental.chat.system.transform":async(_,H)=>{H.system.push(Q_)},tool:B_({ecuc_read:P({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, and @def = its DEFINITION-REF). 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."+I,args:{path:$.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:$.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:$.boolean().optional().describe("include the param-def summary for the element"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,H){let Y=["read"];if(_.path)Y.push(_.path);if(Y.push("--depth",String(_.depth??2)),_.schema)Y.push("--schema");let Q=await G(K(H.directory,_.project_dir),Y,H.abort);if(!Q.ok)return X(Q)+M(Q);let{ok:Z,engine:V,ms:D,warnings:B,...F}=Q;return X(Q)+JSON.stringify(F,null,1)}}),ecuc_edit:P({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."+I,args:{ops:U($.array(U($.object({op:$.enum(["set","add","remove"]),path:$.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:$.string().optional().describe("parameter/reference def short-name"),value:$.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:$.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:U($.record($.string(),$.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:$.boolean().optional().describe("compute and show the diff without writing"),force:$.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,H){let Y=["edit","--ops",JSON.stringify(_.ops)];if(_.dry_run)Y.push("--dry-run");if(_.force)Y.push("--force");let Q=await G(K(H.directory,_.project_dir),Y,H.abort);if(!Q.ok){let B=Q.changes??[];return X(Q)+M(Q)+(B.length?`
15
15
  Rejected diff:
16
- ${B.map((W)=>W.diff).join(`
17
- `)}`:"")}let Y=Q.changes??[],$=Q.validation,D=[`${Q.written?"Written":"Dry run (nothing written)"}: ${Q.applied} op(s), ${Y.length} file(s) changed.`,...(Q.notes??[]).map((B)=>`NOTE: ${B}`),...Y.map((B)=>B.diff)];if($)D.push($.validated?`Validation of touched files: ${JSON.stringify($.summary)}`:"Validation skipped: no param defs loaded.",...w($.diagnostics,50));return{title:`ecuc_edit ${Y.map((B)=>B.file).join(", ")}`,output:N(Q)+D.join(`
18
- `)}}}),ecuc_validate:P({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. Without param defs it only warns."+U,args:{files:M(Z.array(Z.string()).optional()).describe("only report diagnostics in these files"),module:Z.string().optional().describe("only validate this module (e.g. Com)"),max:Z.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=K(J.directory,_.project_dir),Q=["validate"];for(let B of _.files??[])Q.push("--file",K(V,B));if(_.module)Q.push("--module",_.module);let Y=await G(V,Q,J.abort);if(!Y.ok)return N(Y)+R(Y);let $=Y.diagnostics,D=Y.validated?`Validated ${Y.modules.join(", ")||"(no modules)"} against ${Y.defsLoaded} module def(s): ${$.length?JSON.stringify(Y.summary):"no findings"}`:"NOT validated: no param definitions loaded.";return{title:`ecuc_validate ${JSON.stringify(Y.summary)}`,output:N(Y)+[D,...w($,_.max??200)].join(`
19
- `)}}}),arxml_diff:P({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:Z.string().describe("old file or directory"),b:Z.string().describe("new file or directory"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=K(J.directory,_.project_dir),Q=await G(V,["diff",K(V,_.a),K(V,_.b)],J.abort);if(!Q.ok)return R(Q);let{ok:Y,engine:$,ms:D,...B}=Q;return JSON.stringify(B,null,1)}}),autosar_trace:P({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. 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."+U,args:{query:Z.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:Z.number().int().optional().describe("restrict to one code data source"),limit:Z.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:Z.union([Z.string(),Z.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)")},async execute(_,J){let V=await S(_.workspace_id,J.abort),Q={query:_.query};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;if(_.limit!==void 0)Q.limit=_.limit;let Y=await T("/api/v1/mcp/autosar/trace",V,Q,J.abort),$=Y.matches??[],D=[`${$.length} match(es) for "${_.query}" in workspace ${V}`+(Y.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(Y.data_sources_queried)})`:""),...y(Y),JSON.stringify($,null,1)];if(!$.length)D.push("No match: try a SHORT-NAME, a full AUTOSAR path from ecuc_read, or the C symbol.");return{title:`autosar_trace ${_.query}`,output:D.join(`
20
- `)}}}),autosar_config_lookup:P({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. 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."+U,args:{path:Z.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:Z.number().int().optional().describe("restrict to one code data source"),workspace_id:Z.union([Z.string(),Z.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)")},async execute(_,J){let V=await S(_.workspace_id,J.abort),Q={path:_.path};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;let Y=await T("/api/v1/mcp/autosar/config-lookup",V,Q,J.abort),$=Y.matches??[];return{title:`autosar_config_lookup ${_.path}`,output:[`${$.length} match(es) for ${_.path} in workspace ${V}`,...y(Y),JSON.stringify($,null,1)].join(`
21
- `)}}}),autosar_generate:H("generate"),autosar_build:H("build")})}},D_=B_;export{D_ as Autosar};
16
+ ${B.map((F)=>F.diff).join(`
17
+ `)}`:"")}let Z=Q.changes??[],V=Q.validation,D=[`${Q.written?"Written":"Dry run (nothing written)"}: ${Q.applied} op(s), ${Z.length} file(s) changed.`,...(Q.notes??[]).map((B)=>`NOTE: ${B}`),...Z.map((B)=>B.diff)];if(V)D.push(V.validated?`Validation of touched files: ${JSON.stringify(V.summary)}`:"Validation skipped: no param defs loaded.",...p(V.diagnostics,50));return{title:`ecuc_edit ${Z.map((B)=>B.file).join(", ")}`,output:X(Q)+D.join(`
18
+ `)}}}),ecuc_validate:P({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. Without param defs it only warns."+I,args:{files:U($.array($.string()).optional()).describe("only report diagnostics in these files"),module:$.string().optional().describe("only validate this module (e.g. Com)"),max:$.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,H){let Y=K(H.directory,_.project_dir),Q=["validate"];for(let B of _.files??[])Q.push("--file",K(Y,B));if(_.module)Q.push("--module",_.module);let Z=await G(Y,Q,H.abort);if(!Z.ok)return X(Z)+M(Z);let V=Z.diagnostics,D=Z.validated?`Validated ${Z.modules.join(", ")||"(no modules)"} against ${Z.defsLoaded} module def(s): ${V.length?JSON.stringify(Z.summary):"no findings"}`:"NOT validated: no param definitions loaded.";return{title:`ecuc_validate ${JSON.stringify(Z.summary)}`,output:X(Z)+[D,...p(V,_.max??200)].join(`
19
+ `)}}}),arxml_diff:P({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:$.string().describe("old file or directory"),b:$.string().describe("new file or directory"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,H){let Y=K(H.directory,_.project_dir),Q=await G(Y,["diff",K(Y,_.a),K(Y,_.b)],H.abort);if(!Q.ok)return M(Q);let{ok:Z,engine:V,ms:D,...B}=Q;return JSON.stringify(B,null,1)}}),autosar_trace:P({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). 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."+I,args:{query:$.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:$.number().int().optional().describe("restrict to one code data source"),limit:$.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:$.union([$.string(),$.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:$.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(_,H){let Y=await S(_.workspace_id,H.abort),Q={query:_.query};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;if(_.limit!==void 0)Q.limit=_.limit;let Z=await T("/api/v1/mcp/autosar/trace",Y,Q,H.abort),V=Z.matches??[],D=await u(K(H.directory,_.project_dir),V.map((F)=>F.arxml).filter((F)=>!!F),H.abort),B=[`${V.length} match(es) for "${_.query}" in workspace ${Y}`+(Z.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(Z.data_sources_queried)})`:""),...h(Z),...D,JSON.stringify(V,null,1)];if(!V.length)B.push("No match: try a SHORT-NAME, a full AUTOSAR path from ecuc_read, or the C symbol.");return{title:`autosar_trace ${_.query}`,output:B.join(`
20
+ `)}}}),autosar_config_lookup:P({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. 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."+I,args:{path:$.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:$.number().int().optional().describe("restrict to one code data source"),workspace_id:$.union([$.string(),$.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)"),project_dir:$.string().optional().describe("local project root used to add I-PDU/direction for signals (default: session directory)")},async execute(_,H){let Y=await S(_.workspace_id,H.abort),Q={path:_.path};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;let Z=await T("/api/v1/mcp/autosar/config-lookup",Y,Q,H.abort),V=Z.matches??[],D=await u(K(H.directory,_.project_dir),V.map((B)=>B.element).filter((B)=>!!B),H.abort);return{title:`autosar_config_lookup ${_.path}`,output:[`${V.length} match(es) for ${_.path} in workspace ${Y}`,...h(Z),...D,JSON.stringify(V,null,1)].join(`
21
+ `)}}}),autosar_audit:P({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 Source/) 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:U($.array($.string()).optional()).describe("C files/dirs to scan (default: audit paths or Source/)"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,H){let Y=K(H.directory,_.project_dir),Q=await G(Y,["audit",...(_.paths??[]).map((F)=>K(Y,F))],H.abort);if(!Q.ok)return X(Q)+M(Q);let{findings:Z,unchecked:V,scanned:D}=Q,B=[`Scanned ${D.sources.length} C file(s), ${D.calls} BSW call(s): ${Z.length?JSON.stringify(Q.summary):"no findings"}`,...Z.map((F)=>`${F.file}:${F.line} ${F.severity.toUpperCase()} ${F.rule} ${F.call}
22
+ ${F.message}
23
+ evidence: ${JSON.stringify(F.evidence)}
24
+ spec: ${F.cite}${F.fix?`
25
+ fix: ${F.fix}`:""}`)];if(V.length)B.push(`Unchecked (${V.length}):`,...V.slice(0,50).map((F)=>` ${F.file}:${F.line} ${F.call} \u2014 ${F.reason}`));return{title:`autosar_audit ${JSON.stringify(Q.summary)}`,output:X(Q)+B.join(`
26
+ `)}}}),autosar_generate:J("generate"),autosar_build:J("build")})}},F_=D_;export{F_ as Autosar};
@@ -1,2 +1,2 @@
1
1
  """ECUC engine: load, read, edit, validate and diff AUTOSAR ECUC ARXML without regenerating files."""
2
- ENGINE_VERSION = "0.1.1"
2
+ ENGINE_VERSION = "0.1.2"
@@ -0,0 +1,452 @@
1
+ """Deterministic audit of BSW API calls in integration C code against the ECUC configuration.
2
+
3
+ Finds calls that take a configuration handle (Com signals/groups, NvM blocks, Dem events, PduR
4
+ source PDUs; Rte_Write_/Rte_Read_ macros resolved through the generated RTE headers), resolves the
5
+ handle to its ECUC container via the symbolic-name rule (<Mod>Conf_<ContainerDef>_<ShortName>,
6
+ TPS_ECUC_02108) or a raw number via the module's handle-ID parameter, and checks:
7
+
8
+ H2L-API-001 raw numeric handle (use the symbolic name)
9
+ H2L-API-002 unknown handle (symbol or number matches no configured container)
10
+ H2L-API-003 direction mismatch (send on a RECEIVE I-PDU signal, receive on a SEND one)
11
+ H2L-API-004 C object width differs from the signal's ComSignalType / ComBitSize
12
+ H2L-API-005 NvM NULL_PTR without a permanent RAM block or explicit synchronization
13
+
14
+ Width is checked only when the C type is known (compound literal, or a declaration in the same
15
+ translation unit); anything else is listed under "unchecked", never guessed.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ from dataclasses import dataclass
22
+ from typing import Optional
23
+
24
+ from .model import Elem, Project
25
+ from .values import parse_int
26
+
27
+ C_EXT = (".c", ".h")
28
+ SKIP_DIRS = {".git", "node_modules", ".hydron", "Output", "build", "Build"}
29
+
30
+
31
+ @dataclass
32
+ class Api:
33
+ module: str
34
+ defs: tuple # container def names the handle may refer to
35
+ handle_param: str
36
+ kind: str # send | receive | nvm | dem | pdur
37
+ data: Optional[int] # index of the data pointer argument
38
+ cite: str
39
+
40
+
41
+ APIS = {
42
+ "Com_SendSignal": Api("Com", ("ComSignal", "ComGroupSignal"), "ComHandleId", "send", 1, "SWS_Com_00197"),
43
+ "Com_ReceiveSignal": Api("Com", ("ComSignal", "ComGroupSignal"), "ComHandleId", "receive", 1, "SWS_Com_00198"),
44
+ "Com_SendSignalGroup": Api("Com", ("ComSignalGroup",), "ComHandleId", "send", None, "SWS_Com_00200"),
45
+ "Com_ReceiveSignalGroup": Api("Com", ("ComSignalGroup",), "ComHandleId", "receive", None, "SWS_Com_00201"),
46
+ "NvM_ReadBlock": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00454"),
47
+ "NvM_WriteBlock": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00455"),
48
+ "NvM_RestoreBlockDefaults": Api("NvM", ("NvMBlockDescriptor",), "NvMNvramBlockIdentifier", "nvm", 1, "SWS_NvM_00456"),
49
+ "Dem_SetEventStatus": Api("Dem", ("DemEventParameter",), "DemEventId", "dem", None, "SWS_Dem_00183"),
50
+ "Dem_ReportErrorStatus": Api("Dem", ("DemEventParameter",), "DemEventId", "dem", None, "SWS_Dem (Dem_ReportErrorStatus, pre-R4.2 releases)"),
51
+ }
52
+ PDUR = Api("PduR", ("PduRSrcPdu",), "PduRSourcePduHandleId", "pdur", None, "SWS_PduR_00406")
53
+ CALL = re.compile(r"\b(Com_(?:Send|Receive)Signal(?:Group)?|NvM_(?:ReadBlock|WriteBlock|RestoreBlockDefaults)|"
54
+ r"Dem_(?:SetEventStatus|ReportErrorStatus)|PduR_\w*Transmit|Rte_(?:Write|Read)_\w+)\s*\(")
55
+ NUM = re.compile(r"^\(?\s*(?:\(\s*\w+\s*\)\s*)?(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\s*\)?$")
56
+ SYM = re.compile(r"^(\w+?)Conf_(\w+?)_(\w+)$")
57
+ NULLS = {"NULL_PTR", "NULL", "0", "((void*)0)", "(void*)0"}
58
+ TYPE_BITS = {"boolean": 8, "uint8": 8, "sint8": 8, "uint16": 16, "sint16": 16, "uint32": 32, "sint32": 32,
59
+ "uint64": 64, "sint64": 64, "float32": 32, "float64": 64, "char": 8, "int8_t": 8, "uint8_t": 8,
60
+ "int16_t": 16, "uint16_t": 16, "int32_t": 32, "uint32_t": 32, "int64_t": 64, "uint64_t": 64,
61
+ "float": 32, "double": 64, "bool": 8}
62
+ SIG_BITS = {"BOOLEAN": 8, "UINT8": 8, "SINT8": 8, "UINT16": 16, "SINT16": 16, "UINT32": 32, "SINT32": 32,
63
+ "UINT64": 64, "SINT64": 64, "FLOAT32": 32, "FLOAT64": 64}
64
+
65
+
66
+ def strip_comments(src: str) -> str:
67
+ """Blank out comments and string literals, keeping offsets and newlines."""
68
+ out = list(src)
69
+ i, n = 0, len(src)
70
+ while i < n:
71
+ c = src[i]
72
+ if src.startswith("/*", i):
73
+ j = src.find("*/", i + 2)
74
+ j = n if j < 0 else j + 2
75
+ for k in range(i, j):
76
+ if out[k] != "\n":
77
+ out[k] = " "
78
+ i = j
79
+ elif src.startswith("//", i):
80
+ j = src.find("\n", i)
81
+ j = n if j < 0 else j
82
+ for k in range(i, j):
83
+ out[k] = " "
84
+ i = j
85
+ elif c in "\"'":
86
+ j = i + 1
87
+ while j < n and src[j] != c:
88
+ j += 2 if src[j] == "\\" else 1
89
+ for k in range(i + 1, min(j, n)):
90
+ if out[k] != "\n":
91
+ out[k] = " "
92
+ i = j + 1
93
+ else:
94
+ i += 1
95
+ return "".join(out)
96
+
97
+
98
+ def split_args(src: str, open_idx: int):
99
+ """Args of the call whose '(' is at open_idx -> (list of arg strings, index after ')')."""
100
+ depth, args, cur, i = 0, [], [], open_idx
101
+ while i < len(src):
102
+ c = src[i]
103
+ if c in "([{":
104
+ depth += 1
105
+ if depth > 1:
106
+ cur.append(c)
107
+ elif c in ")]}":
108
+ depth -= 1
109
+ if depth == 0:
110
+ args.append("".join(cur).strip())
111
+ return [a for a in args if a != "" or len(args) > 1], i + 1
112
+ cur.append(c)
113
+ elif c == "," and depth == 1:
114
+ args.append("".join(cur).strip())
115
+ cur = []
116
+ else:
117
+ cur.append(c)
118
+ i += 1
119
+ return None, i
120
+
121
+
122
+ def parse_defines(files: list) -> dict:
123
+ """NAME -> (params|None, body, file, line) for every #define in the given headers."""
124
+ out: dict = {}
125
+ rx = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(\w+)(\([^)]*\))?[ \t]*(.*)$")
126
+ for f in files:
127
+ try:
128
+ text = strip_comments(open(f, encoding="utf-8", errors="replace").read())
129
+ except OSError:
130
+ continue
131
+ lines = text.split("\n")
132
+ i = 0
133
+ while i < len(lines):
134
+ line = lines[i]
135
+ start = i
136
+ while line.endswith("\\") and i + 1 < len(lines):
137
+ i += 1
138
+ line = line[:-1] + " " + lines[i]
139
+ m = rx.match(line)
140
+ if m:
141
+ params = [p.strip() for p in m.group(2)[1:-1].split(",")] if m.group(2) else None
142
+ out.setdefault(m.group(1), (params, m.group(3).strip(), f, start + 1))
143
+ i += 1
144
+ return out
145
+
146
+
147
+ def _files(root: str, paths: list, exts=C_EXT, skip=SKIP_DIRS) -> list:
148
+ res = []
149
+ for p in paths:
150
+ full = p if os.path.isabs(p) else os.path.join(root, p)
151
+ if os.path.isfile(full):
152
+ res.append(full)
153
+ continue
154
+ for dp, dns, fns in os.walk(full):
155
+ dns[:] = sorted(d for d in dns if d not in skip and not d.startswith("."))
156
+ res.extend(os.path.join(dp, fn) for fn in sorted(fns) if fn.endswith(exts))
157
+ return res
158
+
159
+
160
+ class Auditor:
161
+ def __init__(self, project: Project, defines: dict):
162
+ self.p = project
163
+ self.defines = defines
164
+ self.findings: list = []
165
+ self.unchecked: list = []
166
+ self.calls = 0
167
+ self._handles: dict = {}
168
+
169
+ # ---------- ECUC lookups ----------
170
+ def module(self, name: str) -> list:
171
+ return [m for m in self.p.modules if (m.def_ref or "").rsplit("/", 1)[-1] == name or m.name == name]
172
+
173
+ def containers(self, module: str, defnames: tuple) -> list:
174
+ out = []
175
+ for m in self.module(module):
176
+ stack = list(m.children)
177
+ while stack:
178
+ e = stack.pop()
179
+ stack.extend(e.children)
180
+ if (e.def_ref or "").rsplit("/", 1)[-1] in defnames:
181
+ out.append(e)
182
+ return out
183
+
184
+ def val(self, e: Elem, name: str) -> Optional[str]:
185
+ for g in self.p.find(e.path):
186
+ for pv in g.params:
187
+ if pv.name == name:
188
+ return pv.value
189
+ return None
190
+
191
+ def by_handle(self, api: Api, num: int) -> list:
192
+ key = (api.module, api.defs)
193
+ if key not in self._handles:
194
+ idx: dict = {}
195
+ for e in self.containers(api.module, api.defs):
196
+ v = parse_int(self.val(e, api.handle_param) or "")
197
+ if v is not None:
198
+ idx.setdefault(v, []).append(e)
199
+ self._handles[key] = idx
200
+ return self._handles[key].get(num, [])
201
+
202
+ def by_symbol(self, api: Api, sym: str) -> Optional[Elem]:
203
+ m = SYM.match(sym)
204
+ if not m:
205
+ return None
206
+ mod, dname, short = m.groups()
207
+ if mod != api.module:
208
+ return None
209
+ for e in self.containers(api.module, (dname,)):
210
+ if e.name == short:
211
+ return e
212
+ return None
213
+
214
+ def symbol_of(self, e: Elem, api: Api) -> str:
215
+ return f"{api.module}Conf_{(e.def_ref or '').rsplit('/', 1)[-1]}_{e.name}"
216
+
217
+ def ipdus_of(self, sig: Elem) -> list:
218
+ """I-PDUs that reference this signal (or, for a group signal, its signal group)."""
219
+ targets = {sig.path}
220
+ if (sig.def_ref or "").endswith("/ComGroupSignal") and sig.parent is not None:
221
+ targets = {sig.parent.path}
222
+ out = []
223
+ for ip in self.containers("Com", ("ComIPdu",)):
224
+ for g in self.p.find(ip.path):
225
+ if any(pv.is_ref and pv.name in ("ComIPduSignalRef", "ComIPduSignalGroupRef") and (pv.value or "").strip() in targets
226
+ for pv in g.params):
227
+ out.append(ip)
228
+ break
229
+ return out
230
+
231
+ # ---------- findings ----------
232
+ def add(self, rule, sev, f, line, call, msg, cite, evidence, fix=None):
233
+ d = {"rule": rule, "severity": sev, "file": self.p.rel(f), "line": line, "call": call,
234
+ "message": msg, "cite": cite, "evidence": evidence}
235
+ if fix:
236
+ d["fix"] = fix
237
+ self.findings.append(d)
238
+
239
+ # ---------- per call ----------
240
+ def handle_value(self, expr: str):
241
+ """-> ("num", int) | ("sym", name, resolved_int|None) | ("expr", text)."""
242
+ e = expr.strip()
243
+ m = NUM.match(e)
244
+ if m:
245
+ return ("num", int(m.group(1), 0))
246
+ while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
247
+ e = e[1:-1].strip()
248
+ if re.fullmatch(r"\w+", e):
249
+ d = self.defines.get(e)
250
+ num = None
251
+ if d and d[0] is None:
252
+ mm = NUM.match(d[1])
253
+ num = int(mm.group(1), 0) if mm else None
254
+ return ("sym", e, num)
255
+ return ("expr", e)
256
+
257
+ def audit_call(self, f: str, line: int, name: str, args: list, unit: str, via: Optional[str] = None):
258
+ if name.startswith("Rte_"):
259
+ self.audit_rte(f, line, name, args, unit)
260
+ return
261
+ api = APIS.get(name) or (PDUR if name.startswith("PduR_") else None)
262
+ if api is None or not args:
263
+ return
264
+ self.calls += 1
265
+ call = f"{name}({', '.join(args)})" + (f" via {via}" if via else "")
266
+ h = self.handle_value(args[0])
267
+ target: Optional[Elem] = None
268
+ cite_sym = "TPS_ECUC_02108 symbolic name values"
269
+ if h[0] == "num":
270
+ hits = self.by_handle(api, h[1])
271
+ if not hits:
272
+ self.add("H2L-API-002", "error", f, line, call,
273
+ f"handle {args[0]} matches no {'/'.join(api.defs)} with {api.handle_param}={h[1]}",
274
+ f"{api.cite}; {cite_sym}", {"handleParam": api.handle_param, "value": h[1]})
275
+ return
276
+ target = hits[0]
277
+ sym = self.symbol_of(target, api)
278
+ self.add("H2L-API-001", "warning", f, line, call,
279
+ f"raw numeric handle {args[0]} = {target.path} ({api.handle_param}={h[1]}); use {sym} so the call "
280
+ "survives handle re-assignment", f"{cite_sym}; TPS_ECUConfiguration 3.4.1.1",
281
+ {"path": target.path, api.handle_param: h[1]}, fix=f"replace {args[0]} with {sym}")
282
+ elif h[0] == "sym":
283
+ target = self.by_symbol(api, h[1])
284
+ if target is None and h[2] is not None:
285
+ hits = self.by_handle(api, h[2])
286
+ target = hits[0] if hits else None
287
+ if target is None:
288
+ if SYM.match(h[1]) or h[2] is not None:
289
+ self.add("H2L-API-002", "error", f, line, call,
290
+ f"handle {h[1]} resolves to no configured {'/'.join(api.defs)} in {api.module}",
291
+ f"{api.cite}; {cite_sym}", {"symbol": h[1], "define": h[2]})
292
+ else:
293
+ self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call,
294
+ "reason": f"handle {h[1]} is not a configured symbol or constant"})
295
+ return
296
+ else:
297
+ self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": "handle is an expression"})
298
+ return
299
+ if api.kind in ("send", "receive"):
300
+ self.check_direction(f, line, call, api, target)
301
+ if api.data is not None and len(args) > api.data:
302
+ self.check_width(f, line, call, target, args[api.data], unit)
303
+ if api.kind == "nvm" and api.data is not None and len(args) > api.data:
304
+ self.check_nvm_null(f, line, call, name, target, args[api.data])
305
+
306
+ def audit_rte(self, f, line, name, args, unit):
307
+ d = self.defines.get(name)
308
+ if d is None or d[0] is None:
309
+ self.unchecked.append({"file": self.p.rel(f), "line": line, "call": f"{name}(...)",
310
+ "reason": "RTE API not a macro in the generated headers (function-mapped)"})
311
+ return
312
+ params, body = d[0], d[1]
313
+ subst = body
314
+ for prm, arg in zip(params, args):
315
+ subst = re.sub(rf"\b{re.escape(prm)}\b", lambda _m, a=arg: f"({a})", subst)
316
+ clean = strip_comments(subst)
317
+ for m in CALL.finditer(clean):
318
+ inner, _ = split_args(clean, m.end() - 1)
319
+ if inner is not None and not m.group(1).startswith("Rte_"):
320
+ self.audit_call(f, line, m.group(1), inner, unit, via=f"{name} ({self.p.rel(d[2])}:{d[3]})")
321
+
322
+ def check_direction(self, f, line, call, api, sig):
323
+ ipdus = self.ipdus_of(sig)
324
+ dirs = [(ip, (self.val(ip, "ComIPduDirection") or "").strip()) for ip in ipdus]
325
+ want = "SEND" if api.kind == "send" else "RECEIVE"
326
+ bad = [(ip, d) for ip, d in dirs if d and d != want]
327
+ if bad and not any(d == want for _, d in dirs):
328
+ ip, d = bad[0]
329
+ self.add("H2L-API-003", "error", f, line, call,
330
+ f"{sig.name} belongs to I-PDU {ip.name} with ComIPduDirection={d}; {call.split('(')[0]} needs a "
331
+ f"{want} signal (Com returns COM_SERVICE_NOT_AVAILABLE / E_NOT_OK)",
332
+ f"ECUC_Com_00493 ComIPduDirection; {api.cite}",
333
+ {"signal": sig.path, "ipdu": ip.path, "ComIPduDirection": d})
334
+
335
+ def c_type(self, expr: str, unit: str):
336
+ """Type of the object a data-pointer argument points to -> (type, array_len|None), or None if unknown."""
337
+ e = expr.strip()
338
+ while e.startswith("(") and e.endswith(")") and split_args("x" + e, 1)[1] == len(e) + 1:
339
+ e = e[1:-1].strip()
340
+ lit = re.fullmatch(r"&\s*\(\s*(\w+)\s*\)\s*\{.*\}", e, re.S) # &(uint8){ x }
341
+ if lit:
342
+ return lit.group(1), None
343
+ ref = re.fullmatch(r"&\s*(\w+)", e) # &var
344
+ elem = re.fullmatch(r"&\s*(\w+)\s*\[[^\]]*\]", e) # &buf[i]
345
+ bare = re.fullmatch(r"(\w+)", e) # buf (array decays) or a pointer variable
346
+ var = (ref or elem or bare).group(1) if (ref or elem or bare) else None
347
+ if var is None:
348
+ return None
349
+ decl = re.search(rf"\b(?:(?:static|volatile|const|extern)\s+)*(\w+)\s+(?:const\s+)?{re.escape(var)}"
350
+ rf"\s*(\[\s*(\w*)\s*\])?\s*[;=,)]", unit)
351
+ if not decl or decl.group(1) in ("return", "else", "case", "goto", "sizeof", "struct", "union", "enum"):
352
+ return None
353
+ is_array = bool(decl.group(2))
354
+ if ref:
355
+ return decl.group(1), (decl.group(3) if is_array else None)
356
+ if elem or (bare and is_array):
357
+ return decl.group(1), None
358
+ return None # bare non-array variable: a pointer whose pointee is not visible here
359
+
360
+ def check_width(self, f, line, call, sig, arg, unit):
361
+ typ = (self.val(sig, "ComSignalType") or "").strip()
362
+ if typ in ("UINT8_N", "UINT8_DYN"):
363
+ self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call, "reason": f"array signal type {typ}"})
364
+ return
365
+ expect = SIG_BITS.get(typ)
366
+ ct = self.c_type(arg, unit)
367
+ if expect is None or ct is None or ct[0] not in TYPE_BITS:
368
+ self.unchecked.append({"file": self.p.rel(f), "line": line, "call": call,
369
+ "reason": f"width unknown (C type {ct[0] if ct else '?'}, ComSignalType {typ or '?'})"})
370
+ return
371
+ cbits = TYPE_BITS[ct[0]]
372
+ if ct[1]:
373
+ n = parse_int(ct[1])
374
+ cbits = cbits * n if n is not None else cbits
375
+ if cbits != expect:
376
+ bs = self.val(sig, "ComBitSize")
377
+ self.add("H2L-API-004", "error", f, line, call,
378
+ f"{arg} is a {ct[0]}{'[' + ct[1] + ']' if ct[1] else ''} ({cbits} bits) but {sig.name} is "
379
+ f"ComSignalType={typ} ({expect // 8}-byte object, ComBitSize={bs}); Com accesses {expect // 8} "
380
+ f"byte(s) at that address — wrong signal or wrong variable",
381
+ "SWS_Com_00197/00198 SignalDataPtr points to an object of the ComSignalType; ECUC_Com_00127 ComSignalType",
382
+ {"signal": sig.path, "ComSignalType": typ, "ComBitSize": bs, "cType": ct[0]})
383
+
384
+ def check_nvm_null(self, f, line, call, name, blk, arg):
385
+ if arg.replace(" ", "") not in NULLS:
386
+ return
387
+ ram = (self.val(blk, "NvMRamBlockDataAddress") or "").strip()
388
+ sync = (self.val(blk, "NvMBlockUseSyncMechanism") or "").strip().lower() in ("true", "1")
389
+ if ram or sync:
390
+ return # valid: SWS_NvM_00898/00900 (permanent RAM) or 00899/00901 (explicit sync)
391
+ self.add("H2L-API-005", "error", f, line, call,
392
+ f"{name} with NULL_PTR on {blk.name}, which has no permanent RAM block (NvMRamBlockDataAddress unset) and "
393
+ "no explicit synchronization; NvM rejects the request (DET parameter error)",
394
+ "SWS_NvM_00898/00899 (ReadBlock), SWS_NvM_00900/00901 (WriteBlock), SWS_NvM_00278/00280",
395
+ {"block": blk.path, "NvMRamBlockDataAddress": None, "NvMBlockUseSyncMechanism": sync})
396
+
397
+
398
+ def audit(project: Project, paths: Optional[list] = None, headers: Optional[list] = None) -> dict:
399
+ root = project.root
400
+ cfg = {}
401
+ try:
402
+ import json
403
+ with open(os.path.join(root, ".hydron", "autosar.json")) as fh:
404
+ raw = json.load(fh).get("audit")
405
+ cfg = raw if isinstance(raw, dict) else ({"paths": raw} if isinstance(raw, list) else {})
406
+ except (OSError, ValueError):
407
+ pass
408
+ paths = paths or cfg.get("paths") or (["Source"] if os.path.isdir(os.path.join(root, "Source")) else ["."])
409
+ headers = headers or cfg.get("headers") or [d for d in ("Output",) if os.path.isdir(os.path.join(root, d))]
410
+ sources = [f for f in _files(root, paths) if f.endswith(".c")]
411
+ hdrs = _files(root, headers, exts=(".h",), skip={".git", "node_modules"}) + [f for f in _files(root, paths) if f.endswith(".h")]
412
+ a = Auditor(project, parse_defines(hdrs))
413
+ for f in sources:
414
+ try:
415
+ unit = strip_comments(open(f, encoding="utf-8", errors="replace").read())
416
+ except OSError:
417
+ continue
418
+ for m in CALL.finditer(unit):
419
+ ls = unit.rfind("\n", 0, m.start()) + 1
420
+ if unit[ls:m.start()].lstrip().startswith("#"):
421
+ continue # macro definition, not a call
422
+ before = unit[ls:m.start()].rstrip()
423
+ prev = re.search(r"(\w+)\s*$", before)
424
+ if prev and prev.group(1) not in ("return", "void") and not before.endswith(")"):
425
+ continue # a declaration/prototype ("Std_ReturnType Com_SendSignal(")
426
+ args, _ = split_args(unit, m.end() - 1)
427
+ if args is None:
428
+ continue
429
+ a.audit_call(f, unit.count("\n", 0, m.start()) + 1, m.group(1), args, unit)
430
+ a.findings.sort(key=lambda d: (d["file"], d["line"], d["rule"]))
431
+ summary: dict = {}
432
+ for d in a.findings:
433
+ summary[d["severity"]] = summary.get(d["severity"], 0) + 1
434
+ return {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls},
435
+ "summary": summary, "findings": a.findings, "unchecked": a.unchecked}
436
+
437
+
438
+ def signal_owners(project: Project, paths: list) -> dict:
439
+ """AUTOSAR path of a ComSignal / ComGroupSignal -> [{ipdu, direction, pdu}] from the local ECUC."""
440
+ a = Auditor(project, {})
441
+ out: dict = {}
442
+ for p in paths:
443
+ g = project.find(p)
444
+ if not g:
445
+ out[p] = None # not in the local checkout
446
+ continue
447
+ sig = g[0]
448
+ if not (sig.def_ref or "").endswith(("/ComSignal", "/ComGroupSignal", "/ComSignalGroup")):
449
+ continue
450
+ out[p] = [{"ipdu": ip.path, "direction": (a.val(ip, "ComIPduDirection") or "").strip() or None,
451
+ "pdu": (a.val(ip, "ComPduIdRef") or "").strip() or None} for ip in a.ipdus_of(sig)]
452
+ return out
@@ -50,10 +50,15 @@ def main(argv=None) -> int:
50
50
  v.add_argument("--file", action="append")
51
51
  v.add_argument("--module")
52
52
  v.add_argument("--max", type=int, default=0, help="cap the number of diagnostics returned (0 = all)")
53
+ au = sub.add_parser("audit")
54
+ au.add_argument("paths", nargs="*", help="C files/dirs to scan (default: audit.paths in .hydron/autosar.json, else Source/)")
55
+ au.add_argument("--headers", action="append", help="generated header dirs for symbols and RTE macros (default Output/)")
56
+ so = sub.add_parser("signal-owners")
57
+ so.add_argument("paths", nargs="+")
53
58
  d = sub.add_parser("diff")
54
59
  d.add_argument("a")
55
60
  d.add_argument("b")
56
- for p in (r, e, v, d): # allow --json after the subcommand too
61
+ for p in (r, e, v, d, au, so): # allow --json after the subcommand too
57
62
  p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
58
63
  a = ap.parse_args(argv)
59
64
  t0 = time.time()
@@ -84,6 +89,15 @@ def run(a) -> dict:
84
89
  return diff(a.a, a.b, rel)
85
90
  from .model import Project
86
91
  project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit"))
92
+ if a.cmd == "signal-owners":
93
+ from .audit import signal_owners
94
+ return {"owners": signal_owners(project, a.paths)}
95
+ if a.cmd == "audit":
96
+ from .audit import audit
97
+ res = audit(project, paths=a.paths or None, headers=a.headers)
98
+ if project.warnings:
99
+ res["warnings"] = project.warnings
100
+ return res
87
101
  if a.cmd == "info":
88
102
  s = project.sources
89
103
  return {"root": s.root, "origin": s.origin, "config": [project.rel(f) for f in s.config],
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ecuc-engine"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Load, read, edit in place, validate and diff AUTOSAR ECUC ARXML (Hydron AUTOSAR pack)"
5
5
  requires-python = ">=3.10"
6
6
  # The core is stdlib-only; autosar-data adds a strict schema check of every edited file.
@@ -41,6 +41,7 @@ in `Output/` is derived from it; in-house code in `Source/` calls the generated
41
41
  | Review a change / PR / two versions | `arxml_diff a b` |
42
42
  | Which C symbols does this config produce, who calls them / where does this symbol come from | `autosar_trace query=…` |
43
43
  | Refs in/out, children of an element in the ingested repo | `autosar_config_lookup path=…` |
44
+ | Review C code that calls Com/NvM/Dem/PduR/RTE APIs | `autosar_audit` (first), then confirm with `autosar_trace` / `ecuc_read` |
44
45
  | Regenerate code | `autosar_generate` (configured command, asks permission) |
45
46
  | Compile/link | `autosar_build` |
46
47
  | Spec questions (SWS/TPS requirement text) | the project's ingested docs search tools |
@@ -49,6 +50,28 @@ in `Output/` is derived from it; in-house code in `Source/` calls the generated
49
50
  `ecuc_read (schema) → ecuc_edit → ecuc_validate → autosar_generate → arxml_diff / inspect Output → autosar_build → autosar_trace (impact on Source/)`.
50
51
  Details and failure handling: `references/workflow.md`.
51
52
 
53
+ ## Integration review recipe (C code against the configuration)
54
+ 1. `autosar_audit` (scope with `paths` if asked about specific files). It resolves every BSW call's handle
55
+ through the ECUC and reports raw numeric handles, unknown handles, direction mismatches, C-width vs
56
+ ComSignalType mismatches and invalid NvM NULL_PTR use, each with evidence and a spec citation.
57
+ 2. Confirm each finding before reporting it: `ecuc_read <signal/block>` for the configured values and
58
+ `autosar_trace <symbol>` for the owning I-PDU, generated symbol and other callers.
59
+ 3. Review what the audit lists as *unchecked* by hand (pointer arguments, expressions as handles), and
60
+ look for logic issues it cannot see (wrong signal chosen although types match, missing error handling).
61
+ 4. Report: file:line, the call, the configured facts (AUTOSAR paths + values), the spec ID, the fix.
62
+
63
+ Facts that decide common review calls:
64
+ - **Direction lives on the I-PDU**, not the signal: a ComSignal is TX or RX through the `ComIPduDirection`
65
+ of the ComIPdu that references it (`ComIPduSignalRef`). Sending an RX signal (or receiving a TX one)
66
+ fails at runtime (ECUC_Com_00493, SWS_Com_00197/00198).
67
+ - **NvM `NULL_PTR` is valid** for `NvM_ReadBlock`/`NvM_WriteBlock` when the block has a permanent RAM block
68
+ (`NvMRamBlockDataAddress` set) or explicit synchronization: NvM then uses that RAM block
69
+ (SWS_NvM_00898/00899, SWS_NvM_00900/00901). It is an error only for a block with neither.
70
+ - The data pointer of `Com_SendSignal`/`Com_ReceiveSignal` must point to an object of the signal's
71
+ `ComSignalType` size; a smaller variable is overread/overwritten.
72
+ - Raw numeric handles break silently when the generator re-assigns IDs: use the symbolic name
73
+ `<Mod>Conf_<ContainerDef>_<ShortName>` (TPS_ECUC_02108).
74
+
52
75
  ## References (load when relevant)
53
76
  - `references/ecuc-model.md` — module/container/param/ref values vs defs, DEFINITION-REF, ECUC JSON, multiplicity, choice containers, refs vs foreign refs.
54
77
  - `references/symbolic-names.md` — the symbolic-name rule (TPS_ECUC_02108) that links config to generated `#define`s, handle IDs.