@h2loop/autosar 0.1.2 → 1.0.1

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": "0.1.2",
3
+ "version": "1.0.1",
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,26 +1,26 @@
1
1
  // @bun
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?`
2
+ import{isAbsolute as Q_,join as l,resolve as V_}from"path";import{tool as X}from"@kilocode/plugin";import{homedir as c}from"os";import{join as G}from"path";var i=()=>process.env.HYDRON_TEST_HOME||c(),C=(q,H)=>process.env[q]||G(i(),H),g=()=>G(C("XDG_CONFIG_HOME",".config"),"hydron-cli"),n=()=>G(C("XDG_DATA_HOME",".local/share"),"hydron-cli"),o=()=>G(C("XDG_STATE_HOME",".local/state"),"hydron-cli");async function S(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 S(G(g(),"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 t(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 a(q){let H=t(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 y(){let H=(await S(G(n(),"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 m(){let q=await y();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 H=await fetch(`${await O()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await m()}`},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 z=()=>G(o(),"h2loop-project.json");async function T(q,H){let _=q===void 0||q===null?"":String(q).trim();if(/^\d+$/.test(_))return _;if(_){let V=await v(H),B=V.filter((F)=>F.name===_),D=B.length?B:V.filter((F)=>F.name?.toLowerCase()===_.toLowerCase());if(D.length===1)return String(D[0].id);if(D.length>1)throw Error(`workspace "${_}" is ambiguous: ${D.map((F)=>`${F.name} (id ${F.id})`).join(", ")}; pass the numeric id`);let W=V.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 ${V.length} project(s)`))}let J=await S(z());if(!J)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let Y=await y();if(J.project&&typeof J.project==="object"){let V=J.project;if(typeof J.account==="string"&&J.account!==await a(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=J;if(Q.id===void 0)throw Error(`${z()} holds no project id; select a project again or pass workspace_id.`);if(!(await v(H)).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 A(q,H,_,J){let Y=`${await O()}${q}?workspace_id=${encodeURIComponent(H)}`,Q=await fetch(Y,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await m()}`},body:JSON.stringify(_),signal:J?AbortSignal.any([J,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 L=/\.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 h(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((Y)=>typeof Y?.filePath==="string"&&L.test(Y.filePath)):!1;if(L.test(_)||J)return`${b} (${q} on ${_||"an .arxml file"})`;return}if(q==="apply_patch"||q==="patch"){let J=String(H.patchText??H.patch??""),Y=J.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??J.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return Y?`${b} (${q} on ${Y[1].trim()})`:void 0}if(q==="bash"){let J=String(H.command??"");if(!L.test(J))return;let Y=/\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(Y||Q)return`${b} (bash: ${J.slice(0,120)})`}return}import{existsSync as s}from"fs";import{dirname as e,join as E}from"path";import{fileURLToPath as r}from"url";var __="autosar-data==0.17.0";function q_(){let q=e(r(import.meta.url)),H=[E(q,"..","python"),E(q,"..","..","python")].find((_)=>s(E(_,"ecuc_engine","__main__.py")));if(!H)throw Error(`ECUC engine not found next to ${q} (expected ../python/ecuc_engine)`);return H}async function H_(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 J_(){if(R.cmd!==void 0)return R.cmd;let q=process.env.AUTOSAR_PYTHON?.trim(),_=q?[[q]]:[["uv","run","--no-project","--quiet","--with",__,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let J of _)if(await H_(J))return R.cmd=J,J;return R.cmd=null,null}async function U(q,H,_){let J=await J_();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 Y=q_(),Q=Bun.spawn([...J,"-m","ecuc_engine","--root",q,...H,"--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 B=Z.trim().split(`
3
+ `).filter(Boolean).at(-1);if(!B)return{ok:!1,error:`engine produced no output (exit ${Q.exitCode}): ${V.trim().slice(-2000)}`};try{return JSON.parse(B)}catch(D){return{ok:!1,error:`engine output was not JSON (${String(D)}): ${B.slice(0,500)} ${V.slice(-500)}`}}}var $=X.schema,Y_=["# 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 f(q){if(typeof q!=="string")return q;let H=q.trim();if(!H.startsWith("[")&&!H.startsWith("{")&&!H.startsWith('"'))return q;try{return f(JSON.parse(H))}catch{return q}}var j=(q)=>$.preprocess(f,q);function k(q){let H=f(q);if(H===void 0||H===null||H==="")return;if(typeof H==="string")return[H];if(Array.isArray(H))return H.map(String).filter((_)=>_.trim());return[String(H)]}var u=()=>$.preprocess(k,$.array($.string()).optional()),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 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 M(q){let H=q.didYouMean?`
5
5
  Did you mean: ${q.didYouMean.join(", ")}`:"",_=q.schema?`
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(`
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(`
7
7
  `)+`
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?`
8
+ `:""}function P(q,H){if(!H)return q;return Q_(H)?H:V_(q,H)}async function Z_(q){let H=Bun.file(l(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 $_(q,H){let _=(await Z_(q))[H];if(typeof _==="string")return{command:_};if(_&&typeof _==="object"&&typeof _.command==="string")return _;return}function p(q,H=150){let _=q.split(`
9
+ `);return _.length>H?[`\u2026 (${_.length-H} earlier lines omitted)`,..._.slice(-H)].join(`
10
+ `):q}async function B_(q,H,_,J){let Y=process.platform==="win32"?["cmd","/d","/s","/c",q]:["sh","-c",q],Q=Date.now(),Z=Bun.spawn(Y,{cwd:H,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([J,AbortSignal.timeout(_*1000)])}),[V,B]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text()]);return{code:await Z.exited,out:V,err:B,secs:Math.round((Date.now()-Q)/1000)}}async function x(q,H,_){let J=H.filter((V)=>V?.path&&/\/Com(Group)?Signal$/.test(String(V.definition_ref??""))&&V.ipdu===void 0&&V.direction===void 0);if(!J.length)return[];let Y=await U(q,["signal-owners",...new Set(J.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 J){let B=Q[V.path];if(B===null||B===void 0){Z.push(`NOTE: ${V.path} is not in the local checkout (${q}); owning I-PDU/direction unknown`);continue}V.ipdu_local=B,Z.push(B.length?`${V.path}: owning I-PDU ${B.map((D)=>`${D.ipdu} (ComIPduDirection=${D.direction??"?"})`).join(", ")} [local ECUC]`:`${V.path}: referenced by no ComIPdu (ComIPduSignalRef) in the local ECUC`)}return Z}var d=(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 D_(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 F_(q){for(let[H,_]of Object.entries(q)){let J=_.execute;_.execute=(Y,Q)=>D_(H,()=>J(Y,Q))}return q}var W_=async(q)=>{let H=(_)=>X({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(J,Y){let Q=P(Y.directory,J.project_dir),Z=await $_(Q,_);if(!Z)return`No \`${_}\` command configured. Add it to ${l(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=J.args??"",B=Z.command.replaceAll("{module}",J.module??""),D=B.includes("{args}")?B.replaceAll("{args}",V):`${B}${V?` ${V}`:""}`,W=P(Q,Z.cwd);await Y.ask({permission:`autosar_${_}`,patterns:[D],always:[Z.command],metadata:{command:D,cwd:W}}),Y.metadata({title:`${_}: ${D}`});let F=await B_(D,W,Z.timeoutSec??3600,Y.abort),K=F.code===0?"OK":`FAILED (exit ${F.code})`;return{title:`autosar_${_} ${K}`,output:[`$ ${D} (cwd ${W}, ${F.secs}s) -> ${K}`,F.out.trim()?`--- stdout (tail) ---
12
+ ${p(F.out)}`:"",F.err.trim()?`--- stderr (tail) ---
13
+ ${p(F.err,80)}`:""].filter(Boolean).join(`
14
+ `),metadata:{exit:F.code}}}});return{"tool.execute.before":async(_,J)=>{let Y=h(_.tool,J.args);if(Y)throw Error(Y)},"experimental.chat.system.transform":async(_,J)=>{J.system.push(Y_)},tool:F_({ecuc_read:X({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(_,J){let Y=["read"];if(_.path)Y.push(_.path);if(Y.push("--depth",String(_.depth??2)),_.schema)Y.push("--schema");let Q=await U(P(J.directory,_.project_dir),Y,J.abort);if(!Q.ok)return N(Q)+M(Q);let{ok:Z,engine:V,ms:B,warnings:D,...W}=Q;return N(Q)+JSON.stringify(W,null,1)}}),ecuc_edit:X({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:j($.array(j($.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:j($.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(_,J){let Y=["edit","--ops",JSON.stringify(_.ops)];if(_.dry_run)Y.push("--dry-run");if(_.force)Y.push("--force");let Q=await U(P(J.directory,_.project_dir),Y,J.abort);if(!Q.ok){let D=Q.changes??[];return N(Q)+M(Q)+(D.length?`
15
15
  Rejected diff:
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}
16
+ ${D.map((W)=>W.diff).join(`
17
+ `)}`:"")}let Z=Q.changes??[],V=Q.validation,B=[`${Q.written?"Written":"Dry run (nothing written)"}: ${Q.applied} op(s), ${Z.length} file(s) changed.`,...(Q.notes??[]).map((D)=>`NOTE: ${D}`),...Z.map((D)=>D.diff)];if(V)B.push(V.validated?`Validation of touched files: ${JSON.stringify(V.summary)}`:"Validation skipped: no param defs loaded.",...w(V.diagnostics,50));return{title:`ecuc_edit ${Z.map((D)=>D.file).join(", ")}`,output:N(Q)+B.join(`
18
+ `)}}}),ecuc_validate:X({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."+I,args:{files:u().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:$.string().optional().describe("only validate this module (e.g. Com); an unknown module is an ERROR, not clean"),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(_,J){let Y=P(J.directory,_.project_dir),Q=["validate"];for(let K of k(_.files)??[])Q.push("--file",K);if(_.module)Q.push("--module",_.module);let Z=await U(Y,Q,J.abort);if(!Z.ok)return N(Z)+M(Z);let{diagnostics:V,scope:B}=Z,D=V.filter((K)=>K.rule!=="H2L-DISCOVERY"),W=Z.validated?`Validated ${Z.modules.join(", ")||"(no modules)"} against ${Z.defsLoaded} module def(s): ${D.length?JSON.stringify(Z.summary):"no findings"}`:`Param definitions NOT loaded: only the H2L-* semantic rules ran. ${D.length?JSON.stringify(Z.summary):"no findings"}`,F=[`Loaded ${B.configFiles} config file(s)`+(B.filter?`; files filter ${JSON.stringify(B.filter.files)} matched ${B.filter.matchedCount}`:"")+(B.module?`; module filter ${B.module}`:"")+":",...B.files.slice(0,30).map((K)=>` ${K.file} (${K.modules.join(", ")})`),...B.files.length>30||B.filesTruncated?[` \u2026 ${B.files.length-30+(B.filesTruncated??0)} more`]:[]];return{title:`ecuc_validate ${JSON.stringify(Z.summary)}`,output:N(Z)+[W,...w(V,_.max??200),...F].join(`
19
+ `)}}}),arxml_diff:X({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, directory (recursive) or glob, relative to the project root or absolute"),b:$.string().describe("new: file, directory (recursive) or glob, relative to the project root or absolute"),project_dir:$.string().optional().describe("project root (default: session directory)")},async execute(_,J){let Y=P(J.directory,_.project_dir),Q=await U(Y,["diff",_.a,_.b],J.abort);if(!Q.ok)return M(Q);let{ok:Z,engine:V,ms:B,...D}=Q;return JSON.stringify(D,null,1)}}),autosar_trace:X({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(_,J){let Y=await T(_.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 Z=await A("/api/v1/mcp/autosar/trace",Y,Q,J.abort),V=Z.matches??[],B=await x(P(J.directory,_.project_dir),V.map((W)=>W.arxml).filter((W)=>!!W),J.abort),D=[`${V.length} match(es) for "${_.query}" in workspace ${Y}`+(Z.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(Z.data_sources_queried)})`:""),...d(Z),...B,JSON.stringify(V,null,1)];if(!V.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:X({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(_,J){let Y=await T(_.workspace_id,J.abort),Q={path:_.path};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;let Z=await A("/api/v1/mcp/autosar/config-lookup",Y,Q,J.abort),V=Z.matches??[],B=await x(P(J.directory,_.project_dir),V.map((D)=>D.element).filter((D)=>!!D),J.abort);return{title:`autosar_config_lookup ${_.path}`,output:[`${V.length} match(es) for ${_.path} in workspace ${Y}`,...d(Z),...B,JSON.stringify(V,null,1)].join(`
21
+ `)}}}),autosar_audit:X({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:u().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:$.string().optional().describe("project root (default: session directory)")},async execute(_,J){let Y=P(J.directory,_.project_dir),Q=await U(Y,["audit",...k(_.paths)??[]],J.abort);if(!Q.ok)return N(Q)+M(Q);let Z=(Q.notes??[]).map((F)=>`NOTE: ${F}`),V=Q.findings,B=Q.unchecked,D=Q.scanned,W=[`Scanned ${D.sources.length} C file(s), ${D.calls} BSW call(s): ${V.length?JSON.stringify(Q.summary):"no findings"}`,...V.map((F)=>`${F.file}:${F.line} ${F.severity.toUpperCase()} ${F.rule} ${F.call}
22
22
  ${F.message}
23
23
  evidence: ${JSON.stringify(F.evidence)}
24
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};
25
+ fix: ${F.fix}`:""}`)];if(B.length)W.push(`Unchecked (${B.length}):`,...B.slice(0,50).map((F)=>` ${F.file}:${F.line} ${F.call} \u2014 ${F.reason}`));return W.push(...Z),{title:`autosar_audit ${JSON.stringify(Q.summary)}`,output:N(Q)+W.join(`
26
+ `)}}}),autosar_generate:H("generate"),autosar_build:H("build")})}},K_=W_;export{K_ 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.2"
2
+ ENGINE_VERSION = "1.0.1"
@@ -24,8 +24,23 @@ from typing import Optional
24
24
  from .model import Elem, Project
25
25
  from .values import parse_int
26
26
 
27
- C_EXT = (".c", ".h")
28
- SKIP_DIRS = {".git", "node_modules", ".hydron", "Output", "build", "Build"}
27
+ from . import paths
28
+
29
+ GENERATED_MARK = re.compile(r"generated by|automatically generated|auto-generated|this file (?:is|was) generated|do not edit",
30
+ re.IGNORECASE)
31
+
32
+
33
+ def is_generated(path: str) -> bool:
34
+ """Generator output, recognised by the file itself (name pattern or header banner), not its folder."""
35
+ name = os.path.basename(path)
36
+ if re.search(r"(?:_(?:Cfg|PBcfg|Lcfg)\.[ch]|^Rte\w*\.c)$", name, re.IGNORECASE):
37
+ return True
38
+ try:
39
+ with open(path, "rb") as fh:
40
+ head = fh.read(2048).decode("utf-8", "replace")
41
+ except OSError:
42
+ return False
43
+ return bool(GENERATED_MARK.search(head))
29
44
 
30
45
 
31
46
  @dataclass
@@ -144,19 +159,6 @@ def parse_defines(files: list) -> dict:
144
159
  return out
145
160
 
146
161
 
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
162
  class Auditor:
161
163
  def __init__(self, project: Project, defines: dict):
162
164
  self.p = project
@@ -395,7 +397,7 @@ class Auditor:
395
397
  {"block": blk.path, "NvMRamBlockDataAddress": None, "NvMBlockUseSyncMechanism": sync})
396
398
 
397
399
 
398
- def audit(project: Project, paths: Optional[list] = None, headers: Optional[list] = None) -> dict:
400
+ def audit(project: Project, paths_arg: Optional[list] = None, headers_arg: Optional[list] = None) -> dict:
399
401
  root = project.root
400
402
  cfg = {}
401
403
  try:
@@ -405,10 +407,24 @@ def audit(project: Project, paths: Optional[list] = None, headers: Optional[list
405
407
  cfg = raw if isinstance(raw, dict) else ({"paths": raw} if isinstance(raw, list) else {})
406
408
  except (OSError, ValueError):
407
409
  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")]
410
+ explicit = paths_arg or cfg.get("paths")
411
+ notes: list = []
412
+ tree = paths.walk(root, paths.is_c)
413
+ notes.extend(tree.notes(root))
414
+ if explicit:
415
+ scan = paths.expand(root, explicit, paths.is_c)
416
+ for p in paths.patterns(root, explicit):
417
+ if not p.expand(paths.is_c):
418
+ notes.append(f"audit path {p.raw!r} matched no C file under {root}")
419
+ sources = [f for f in scan if f.lower().endswith(".c")]
420
+ generated = []
421
+ else: # whole tree, minus generator output (recognised by the file, not the folder)
422
+ cfiles = [f for f in tree.files if f.lower().endswith(".c")]
423
+ generated = [f for f in cfiles if is_generated(f)]
424
+ gen = set(generated)
425
+ sources = [f for f in cfiles if f not in gen]
426
+ extra = paths.expand(root, headers_arg or cfg.get("headers") or [], paths.is_c)
427
+ hdrs = sorted({f for f in tree.files + extra if f.lower().endswith(".h")})
412
428
  a = Auditor(project, parse_defines(hdrs))
413
429
  for f in sources:
414
430
  try:
@@ -431,8 +447,15 @@ def audit(project: Project, paths: Optional[list] = None, headers: Optional[list
431
447
  summary: dict = {}
432
448
  for d in a.findings:
433
449
  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}
450
+ res = {"scanned": {"sources": [project.rel(f) for f in sources], "headers": len(hdrs), "calls": a.calls,
451
+ "generatedSkipped": len(generated)},
452
+ "summary": summary, "findings": a.findings, "unchecked": a.unchecked}
453
+ if notes:
454
+ res["notes"] = notes
455
+ if explicit and not sources:
456
+ res["error"] = (f"audit paths {list(explicit)} matched no .c file under {root}; nothing was audited. "
457
+ "Pass files, directories or globs relative to the project root, or omit paths.")
458
+ return res
436
459
 
437
460
 
438
461
  def signal_owners(project: Project, paths: list) -> dict:
@@ -81,12 +81,12 @@ def run(a) -> dict:
81
81
  return {"error": f"ops is not valid JSON: {ex}"}
82
82
  if a.cmd == "diff":
83
83
  from .diff import diff
84
- for x in (a.a, a.b):
85
- if not os.path.exists(x):
86
- return {"error": f"{x} does not exist"}
87
84
  root = os.path.abspath(a.root)
88
85
  rel = lambda f: os.path.relpath(f, root) if not os.path.relpath(f, root).startswith("..") else f # noqa: E731
89
- return diff(a.a, a.b, rel)
86
+ try:
87
+ return diff(a.a, a.b, rel, root)
88
+ except FileNotFoundError as ex:
89
+ return {"error": str(ex)}
90
90
  from .model import Project
91
91
  project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit"))
92
92
  if a.cmd == "signal-owners":
@@ -94,7 +94,7 @@ def run(a) -> dict:
94
94
  return {"owners": signal_owners(project, a.paths)}
95
95
  if a.cmd == "audit":
96
96
  from .audit import audit
97
- res = audit(project, paths=a.paths or None, headers=a.headers)
97
+ res = audit(project, paths_arg=a.paths or None, headers_arg=a.headers)
98
98
  if project.warnings:
99
99
  res["warnings"] = project.warnings
100
100
  return res
@@ -9,7 +9,13 @@ from __future__ import annotations
9
9
  import os
10
10
  from typing import Optional
11
11
 
12
- from .model import MODULE_VALUES, CONTAINER_VALUE, _walk_arxml, build_file_model
12
+ from . import paths
13
+ from .model import MODULE_VALUES, CONTAINER_VALUE, build_file_model
14
+
15
+
16
+ def _expand(target: str, root: Optional[str] = None) -> list:
17
+ """A dir (recursive), a glob (** allowed) or a file, relative to root -> ARXML files."""
18
+ return paths.expand(root or os.getcwd(), [target], paths.is_arxml)
13
19
  from .values import parse_int, parse_float
14
20
  from .xmltree import Node
15
21
 
@@ -42,8 +48,10 @@ def _canon(node: Node, top: bool = True) -> str:
42
48
  return f"<{node.tag}[{attrs}]{txt}{''.join(kids)}>"
43
49
 
44
50
 
45
- def _side(target: str) -> dict:
46
- files = [target] if os.path.isfile(target) else sorted(_walk_arxml(target))
51
+ def _side(target: str, root: Optional[str] = None) -> dict:
52
+ files = _expand(target, root)
53
+ if not files:
54
+ raise FileNotFoundError(f"{target!r} matched no .arxml file (file, directory or glob, relative to {root or os.getcwd()})")
47
55
  out: dict = {}
48
56
  for f in files:
49
57
  with open(f, "rb") as fh:
@@ -82,8 +90,8 @@ def _side(target: str) -> dict:
82
90
  return out
83
91
 
84
92
 
85
- def diff(a: str, b: str, rel=lambda f: f) -> dict:
86
- A, B = _side(a), _side(b)
93
+ def diff(a: str, b: str, rel=lambda f: f, root: Optional[str] = None) -> dict:
94
+ A, B = _side(a, root), _side(b, root)
87
95
  added = [{"path": p, "tag": B[p]["tag"], "at": f"{rel(B[p]['file'])}:{B[p]['line']}"} for p in sorted(set(B) - set(A))]
88
96
  removed = [{"path": p, "tag": A[p]["tag"], "at": f"{rel(A[p]['file'])}:{A[p]['line']}"} for p in sorted(set(A) - set(B))]
89
97
  changed = []
@@ -16,9 +16,10 @@ from dataclasses import dataclass, field
16
16
  from typing import Optional
17
17
 
18
18
  from . import defs as D
19
+ from . import paths
19
20
  from .xmltree import Node, parse
20
21
 
21
- SKIP_DIRS = {".git", "node_modules", ".hydron", ".venv", "__pycache__", ".cache", ".idea", ".vscode"}
22
+ SKIP_DIRS = paths.TOOLING_DIRS # kept for importers; discovery lives in paths.py
22
23
  PROJECT_CONFIG = os.path.join(".hydron", "autosar.json")
23
24
 
24
25
  MODULE_VALUES = "ECUC-MODULE-CONFIGURATION-VALUES"
@@ -102,30 +103,28 @@ def _scan_markers(path: str) -> set:
102
103
  return found
103
104
 
104
105
 
105
- def _walk_arxml(root: str):
106
- # Sorted walk: os.walk order is filesystem-dependent (macOS and Linux differ),
107
- # and load order decides which definition of a path wins.
108
- for dp, dns, fns in os.walk(root):
109
- dns[:] = sorted(d for d in dns if d not in SKIP_DIRS and not d.startswith("."))
110
- for fn in sorted(fns):
111
- if fn.lower().endswith(".arxml"):
112
- yield os.path.join(dp, fn)
106
+ def _walk_arxml(root: str, notes: Optional[list] = None):
107
+ """Every ARXML file under root (any depth/folder name/extension case); sorted, deterministic."""
108
+ w = paths.walk(root, paths.is_arxml)
109
+ if notes is not None:
110
+ notes.extend(w.notes(root))
111
+ return w.files
113
112
 
114
113
 
115
- def _globs(root: str, pats) -> list:
114
+ def _globs(root: str, pats, warnings: Optional[list] = None, what: str = "") -> list:
115
+ """Files/dirs/globs (relative or absolute, trailing slash optional) -> ARXML files on disk."""
116
+ pats = [pats] if isinstance(pats, str) else list(pats or [])
116
117
  out = []
117
- for p in pats or []:
118
- p = os.path.expanduser(p)
119
- full = p if os.path.isabs(p) else os.path.join(root, p)
120
- if os.path.isdir(full):
121
- out.extend(_walk_arxml(full))
122
- else:
123
- out.extend(sorted(glob.glob(full, recursive=True)))
118
+ for p in paths.patterns(root, pats):
119
+ got = [f for f in p.expand(paths.is_arxml) if paths.is_arxml(f)]
120
+ if not got and warnings is not None:
121
+ warnings.append(f"{what or 'path'} {p.raw!r} matched no .arxml file under {root}")
122
+ out.extend(got)
124
123
  seen, res = set(), []
125
124
  for f in out:
126
- a = os.path.abspath(f)
127
- if a not in seen and os.path.isfile(a):
128
- seen.add(a)
125
+ a, real = os.path.abspath(f), os.path.realpath(f)
126
+ if real not in seen and os.path.isfile(a):
127
+ seen.add(real)
129
128
  res.append(a)
130
129
  return res
131
130
 
@@ -138,6 +137,7 @@ class Sources:
138
137
  input: list
139
138
  warnings: list
140
139
  origin: dict # which mechanism found each set
140
+ notes: list = field(default_factory=list) # info: hidden dirs included, cycles, caps
141
141
 
142
142
 
143
143
  def discover(root: str, config_files: Optional[list] = None, def_files: Optional[list] = None) -> Sources:
@@ -153,30 +153,31 @@ def discover(root: str, config_files: Optional[list] = None, def_files: Optional
153
153
  warnings.append(f"{PROJECT_CONFIG} is not valid JSON ({e}); ignoring it")
154
154
  origin = {}
155
155
  scanned = None
156
+ notes: list = []
156
157
 
157
158
  def scan():
158
159
  nonlocal scanned
159
160
  if scanned is None:
160
- scanned = {f: _scan_markers(f) for f in _walk_arxml(root)}
161
+ scanned = {f: _scan_markers(f) for f in _walk_arxml(root, notes)}
161
162
  return scanned
162
163
 
163
164
  if def_files:
164
- dfiles, origin["defs"] = _globs(root, def_files), "argument"
165
+ dfiles, origin["defs"] = _globs(root, def_files, warnings, "--defs"), "argument"
165
166
  elif cfg.get("paramDefs"):
166
- dfiles, origin["defs"] = _globs(root, cfg["paramDefs"]), PROJECT_CONFIG
167
+ dfiles, origin["defs"] = _globs(root, cfg["paramDefs"], warnings, "paramDefs"), PROJECT_CONFIG
167
168
  else:
168
169
  dfiles, origin["defs"] = [f for f, m in scan().items() if "defs" in m], "auto-discovered"
169
170
  if not dfiles and os.environ.get("AUTOSAR_PARAMDEFS"):
170
- dfiles = _globs(root, os.environ["AUTOSAR_PARAMDEFS"].split(os.pathsep))
171
+ dfiles = _globs(root, os.environ["AUTOSAR_PARAMDEFS"].split(os.pathsep), warnings, "AUTOSAR_PARAMDEFS")
171
172
  origin["defs"] = "AUTOSAR_PARAMDEFS"
172
173
  if config_files:
173
- cfiles, origin["config"] = _globs(root, config_files), "argument"
174
+ cfiles, origin["config"] = _globs(root, config_files, warnings, "--config"), "argument"
174
175
  elif cfg.get("config"):
175
- cfiles, origin["config"] = _globs(root, cfg["config"]), PROJECT_CONFIG
176
+ cfiles, origin["config"] = _globs(root, cfg["config"], warnings, "config"), PROJECT_CONFIG
176
177
  else:
177
178
  cfiles, origin["config"] = [f for f, m in scan().items() if "config" in m], "auto-discovered"
178
179
  if cfg.get("input"):
179
- ifiles, origin["input"] = _globs(root, cfg["input"]), PROJECT_CONFIG
180
+ ifiles, origin["input"] = _globs(root, cfg["input"], warnings, "input"), PROJECT_CONFIG
180
181
  else:
181
182
  known = set(cfiles) | set(dfiles)
182
183
  ifiles, origin["input"] = [f for f in scan() if f not in known], "auto-discovered"
@@ -185,7 +186,7 @@ def discover(root: str, config_files: Optional[list] = None, def_files: Optional
185
186
  "No ECUC param definitions found (no ECUC-MODULE-DEF in the project, no paramDefs in "
186
187
  f"{PROJECT_CONFIG}, AUTOSAR_PARAMDEFS unset). Values are readable but NOT validated, and edits "
187
188
  "cannot add new elements. Point paramDefs at the AUTOSAR / vendor BSWMD param-def ARXML.")
188
- return Sources(root, cfiles, dfiles, ifiles, warnings, origin)
189
+ return Sources(root, cfiles, dfiles, ifiles, warnings, origin, notes)
189
190
 
190
191
 
191
192
  def _dest(n: Optional[Node]) -> Optional[str]:
@@ -0,0 +1,221 @@
1
+ """Layout-agnostic file discovery and path filters.
2
+
3
+ Nothing here assumes Config/, Input/, Source/ or Output/:
4
+ - walk() finds files at any depth, skips only tooling dirs, includes other hidden dirs (and says so),
5
+ follows symlinked dirs once with cycle protection, and stops at a cap with a clear message.
6
+ - Pattern() matches a user path argument against files: an exact file, a directory (= path prefix,
7
+ with or without trailing slash), a glob with ** (relative to the root, or anywhere when it has no
8
+ slash), relative or absolute.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import fnmatch
13
+ import os
14
+ import re
15
+ from dataclasses import dataclass, field
16
+ from typing import Callable, Iterable, Optional
17
+
18
+ TOOLING_DIRS = {".git", ".hg", ".svn", ".hydron", ".venv", "venv", "node_modules", "__pycache__", ".cache",
19
+ ".idea", ".vscode", ".pytest_cache", ".mypy_cache", ".tox"}
20
+ MAX_FILES = int(os.environ.get("AUTOSAR_MAX_FILES", "200000"))
21
+ GLOB_CHARS = set("*?[")
22
+
23
+
24
+ def is_arxml(name: str) -> bool:
25
+ return name.lower().endswith(".arxml") # .arxml, .ARXML, .ecuc.arxml, ...
26
+
27
+
28
+ def is_c(name: str) -> bool:
29
+ return name.lower().endswith((".c", ".h"))
30
+
31
+
32
+ @dataclass
33
+ class Walk:
34
+ files: list = field(default_factory=list)
35
+ hidden: list = field(default_factory=list) # hidden dirs that contributed files
36
+ cycles: list = field(default_factory=list) # symlinked dirs skipped as already visited
37
+ truncated: bool = False
38
+ visited_dirs: int = 0
39
+
40
+ def notes(self, root: str) -> list:
41
+ out = []
42
+ for d in self.hidden:
43
+ out.append(f"included hidden directory {os.path.relpath(d, root)}/ (it contains matching files)")
44
+ for d in self.cycles[:5]:
45
+ out.append(f"symlinked directory {os.path.relpath(d, root)} skipped: already visited (cycle)")
46
+ if self.truncated:
47
+ out.append(f"file walk stopped at {MAX_FILES} files; narrow the scan with .hydron/autosar.json globs "
48
+ "or raise AUTOSAR_MAX_FILES")
49
+ return out
50
+
51
+
52
+ def walk(root: str, want: Callable[[str], bool], cap: int = 0) -> Walk:
53
+ """All files under ``root`` (any depth) for which ``want(name)`` is true, sorted per directory."""
54
+ cap = cap or MAX_FILES
55
+ res = Walk()
56
+ seen: set = set()
57
+ hidden_hits: dict = {}
58
+
59
+ def visit(d: str, hidden_anc: Optional[str]):
60
+ real = os.path.realpath(d)
61
+ if real in seen:
62
+ res.cycles.append(d)
63
+ return
64
+ seen.add(real)
65
+ res.visited_dirs += 1
66
+ try:
67
+ entries = sorted(os.scandir(d), key=lambda e: e.name)
68
+ except OSError:
69
+ return
70
+ subdirs = []
71
+ for e in entries:
72
+ if res.truncated:
73
+ return
74
+ try:
75
+ if e.is_dir(follow_symlinks=True):
76
+ if e.name not in TOOLING_DIRS:
77
+ subdirs.append(e)
78
+ elif e.is_file(follow_symlinks=True) and want(e.name):
79
+ res.files.append(e.path)
80
+ if hidden_anc:
81
+ hidden_hits.setdefault(hidden_anc, True)
82
+ if len(res.files) >= cap:
83
+ res.truncated = True
84
+ return
85
+ except OSError:
86
+ continue
87
+ for e in subdirs:
88
+ anc = hidden_anc or (e.path if e.name.startswith(".") else None)
89
+ visit(e.path, anc)
90
+
91
+ if os.path.isdir(root):
92
+ visit(root, None)
93
+ res.hidden = sorted(hidden_hits)
94
+ return res
95
+
96
+
97
+ def _glob_regex(pat: str) -> re.Pattern:
98
+ """Glob with ** (any number of dirs), * and ? (within a segment), [..] classes."""
99
+ out, i = [], 0
100
+ while i < len(pat):
101
+ if pat.startswith("**/", i):
102
+ out.append("(?:.*/)?")
103
+ i += 3
104
+ elif pat.startswith("**", i):
105
+ out.append(".*")
106
+ i += 2
107
+ elif pat[i] == "*":
108
+ out.append("[^/]*")
109
+ i += 1
110
+ elif pat[i] == "?":
111
+ out.append("[^/]")
112
+ i += 1
113
+ elif pat[i] == "[":
114
+ j = pat.find("]", i)
115
+ if j < 0:
116
+ out.append(re.escape(pat[i]))
117
+ i += 1
118
+ else:
119
+ out.append(fnmatch.translate(pat[i:j + 1])[4:-3])
120
+ i = j + 1
121
+ else:
122
+ out.append(re.escape(pat[i]))
123
+ i += 1
124
+ return re.compile("".join(out) + r"\Z", re.IGNORECASE) # *.ARXML == *.arxml
125
+
126
+
127
+ def _norm(p: str) -> str:
128
+ return p.replace("\\", "/")
129
+
130
+
131
+ class Pattern:
132
+ """One user-supplied path argument, resolved against ``root``."""
133
+
134
+ def __init__(self, root: str, raw: str):
135
+ self.raw = raw
136
+ p = os.path.expanduser(str(raw).strip())
137
+ stripped = p.rstrip("/\\") or p
138
+ self.is_glob = any(ch in GLOB_CHARS for ch in stripped)
139
+ full = stripped if os.path.isabs(stripped) else os.path.join(root, stripped)
140
+ self.abs = os.path.normpath(full)
141
+ self.real = os.path.realpath(self.abs)
142
+ self.root = os.path.realpath(root)
143
+ if self.is_glob:
144
+ self.kind = "glob"
145
+ self.rx = _glob_regex(_norm(stripped))
146
+ self.anywhere = "/" not in _norm(stripped).lstrip("./") # "*.arxml" or "Com*.arxml": match basenames
147
+ elif os.path.isdir(self.abs):
148
+ self.kind = "dir"
149
+ elif os.path.isfile(self.abs):
150
+ self.kind = "file"
151
+ else:
152
+ self.kind = "missing"
153
+
154
+ def matches(self, path: str) -> bool:
155
+ a = os.path.normpath(os.path.abspath(path))
156
+ r = os.path.realpath(a)
157
+ if self.kind == "file":
158
+ return a == self.abs or r == self.real
159
+ if self.kind == "dir":
160
+ return any(x.startswith(base.rstrip(os.sep) + os.sep) for x in (a, r) for base in (self.abs, self.real))
161
+ if self.kind == "glob":
162
+ rel = _norm(os.path.relpath(r, self.root))
163
+ cands = [rel, _norm(a), _norm(r), os.path.basename(a)] if self.anywhere else [rel, _norm(a), _norm(r)]
164
+ return any(self.rx.match(c) for c in cands) or (self.anywhere and any(self.rx.match(c.split("/")[-1]) for c in cands))
165
+ # missing on disk: maybe a path relative to root with odd separators, or a basename
166
+ name = _norm(str(self.raw)).strip("/")
167
+ rel = _norm(os.path.relpath(r, self.root))
168
+ return rel == name or rel.startswith(name + "/") or os.path.basename(a) == name
169
+
170
+ def expand(self, want: Callable[[str], bool]) -> list:
171
+ """Files on disk this pattern names (dirs walked recursively)."""
172
+ if self.kind == "file":
173
+ return [self.abs]
174
+ if self.kind == "dir":
175
+ return walk(self.abs, want).files
176
+ if self.kind == "glob":
177
+ # Walk (cycle-safe, tooling dirs skipped) from the glob's fixed prefix instead of glob.glob,
178
+ # which follows symlink loops and is case-sensitive.
179
+ stripped = _norm(os.path.expanduser(str(self.raw).strip())).rstrip("/")
180
+ fixed = []
181
+ for seg in stripped.split("/"):
182
+ if any(ch in GLOB_CHARS for ch in seg):
183
+ break
184
+ fixed.append(seg)
185
+ prefix = "/".join(fixed)
186
+ base = prefix if os.path.isabs(stripped) else os.path.join(self.root, prefix) if prefix else self.root
187
+ if self.anywhere:
188
+ base = self.root
189
+ if not os.path.isdir(base):
190
+ return []
191
+ return [f for f in walk(base, want).files if self.matches(f)]
192
+ return []
193
+
194
+
195
+ def patterns(root: str, raws: Optional[Iterable]) -> list:
196
+ return [Pattern(root, r) for r in (raws or []) if str(r).strip()]
197
+
198
+
199
+ def expand(root: str, raws: Optional[Iterable], want: Callable[[str], bool]) -> list:
200
+ """Files named by the patterns, each real file once (symlinked routes collapse)."""
201
+ seen, out = set(), []
202
+ for p in patterns(root, raws):
203
+ for f in p.expand(want):
204
+ real = os.path.realpath(f)
205
+ if real not in seen:
206
+ seen.add(real)
207
+ out.append(os.path.abspath(f))
208
+ return out
209
+
210
+
211
+ def select(root: str, raws: Optional[Iterable], files: Iterable[str]) -> tuple:
212
+ """(matched files, patterns that matched nothing) for a filter over already-loaded files."""
213
+ pats = patterns(root, raws)
214
+ files = list(files)
215
+ hit, empty = [], []
216
+ for p in pats:
217
+ m = [f for f in files if p.matches(f)]
218
+ if not m:
219
+ empty.append(p.raw)
220
+ hit.extend(m)
221
+ return sorted(set(hit)), empty
@@ -17,6 +17,8 @@ Rules:
17
17
  ECUC-REF-UNRESOLVED foreign reference target not in any loaded file (warning)
18
18
  ECUC-DUP-NAME two containers with the same AUTOSAR path
19
19
  ECUC-OBSOLETE value for an obsolete def (warning)
20
+ H2L-FILTER-NOMATCH a files/module filter matched nothing that was loaded (error, never a silent "clean")
21
+ H2L-DISCOVERY discovery notes: hidden dir included, symlink cycle skipped, walk cap (info)
20
22
  Hand-written semantic rules (ecuc_engine/rules, ids H2L-*) run afterwards, with or without defs.
21
23
  """
22
24
  from __future__ import annotations
@@ -24,6 +26,7 @@ from __future__ import annotations
24
26
  from typing import Optional
25
27
 
26
28
  from . import defs as D
29
+ from . import paths
27
30
  from .model import Project
28
31
  from .values import check_value, expected_value_tag
29
32
 
@@ -210,9 +213,36 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
210
213
  dg.items.extend(run_rules(project))
211
214
 
212
215
  items = dg.items
216
+ loaded = sorted(project.files)
217
+ by_file: dict = {}
218
+ for m in project.modules:
219
+ by_file.setdefault(project.rel(m.file), set()).add(m.name)
220
+ sample = ", ".join(project.rel(f) for f in loaded[:8]) + (f", … (+{len(loaded) - 8})" if len(loaded) > 8 else "")
221
+ scope: dict = {"root": project.root, "configFiles": len(loaded), "paramDefFiles": len(project.sources.defs),
222
+ "inputFiles": len(project.sources.input)}
223
+ nomatch = []
213
224
  if files:
214
- want = {project.rel(f) for f in files} | set(files)
225
+ matched, empty = paths.select(project.root, files, loaded)
226
+ for raw in empty:
227
+ nomatch.append(_nomatch(project, f"files filter {raw!r} matched none of the {len(loaded)} loaded config "
228
+ f"file(s) [{sample or 'none'}]; NOTHING was validated for it. Pass a file, "
229
+ f"directory or glob relative to {project.root}, or omit 'files'."))
230
+ want = {project.rel(f) for f in matched}
215
231
  items = [i for i in items if i["file"] in want]
232
+ scope["filter"] = {"files": list(files), "matched": sorted(want)[:200], "matchedCount": len(want)}
233
+ if module:
234
+ names = {m.name for m in project.modules} | {(m.def_ref or "").rsplit("/", 1)[-1] for m in project.modules}
235
+ if module not in names:
236
+ nomatch.append(_nomatch(project, f"module filter {module!r} matches no loaded module "
237
+ f"({', '.join(sorted(names - {''})) or 'none loaded'}); NOTHING was validated."))
238
+ scope["module"] = module
239
+ info = [{"file": "", "line": 0, "path": "", "rule": "H2L-DISCOVERY", "severity": "info", "message": n, "defRef": None}
240
+ for n in project.sources.notes]
241
+ items = nomatch + items + info
242
+ shown = sorted(by_file.items())
243
+ scope["files"] = [{"file": f, "modules": sorted(ms)} for f, ms in shown[:100]]
244
+ if len(shown) > 100:
245
+ scope["filesTruncated"] = len(shown) - 100
216
246
  items.sort(key=lambda i: (i["file"], i["line"], SEV_ORDER.get(i["severity"], 9), i["rule"]))
217
247
  summary: dict = {}
218
248
  for i in items:
@@ -223,5 +253,11 @@ def validate(project: Project, files: Optional[list] = None, module: Optional[st
223
253
  "defsLoaded": len(defs.modules),
224
254
  "modules": sorted({m.name for m in project.modules}),
225
255
  "summary": summary,
256
+ "scope": scope,
226
257
  "diagnostics": items,
227
258
  }
259
+
260
+
261
+ def _nomatch(project: Project, msg: str) -> dict:
262
+ return {"file": "", "line": 0, "path": project.root, "rule": "H2L-FILTER-NOMATCH", "severity": "error",
263
+ "message": msg, "defRef": None}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ecuc-engine"
3
- version = "0.1.2"
3
+ version = "1.0.1"
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.
@@ -79,6 +79,16 @@ Facts that decide common review calls:
79
79
  - `references/project-layout.md` — Config / Input / Output / Source / Build / SIP and `.hydron/autosar.json`.
80
80
  - `references/workflow.md` — the edit → validate → generate → build → trace loop, diagnostics rules, and what to do when a step fails.
81
81
 
82
+ ## Scoping and empty results
83
+ - Path arguments (`ecuc_validate files`, `autosar_audit paths`, `arxml_diff a/b`, `.hydron/autosar.json`
84
+ globs) take files, directories (recursive) or globs (`**/Com*.arxml`), relative to the project root or
85
+ absolute. Project layouts vary: never assume `Config/`, `Input/` or `Source/` exist — the tools find
86
+ ARXML and C anywhere and classify by content.
87
+ - A filter that matches nothing is an ERROR (`H2L-FILTER-NOMATCH`), never "clean". Prefer validating
88
+ unfiltered, then narrowing.
89
+ - **If validate reports nothing on a project you expect to have issues, run it unfiltered and check the
90
+ loaded-file list** printed at the end of the output (files and modules actually validated).
91
+
82
92
  ## Reading diagnostics
83
93
  `file:line SEVERITY RULE path — message [def DEFINITION-REF]`. Rules: ECUC-MISSING-PARAM,
84
94
  ECUC-MULT-CONTAINER/-PARAM, ECUC-CHOICE, ECUC-RANGE, ECUC-ENUM, ECUC-TYPE, ECUC-STRING,
@@ -9,7 +9,10 @@
9
9
  | `Build/` | makefiles/CMake, linker scripts, compiler options | build owner |
10
10
  | `SIP/` (or `BSW/`, `ThirdParty/`) | the vendor's Software Integration Package: static BSW code, BSWMD with vendor param defs (VSMD), generators | vendor; read-only |
11
11
 
12
- Names vary; the tools classify `.arxml` files by content, not by folder.
12
+ Names vary; the tools classify `.arxml` files (any extension case, e.g. `.ecuc.arxml`, `.ARXML`) by
13
+ content, not by folder, at any depth. Tooling dirs (`.git`, `.hydron`, `node_modules`, …) are skipped; other
14
+ hidden dirs are scanned and reported; symlinked dirs are followed once. For C, generator output is
15
+ recognised by its banner or `*_Cfg.*`/`*_PBcfg.c`/`Rte*.c` names, not by the `Output/` folder.
13
16
 
14
17
  ## `.hydron/autosar.json`
15
18
  Optional; pins what the tools load and how to generate/build: