@h2loop/autosar 1.10.0 → 1.11.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": "1.10.0",
3
+ "version": "1.11.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,10 +1,10 @@
1
1
  // @bun
2
- import{readFileSync as ze}from"fs";import{isAbsolute as Ke,join as te,resolve as Ze}from"path";import{tool as S}from"@kilocode/plugin";import{homedir as Oe}from"os";import{join as R}from"path";var Re=()=>process.env.HYDRON_TEST_HOME||Oe(),J=(n,t)=>process.env[n]||R(Re(),t),Ee=()=>R(J("XDG_CONFIG_HOME",".config"),"hydron-cli"),Ce=()=>R(J("XDG_DATA_HOME",".local/share"),"hydron-cli"),Te=()=>R(J("XDG_STATE_HOME",".local/state"),"hydron-cli");async function G(n){let t=Bun.file(n);if(!await t.exists())return;return t.json().catch(()=>{return})}async function W(){let n=process.env.KILO_API_URL?.trim();if(n)return n.replace(/\/+$/,"");let t=await G(R(Ee(),"server-config.json")),s=typeof t?.apiBaseUrl==="string"?t.apiBaseUrl:void 0;if(!s)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return s.replace(/\/+$/,"")}function Ne(n){let t=n?.split(".")[1];if(!t)return"";try{let s=JSON.parse(Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof s.sub==="string"?s.sub:""}catch{return""}}async function xe(n){let t=Ne(n.access);if(t)return t;if(!n.refresh)return"";return new Bun.CryptoHasher("sha256").update(n.refresh).digest("hex").slice(0,12)}async function ce(){let t=(await G(R(Ce(),"auth.json")))?.hydron;if(!t||t.type!=="oauth"||!t.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return t}async function de(){let n=await ce();if(typeof n.expires==="number"&&n.expires>0&&Date.now()>=n.expires-30000)throw Error("The H2Loop session token has expired. Send any message in Hydron (the CLI refreshes it) or re-login with `hydron auth login`, then retry.");return n.access}async function oe(n){let t=await fetch(`${await W()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await de()}`},signal:n?AbortSignal.any([n,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(t.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!t.ok)throw Error(`could not list H2Loop projects: HTTP ${t.status}`);let s=await t.json();return Array.isArray(s)?s:s?.items??[]}var ae=()=>R(Te(),"h2loop-project.json");async function P(n,t){let s=n===void 0||n===null?"":String(n).trim();if(/^\d+$/.test(s))return s;if(s){let i=await oe(t),r=i.filter((u)=>u.name===s),d=r.length?r:i.filter((u)=>u.name?.toLowerCase()===s.toLowerCase());if(d.length===1)return String(d[0].id);if(d.length>1)throw Error(`workspace "${s}" is ambiguous: ${d.map((u)=>`${u.name} (id ${u.id})`).join(", ")}; pass the numeric id`);let c=i.filter((u)=>u.name?.toLowerCase().includes(s.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${s}"`+(c.length?`; similar: ${c.map((u)=>`${u.name} (id ${u.id})`).join(", ")}`:`; you have ${i.length} project(s)`))}let a=await G(ae());if(!a)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let f=await ce();if(a.project&&typeof a.project==="object"){let i=a.project;if(typeof a.account==="string"&&a.account!==await xe(f))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(i.id!==void 0)return String(i.id)}let e=a;if(e.id===void 0)throw Error(`${ae()} holds no project id; select a project again or pass workspace_id.`);if(!(await oe(t)).some((i)=>String(i.id)===String(e.id)))throw Error(`The selected project ${e.name??""} (id ${e.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(e.id)}async function M(n,t,s,a,f=120000){let e=`${await W()}${n}?workspace_id=${encodeURIComponent(t)}`,o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await de()}`},body:JSON.stringify(s),signal:a?AbortSignal.any([a,AbortSignal.timeout(f)]):AbortSignal.timeout(f)}),i=await o.text();if(o.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(o.status===404&&i.includes("Not Found"))throw Error(`${n} is not available on ${await W()} (backend predates the AUTOSAR endpoints).`);if(!o.ok)throw Error(`H2Loop ${n} failed: HTTP ${o.status} ${i.slice(0,800)}`);try{return JSON.parse(i)}catch(r){throw Error(`H2Loop ${n} returned non-JSON (${String(r)}): ${i.slice(0,300)}`)}}var V=/\.arxml\b/i,X="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit for ECUC values, arxml_edit for System-extract/SWC elements (or autosar_add_signal for a new signal), then ecuc_validate. To start a NEW .arxml file, use arxml_edit with {op:'new_file', file, packages} followed by add ops.";function le(n,t){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!t)return;let s=typeof t.filePath==="string"?t.filePath:typeof t.file_path==="string"?t.file_path:"";if(["edit","write","multiedit"].includes(n)){let a=Array.isArray(t.edits)?t.edits.some((f)=>typeof f?.filePath==="string"&&V.test(f.filePath)):!1;if(V.test(s)||a)return`${X} (${n} on ${s||"an .arxml file"})`;return}if(n==="apply_patch"||n==="patch"){let a=String(t.patchText??t.patch??""),f=a.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??a.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return f?`${X} (${n} on ${f[1].trim()})`:void 0}if(n==="bash"){let a=String(t.command??"");if(!V.test(a))return;let f=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(a)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(a),e=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(a)||/\btee\b[^|;&]*\.arxml\b/i.test(a),o=/\b(?:mv|cp|ln|install|rsync)\b[^|;&]*\.arxml['"]?\s*(?:$|[|;&])/im.test(a)||/\b(?:os\.(?:rename|replace)|shutil\.(?:move|copy\w*)|fs\.(?:rename|copyFile)\w*|Path\([^)]*\)\.rename)\([^)]*\.arxml/i.test(a)||/\.(?:rename|replace)\(\s*['"][^'"]*\.arxml/i.test(a),i=/open\([^)]*\.arxml[^)]*,\s*(?:mode\s*=\s*)?['"][wa+]/i.test(a)||/\.arxml['"]?\s*\)?\s*\.write_(?:text|bytes)\b/i.test(a)||/write_(?:text|bytes)\([^)]*\)[^;\n]*\.arxml/i.test(a)||/\bwriteFile(?:Sync)?\([^)]*\.arxml/i.test(a)||/\.write\(\s*['"][^'"]*\.arxml/i.test(a)||/\b(?:awk|gawk)\b[^|;&]*-i\s+inplace/.test(a)||/\bed\b\s+-s\s+[^|;&]*\.arxml/.test(a);if(f||e||i||o)return`${X} (bash: ${a.slice(0,120)})`}return}var je=/(?:^|[;&|(]\s*|\bnode\s+\S*|\bnpx\s+)(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\b/m,De='Not a shell command: ecuc_*/arxml_*/autosar_* are TOOLS of this session. Call the tool directly (a tool call, not bash) with its arguments as JSON, e.g. arxml_edit with {"ops": [ ... ]}. If a tool call fails, read its error, fix the arguments and call the tool again.';function ue(n,t){if(n!=="bash"||!t)return;let s=String(t.command??""),a=/\s(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\s+(?:--|<|\{|\[|'|")/.test(s);if(je.test(s)||a)return`${De} (bash: ${s.slice(0,100)})`;if(/\becuc_engine\b|hydron-cli[\\/]store[\\/]/.test(s))return`Blocked: do not run the AUTOSAR pack's engine or files from the plugin store by hand. Use its tools (ecuc_read/ecuc_edit/arxml_edit/ecuc_validate ...) as tool calls. (bash: ${s.slice(0,100)})`;if(/(?:^|[;&|]\s*)(?:\S*\/)?(?:hydron|kilo|opencode)\s+(?:run|serve)\b/m.test(s))return"Blocked: do not start another agent session from inside this one; do the work here with your tools.";return}var Ie=new Set(["autosar_build_context","ecuc_read","ecuc_find","ecuc_crosscheck","autosar_config_lookup","autosar_trace","autosar_audit","query_hydron_project","get_symbol_info"]),q=new Set(["ecuc_edit","arxml_edit","autosar_add_signal","autosar_add_like"]),he=/\.(c|h)$/i,ge=/(^|[\\/])(Output|gen|Gen|GenData|generated)[\\/]/;var fe=/(^|[\\/])[A-Z][A-Za-z0-9]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]$/,Le=/\b[A-Z][A-Za-z0-9]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]\b/,Y=new Map;function F(n){let t=n??"-",s=Y.get(t);if(!s)s={grounded:[],contextFor:[],changed:[],validation:"never",cFiles:[],rte:"n/a",docs:"n/a",blocks:0,modules:[],moduleBlocks:{},codegen:!1,cfgBlocks:0,firstBlocks:0},Y.set(t,s);return s}function K(n){if(!n)return"";return String(n.filePath??n.file_path??"")}function O(n){if(!n||!n.startsWith("/"))return;let t=n.split("/").filter(Boolean);return t.length>=2?t[1]:void 0}function Ue(n){return z(n?.ops)}function Pe(n,t){if(n!=="ecuc_edit")return[];let s=[];for(let a of Ue(t)){let f=O(String(a.path??a.parent_path??""));if(f&&!s.includes(f))s.push(f)}return s}function Me(n,t){if(!t)return;if(n==="edit"||n==="write"||n==="multiedit"){let s=K(t);return fe.test(s)?s:void 0}if(n==="apply_patch"||n==="patch"){let s=String(t.patchText??t.patch??"").match(/^\*\*\* (?:Add|Update) File: (.+)$/m);return s&&fe.test(s[1].trim())?s[1].trim():void 0}if(n==="bash"){let s=String(t.command??""),a=s.match(Le);if(!a)return;return/>\s*['"]?[^\s'"|;&]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]\b/.test(s)||/\btee\b/.test(s)||/open\([^)]*,\s*['"][wa]/.test(s)||/write_text|writeFile|\.write\(/.test(s)||/\bsed\b[^|;&]*\s-i/.test(s)?a[0]:void 0}return}function pe(n,t,s){if(process.env.AUTOSAR_LOOP_GATES==="0")return;let a=F(n);if(a.blocks>=6)return;let f=Me(t,s);if(f&&!a.codegen&&a.cfgBlocks<2)return a.blocks++,a.cfgBlocks++,`GENERATE, DO NOT HAND-WRITE: ${f} is generated configuration code. It is derived from the ECUC ARXML, so (1) make the ARXML right (ecuc_read / ecuc_edit / ecuc_validate until clean), then (2) call autosar_codegen with modules=[<the module>] and out_dir='Output' (or the folder the user named): it writes <Mip>_Cfg.h, <Mip>_Cfg.c/_Lcfg.c/_PBcfg.c deterministically from the configuration. Only if the generator cannot produce what is needed, edit its output afterwards and say why.`;if(q.has(t)&&!s?.dry_run){let o=Pe(t,s).filter((i)=>!a.modules.includes(i)&&(a.moduleBlocks[i]??0)<1);if(o.length){for(let i of o)a.moduleBlocks[i]=(a.moduleBlocks[i]??0)+1;return a.blocks++,`GROUND FIRST (${o.join(", ")}): this change was decided without the context engine for ${o.join(", ")}. Facts needed: the element's current values, its param-def schema (allowed params, ranges, literals, multiplicity), the consistency rules that will be checked, and the constraints from the ingested specifications.`}}if(q.has(t)&&!s?.dry_run&&a.grounded.length===0&&a.firstBlocks<2)return a.blocks++,a.firstBlocks++,"GROUND FIRST: no facts have been gathered in this session, so this change would be decided from memory. Before editing, get every fact the change depends on from the tools and list them with their source: autosar_build_context (target = the module/container/element, or swc+runnable) gives the project facts AND the facts from the ingested documents in one call; ecuc_read (schema=true) for what may be set; ecuc_find for what exists and references what. Then plan from those facts only and retry this edit.";let e=K(s);if((t==="edit"||t==="write"||t==="multiedit")&&he.test(e)&&!ge.test(e)){if(!a.grounded.includes("autosar_build_context")&&!a.grounded.includes("autosar_audit"))return a.blocks++,`GROUND FIRST: call autosar_build_context for the SW-C/runnable this C file implements (it lists the allowed RTE APIs with exact signatures, the related configuration and the relevant document facts) before changing ${e}. For C that is not runnable code, autosar_audit grounds BSW usage instead. Then retry.`}return}function Z(n,t,s,a){let f=F(n),e=/^\s*(ERROR|Blocked|GROUND FIRST)/.test(a)||a.includes("edit rejected");if(Ie.has(t)&&!e){if(!f.grounded.includes(t))f.grounded.push(t);if(t==="autosar_build_context"){let i=String(s?.target??s?.runnable??s?.swc??"");if(i&&!f.contextFor.includes(i))f.contextFor.push(i);let r=new Set,d=O(i);if(d)r.add(d);for(let c of a.matchAll(/"@path":\s*"(\/[^"]+)"/g)){let u=O(c[1]);if(u)r.add(u);if(r.size>8)break}for(let c of r)if(!f.modules.includes(c))f.modules.push(c)}}if(q.has(t)&&!e&&!s?.dry_run&&!/^Dry run/.test(a)){f.validation=/AUTO-VALIDATE: clean/.test(a)?"clean":/AUTO-VALIDATE/.test(a)?"errors":"stale",f.docs="pending";for(let i of Fe(t,s,a))if(!f.changed.includes(i))f.changed.push(i)}let o=K(s);if((t==="edit"||t==="write"||t==="multiedit")&&he.test(o)&&!ge.test(o)&&!e){if(!f.cFiles.includes(o))f.cFiles.push(o);f.rte="pending"}if(t==="ecuc_validate"&&!e){let i=a.split(`
2
+ import{readFileSync as ze}from"fs";import{isAbsolute as Ke,join as te,resolve as Ze}from"path";import{tool as S}from"@kilocode/plugin";import{homedir as Oe}from"os";import{join as E}from"path";var Ee=()=>process.env.HYDRON_TEST_HOME||Oe(),J=(n,t)=>process.env[n]||E(Ee(),t),Re=()=>E(J("XDG_CONFIG_HOME",".config"),"hydron-cli"),Ce=()=>E(J("XDG_DATA_HOME",".local/share"),"hydron-cli"),Te=()=>E(J("XDG_STATE_HOME",".local/state"),"hydron-cli");async function G(n){let t=Bun.file(n);if(!await t.exists())return;return t.json().catch(()=>{return})}async function W(){let n=process.env.KILO_API_URL?.trim();if(n)return n.replace(/\/+$/,"");let t=await G(E(Re(),"server-config.json")),s=typeof t?.apiBaseUrl==="string"?t.apiBaseUrl:void 0;if(!s)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return s.replace(/\/+$/,"")}function Ne(n){let t=n?.split(".")[1];if(!t)return"";try{let s=JSON.parse(Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof s.sub==="string"?s.sub:""}catch{return""}}async function xe(n){let t=Ne(n.access);if(t)return t;if(!n.refresh)return"";return new Bun.CryptoHasher("sha256").update(n.refresh).digest("hex").slice(0,12)}async function ce(){let t=(await G(E(Ce(),"auth.json")))?.hydron;if(!t||t.type!=="oauth"||!t.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return t}async function de(){let n=await ce();if(typeof n.expires==="number"&&n.expires>0&&Date.now()>=n.expires-30000)throw Error("The H2Loop session token has expired. Send any message in Hydron (the CLI refreshes it) or re-login with `hydron auth login`, then retry.");return n.access}async function oe(n){let t=await fetch(`${await W()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await de()}`},signal:n?AbortSignal.any([n,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(t.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!t.ok)throw Error(`could not list H2Loop projects: HTTP ${t.status}`);let s=await t.json();return Array.isArray(s)?s:s?.items??[]}var ae=()=>E(Te(),"h2loop-project.json");async function P(n,t){let s=n===void 0||n===null?"":String(n).trim();if(/^\d+$/.test(s))return s;if(s){let i=await oe(t),r=i.filter((u)=>u.name===s),d=r.length?r:i.filter((u)=>u.name?.toLowerCase()===s.toLowerCase());if(d.length===1)return String(d[0].id);if(d.length>1)throw Error(`workspace "${s}" is ambiguous: ${d.map((u)=>`${u.name} (id ${u.id})`).join(", ")}; pass the numeric id`);let c=i.filter((u)=>u.name?.toLowerCase().includes(s.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${s}"`+(c.length?`; similar: ${c.map((u)=>`${u.name} (id ${u.id})`).join(", ")}`:`; you have ${i.length} project(s)`))}let a=await G(ae());if(!a)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let f=await ce();if(a.project&&typeof a.project==="object"){let i=a.project;if(typeof a.account==="string"&&a.account!==await xe(f))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if(i.id!==void 0)return String(i.id)}let e=a;if(e.id===void 0)throw Error(`${ae()} holds no project id; select a project again or pass workspace_id.`);if(!(await oe(t)).some((i)=>String(i.id)===String(e.id)))throw Error(`The selected project ${e.name??""} (id ${e.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(e.id)}async function M(n,t,s,a,f=120000){let e=`${await W()}${n}?workspace_id=${encodeURIComponent(t)}`,o=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await de()}`},body:JSON.stringify(s),signal:a?AbortSignal.any([a,AbortSignal.timeout(f)]):AbortSignal.timeout(f)}),i=await o.text();if(o.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(o.status===404&&i.includes("Not Found"))throw Error(`${n} is not available on ${await W()} (backend predates the AUTOSAR endpoints).`);if(!o.ok)throw Error(`H2Loop ${n} failed: HTTP ${o.status} ${i.slice(0,800)}`);try{return JSON.parse(i)}catch(r){throw Error(`H2Loop ${n} returned non-JSON (${String(r)}): ${i.slice(0,300)}`)}}var V=/\.arxml\b/i,X="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit for ECUC values, arxml_edit for System-extract/SWC elements (or autosar_add_signal for a new signal), then ecuc_validate. To start a NEW .arxml file, use arxml_edit with {op:'new_file', file, packages} followed by add ops.";function le(n,t){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!t)return;let s=typeof t.filePath==="string"?t.filePath:typeof t.file_path==="string"?t.file_path:"";if(["edit","write","multiedit"].includes(n)){let a=Array.isArray(t.edits)?t.edits.some((f)=>typeof f?.filePath==="string"&&V.test(f.filePath)):!1;if(V.test(s)||a)return`${X} (${n} on ${s||"an .arxml file"})`;return}if(n==="apply_patch"||n==="patch"){let a=String(t.patchText??t.patch??""),f=a.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??a.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return f?`${X} (${n} on ${f[1].trim()})`:void 0}if(n==="bash"){let a=String(t.command??"");if(!V.test(a))return;let f=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(a)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(a),e=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(a)||/\btee\b[^|;&]*\.arxml\b/i.test(a),o=/\b(?:mv|cp|ln|install|rsync)\b[^|;&]*\.arxml['"]?\s*(?:$|[|;&])/im.test(a)||/\b(?:os\.(?:rename|replace)|shutil\.(?:move|copy\w*)|fs\.(?:rename|copyFile)\w*|Path\([^)]*\)\.rename)\([^)]*\.arxml/i.test(a)||/\.(?:rename|replace)\(\s*['"][^'"]*\.arxml/i.test(a),i=/open\([^)]*\.arxml[^)]*,\s*(?:mode\s*=\s*)?['"][wa+]/i.test(a)||/\.arxml['"]?\s*\)?\s*\.write_(?:text|bytes)\b/i.test(a)||/write_(?:text|bytes)\([^)]*\)[^;\n]*\.arxml/i.test(a)||/\bwriteFile(?:Sync)?\([^)]*\.arxml/i.test(a)||/\.write\(\s*['"][^'"]*\.arxml/i.test(a)||/\b(?:awk|gawk)\b[^|;&]*-i\s+inplace/.test(a)||/\bed\b\s+-s\s+[^|;&]*\.arxml/.test(a);if(f||e||i||o)return`${X} (bash: ${a.slice(0,120)})`}return}var je=/(?:^|[;&|(]\s*|\bnode\s+\S*|\bnpx\s+)(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\b/m,De='Not a shell command: ecuc_*/arxml_*/autosar_* are TOOLS of this session. Call the tool directly (a tool call, not bash) with its arguments as JSON, e.g. arxml_edit with {"ops": [ ... ]}. If a tool call fails, read its error, fix the arguments and call the tool again.';function ue(n,t){if(n!=="bash"||!t)return;let s=String(t.command??""),a=/\s(?:ecuc_(?:read|edit|validate|find|crosscheck)|arxml_(?:edit|diff)|autosar_[a-z_]+)\s+(?:--|<|\{|\[|'|")/.test(s);if(je.test(s)||a)return`${De} (bash: ${s.slice(0,100)})`;if(/\becuc_engine\b|hydron-cli[\\/]store[\\/]/.test(s))return`Blocked: do not run the AUTOSAR pack's engine or files from the plugin store by hand. Use its tools (ecuc_read/ecuc_edit/arxml_edit/ecuc_validate ...) as tool calls. (bash: ${s.slice(0,100)})`;if(/(?:^|[;&|]\s*)(?:\S*\/)?(?:hydron|kilo|opencode)\s+(?:run|serve)\b/m.test(s))return"Blocked: do not start another agent session from inside this one; do the work here with your tools.";return}var Ie=new Set(["autosar_build_context","ecuc_read","ecuc_find","ecuc_crosscheck","autosar_config_lookup","autosar_trace","autosar_audit","query_hydron_project","get_symbol_info"]),q=new Set(["ecuc_edit","arxml_edit","autosar_add_signal","autosar_add_like"]),he=/\.(c|h)$/i,ge=/(^|[\\/])(Output|gen|Gen|GenData|generated)[\\/]/;var fe=/(^|[\\/])[A-Z][A-Za-z0-9]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]$/,Le=/\b[A-Z][A-Za-z0-9]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]\b/,Y=new Map;function F(n){let t=n??"-",s=Y.get(t);if(!s)s={grounded:[],contextFor:[],changed:[],validation:"never",cFiles:[],rte:"n/a",docs:"n/a",blocks:0,modules:[],moduleBlocks:{},codegen:!1,cfgBlocks:0,firstBlocks:0},Y.set(t,s);return s}function K(n){if(!n)return"";return String(n.filePath??n.file_path??"")}function O(n){if(!n)return;let t=n.split("/").filter(Boolean),s=t[0]==="AUTOSAR"&&t[1]==="EcucDefs"?t[2]:t.length>=2?t[1]:void 0;return s&&/^[A-Z][A-Za-z0-9]{1,30}$/.test(s)&&s!=="EcucDefs"?s:void 0}function Ue(n){return z(n?.ops)}function Pe(n,t){if(n!=="ecuc_edit")return[];let s=[];for(let a of Ue(t)){let f=O(String(a.path??a.parent_path??""));if(f&&!s.includes(f))s.push(f)}return s}function Me(n,t){if(!t)return;if(n==="edit"||n==="write"||n==="multiedit"){let s=K(t);return fe.test(s)?s:void 0}if(n==="apply_patch"||n==="patch"){let s=String(t.patchText??t.patch??"").match(/^\*\*\* (?:Add|Update) File: (.+)$/m);return s&&fe.test(s[1].trim())?s[1].trim():void 0}if(n==="bash"){let s=String(t.command??""),a=s.match(Le);if(!a)return;return/>\s*['"]?[^\s'"|;&]*_(?:Cfg|PBcfg|PBCfg|Lcfg|LCfg)\.[ch]\b/.test(s)||/\btee\b/.test(s)||/open\([^)]*,\s*['"][wa]/.test(s)||/write_text|writeFile|\.write\(/.test(s)||/\bsed\b[^|;&]*\s-i/.test(s)?a[0]:void 0}return}function pe(n,t,s){if(process.env.AUTOSAR_LOOP_GATES==="0")return;let a=F(n);if(a.blocks>=6)return;let f=Me(t,s);if(f&&!a.codegen&&a.cfgBlocks<2)return a.blocks++,a.cfgBlocks++,`GENERATE, DO NOT HAND-WRITE: ${f} is generated configuration code. It is derived from the ECUC ARXML, so (1) make the ARXML right (ecuc_read / ecuc_edit / ecuc_validate until clean), then (2) call autosar_codegen with modules=[<the module>] and out_dir='Output' (or the folder the user named): it writes <Mip>_Cfg.h, <Mip>_Cfg.c/_Lcfg.c/_PBcfg.c deterministically from the configuration. Only if the generator cannot produce what is needed, edit its output afterwards and say why.`;if(q.has(t)&&!s?.dry_run){let o=Pe(t,s).filter((i)=>!a.modules.includes(i)&&(a.moduleBlocks[i]??0)<1);if(o.length){for(let i of o)a.moduleBlocks[i]=(a.moduleBlocks[i]??0)+1;return a.blocks++,`GROUND FIRST (${o.join(", ")}): this change was decided without the context engine for ${o.join(", ")}. Facts needed: the element's current values, its param-def schema (allowed params, ranges, literals, multiplicity), the consistency rules that will be checked, and the constraints from the ingested specifications.`}}if(q.has(t)&&!s?.dry_run&&a.grounded.length===0&&a.firstBlocks<2)return a.blocks++,a.firstBlocks++,"GROUND FIRST: no facts have been gathered in this session, so this change would be decided from memory. Before editing, get every fact the change depends on from the tools and list them with their source: autosar_build_context (target = the module/container/element, or swc+runnable) gives the project facts AND the facts from the ingested documents in one call; ecuc_read (schema=true) for what may be set; ecuc_find for what exists and references what. Then plan from those facts only and retry this edit.";let e=K(s);if((t==="edit"||t==="write"||t==="multiedit")&&he.test(e)&&!ge.test(e)){if(!a.grounded.includes("autosar_build_context")&&!a.grounded.includes("autosar_audit"))return a.blocks++,`GROUND FIRST: call autosar_build_context for the SW-C/runnable this C file implements (it lists the allowed RTE APIs with exact signatures, the related configuration and the relevant document facts) before changing ${e}. For C that is not runnable code, autosar_audit grounds BSW usage instead. Then retry.`}return}function Z(n,t,s,a){let f=F(n),e=/^\s*(ERROR|Blocked|GROUND FIRST)/.test(a)||a.includes("edit rejected");if(Ie.has(t)&&!e){if(!f.grounded.includes(t))f.grounded.push(t);if(t==="autosar_build_context"){let i=String(s?.target??s?.runnable??s?.swc??"");if(i&&!f.contextFor.includes(i))f.contextFor.push(i);let r=new Set,d=O(i);if(d)r.add(d);for(let c of a.matchAll(/"@path":\s*"(\/[^"]+)"/g)){let u=O(c[1]);if(u)r.add(u);if(r.size>8)break}for(let c of r)if(!f.modules.includes(c))f.modules.push(c)}}if(q.has(t)&&!e&&!s?.dry_run&&!/^Dry run/.test(a)){f.validation=/AUTO-VALIDATE: clean/.test(a)?"clean":/AUTO-VALIDATE/.test(a)?"errors":"stale",f.docs="pending";for(let i of Fe(t,s,a))if(!f.changed.includes(i))f.changed.push(i)}let o=K(s);if((t==="edit"||t==="write"||t==="multiedit")&&he.test(o)&&!ge.test(o)&&!e){if(!f.cFiles.includes(o))f.cFiles.push(o);f.rte="pending"}if(t==="ecuc_validate"&&!e){let i=a.split(`
3
3
  `)[0]??"",r=/^NEW since/.test(i)?/"error":\s*[1-9]/.test(i):/"error":\s*[1-9]/.test(i)||/^\S+:\d+ error /m.test(a);if(f.validation=r?"errors":"clean",s?.docs&&f.docs==="pending"&&!/DOCUMENT CHECK UNAVAILABLE/.test(a))f.docs="done"}if(t==="autosar_codegen"&&!/^\s*(Blocked|GROUND FIRST)/.test(a))f.codegen=!0;if(t==="autosar_verify_rte"&&!e)f.rte=/\u2014 FAILING/.test(a)?"fail":"pass"}function Fe(n,t,s){let a=[];if(n==="ecuc_edit"){let f=z(t?.ops);for(let e of f){let o=String(e.path??""),i=e.param?`${o}/${e.param}`:e.container?`${o}/${e.container}`:o;a.push(e.value!==void 0?`${i} = ${JSON.stringify(e.value).slice(0,80)}`:`${i} (${e.op})`)}}else if(n==="arxml_edit"){let f=s.match(/created (\[[^\]]*\])/);if(f)try{a.push(...JSON.parse(f[1]))}catch{}for(let e of z(t?.ops))if(e.op==="set")a.push(`${e.path}/${e.element} = ${JSON.stringify(e.value)}`)}else if(n==="autosar_add_signal")a.push(`new Com signal ${t?.name} in ${t?.ipdu}`);else if(n==="autosar_add_like"){let f=s.match(/Created:\n((?: .*\n?)+)/);if(f)a.push(...f[1].split(`
4
4
  `).map((e)=>e.trim()).filter(Boolean));else a.push(`${t?.name} like ${t?.like}`)}return a.filter(Boolean)}function z(n){let t=n;for(let s=0;s<3&&typeof t==="string";s++)try{t=JSON.parse(t)}catch{return[]}if(Array.isArray(t))return t.filter((s)=>s&&typeof s==="object");if(t&&typeof t==="object")return[t];return[]}function me(n){let t=Y.get(n??"-");if(!t)return;let s=[];if(t.validation==="stale")s.push("Config/ARXML changed since the last ecuc_validate: run it (unfiltered) now.");if(t.validation==="errors")s.push("The last ecuc_validate reported errors: fix them and validate again.");if(t.rte==="pending")s.push(`C changed (${t.cFiles.slice(-5).join(", ")}): run autosar_verify_rte on it.`);if(t.rte==="fail")s.push("autosar_verify_rte is FAILING: fix the calls (or apply its arxml_edit ops) and verify again.");if(t.docs==="pending"&&t.validation==="clean")s.push(`Changed elements not yet checked against the ingested documents (${t.changed.slice(0,8).join("; ")}${t.changed.length>8?"; \u2026":""}): run ecuc_validate docs=true and answer its checklist item by item.`);if(!s.length)return;return["# AUTOSAR: open verification items (do not report the task as done until each is resolved)",...s.map((a)=>`- ${a}`)].join(`
5
5
  `)}var Be=/^(?:hi+|hello+|hey+|hiya|yo|hola|namaste|good\s+(?:morning|afternoon|evening|day)|thanks?|thank\s+you|thx|ty|ok(?:ay)?|cool|nice|great|bye|good\s*bye|see\s+you)(?:\s+(?:there|all|team|hydron|again|so\s+much|a\s+lot))*$/i,ye=new Map;function He(n){let t=n.trim().replace(/[\p{Extended_Pictographic}\u200D\uFE0F]/gu,"").replace(/[!.?,:;~\-]+/g," ").replace(/\s+/g," ").trim();return t.length>0&&t.length<=40&&Be.test(t)}function be(n,t){ye.set(n??"-",He(t))}function we(n,t){if(process.env.AUTOSAR_LOOP_GATES==="0")return;if(!ye.get(n??"-"))return;return`NO TOOLS: the user only greeted you (no task). Do not call ${t} or any other tool, and do not read or explore the project. Reply in one short line (e.g. what you can help with here) and stop.`}import{existsSync as We}from"fs";import{dirname as Je,join as Q}from"path";import{fileURLToPath as Ge}from"url";var Ve="autosar-data==0.17.0";function Xe(){let n=Je(Ge(import.meta.url)),t=[Q(n,"..","python"),Q(n,"..","..","python")].find((s)=>We(Q(s,"ecuc_engine","__main__.py")));if(!t)throw Error(`ECUC engine not found next to ${n} (expected ../python/ecuc_engine)`);return t}async function qe(n){try{return await Bun.spawn([...n,"-c","import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}var H={};async function Ye(){if(H.cmd!==void 0)return H.cmd;let n=process.env.AUTOSAR_PYTHON?.trim(),s=n?[[n]]:[["uv","run","--no-project","--quiet","--with",Ve,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let a of s)if(await qe(a))return H.cmd=a,a;return H.cmd=null,null}async function w(n,t,s){let a=await Ye();if(!a)return{ok:!1,error:"no Python >= 3.10 found. Install uv (https://docs.astral.sh/uv/) or Python 3.10+, or set AUTOSAR_PYTHON to an interpreter path."};let f=Xe(),e=Bun.spawn([...a,"-m","ecuc_engine","--root",n,...t,"--json"],{cwd:n,env:{...process.env,PYTHONPATH:[f,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:s}),[o,i]=await Promise.all([new Response(e.stdout).text(),new Response(e.stderr).text()]);await e.exited;let r=o.trim().split(`
6
6
  `).filter(Boolean).at(-1);if(!r)return{ok:!1,error:`engine produced no output (exit ${e.exitCode}): ${i.trim().slice(-2000)}`};try{return JSON.parse(r)}catch(d){return{ok:!1,error:`engine output was not JSON (${String(d)}): ${r.slice(0,500)} ${i.slice(-500)}`}}}var l=S.schema,Qe=["# AUTOSAR pack: only for AUTOSAR work","If the request is about this AUTOSAR project or its work (ARXML/ECUC config, BSW/RTE/MCAL code, AUTOSAR","specs): load the `autosar` skill and use its tools (ecuc_read/find, ecuc_edit/arxml_edit, ecuc_validate,","autosar_build_context). Otherwise (a greeting, an unrelated question) just answer: no tools, no exploring."].join(`
7
- `);function N(n){if(typeof n!=="string")return n;let t=n.trim();if(!t.startsWith("[")&&!t.startsWith("{")&&!t.startsWith('"'))return n;try{return N(JSON.parse(t))}catch{return n}}var E=(n)=>l.preprocess(N,n);function T(n){let t=N(n);if(t===void 0||t===null||t==="")return;if(typeof t==="string")return[t];if(Array.isArray(t))return t.map(String).filter((s)=>s.trim());return[String(t)]}var D=()=>l.preprocess(T,l.array(l.string()).optional()),C=" In answers cite, for every element, its AUTOSAR path (the value's @path, e.g. /Pkg/Com/ComConfig/MySig), its"+" DEFINITION-REF (@def, e.g. /AUTOSAR/EcucDefs/Com/ComConfig/ComSignal) and file:line \u2014 never give the"+` DEFINITION-REF when the path is asked. A value's file:line is its "@at:<Param>" (the VALUE element; "@at" is`+' the container); integers come with "@hex:<Param>" \u2014 copy both, never compute a line or a base conversion'+" yourself. Cite a requirement ID only if it appears verbatim in a tool result or spec text read this session.";function I(n,t=200){let s=n.slice(0,t).map((a)=>`${a.file}:${a.line} ${a.severity.toUpperCase()} ${a.rule} ${a.path} \u2014 ${a.message}${a.defRef?` [def ${a.defRef}]`:""}`);if(n.length>t)s.push(`\u2026 ${n.length-t} more`);return s}function A(n){let t=n.didYouMean?`
7
+ `);function N(n){if(typeof n!=="string")return n;let t=n.trim();if(!t.startsWith("[")&&!t.startsWith("{")&&!t.startsWith('"'))return n;try{return N(JSON.parse(t))}catch{return n}}var R=(n)=>l.preprocess(N,n);function T(n){let t=N(n);if(t===void 0||t===null||t==="")return;if(typeof t==="string")return[t];if(Array.isArray(t))return t.map(String).filter((s)=>s.trim());return[String(t)]}var D=()=>l.preprocess(T,l.array(l.string()).optional()),C=" In answers cite, for every element, its AUTOSAR path (the value's @path, e.g. /Pkg/Com/ComConfig/MySig), its"+" DEFINITION-REF (@def, e.g. /AUTOSAR/EcucDefs/Com/ComConfig/ComSignal) and file:line \u2014 never give the"+` DEFINITION-REF when the path is asked. A value's file:line is its "@at:<Param>" (the VALUE element; "@at" is`+' the container); integers come with "@hex:<Param>" \u2014 copy both, never compute a line or a base conversion'+" yourself. Cite a requirement ID only if it appears verbatim in a tool result or spec text read this session.";function I(n,t=200){let s=n.slice(0,t).map((a)=>`${a.file}:${a.line} ${a.severity.toUpperCase()} ${a.rule} ${a.path} \u2014 ${a.message}${a.defRef?` [def ${a.defRef}]`:""}`);if(n.length>t)s.push(`\u2026 ${n.length-t} more`);return s}function A(n){let t=n.didYouMean?`
8
8
  Did you mean: ${n.didYouMean.join(", ")}`:"",s=n.schema?`
9
9
  Schema errors: ${JSON.stringify(n.schema)}`:"";return`ERROR: ${n.error}${t}${s}`}function v(n){let t=n.warnings??[];return t.length?t.map((s)=>`WARNING: ${s}`).join(`
10
10
  `)+`
@@ -31,7 +31,7 @@ List the facts your edit relies on (F#/D#), adjust the edit to them, then retry
31
31
  NOTE: the documents index did not answer. For specification facts ask again with ONE short, specific question, or use autosar_build_context target=<module/element> (project facts + schema + rules + documents).`:"")+(r?`
32
32
 
33
33
  LOCAL PROJECT FACTS (from this workspace's ECUC ARXML; the documents index does not contain this project's configuration, so for its current values use these and ecuc_read, not the answer above):
34
- `+r:"")}catch{}Z(e.sessionID,e.tool,e.args,String(o?.output??""))},"experimental.chat.system.transform":async(e,o)=>{o.system.push(Qe);let i=me(e?.sessionID);if(i)o.system.push(i)},tool:a=ct(ot({ecuc_read:S({description:`THE way to answer any question about configured ECUC values (use it instead of grep/read on .arxml). Read AUTOSAR ECUC configuration as compact ECUC JSON (keyed by def names: "<DefName>:<ShortName>" for containers, params by def short-name, references as "->/path"; every container carries @path = its AUTOSAR path, @def = its DEFINITION-REF and @at = its file:line; every value is followed by "@at:<Param>" = file:line of its VALUE element, and integers by "@hex:<Param>" ("@dec:<Param>" when the ARXML text is hex)). No path: lists modules and the param-def/config files found. path may be a full AUTOSAR path, a SHORT-NAME ('DoorLatch') or a partial path ('ComConfig/DoorLatch', 'DoorLatch/ComBitPosition'); ambiguous names return candidate paths. A module/container: its values to `+"`depth` levels (merged across split files). A param path (\u2026/Container/Param) or a def path "+"(/AUTOSAR/EcucDefs/\u2026): the value and/or the definition. schema=true adds the def summary (allowed "+"params with type/range/literals/multiplicity, allowed sub-containers) \u2014 read it before ecuc_edit. "+"Never read or edit ARXML by hand."+C,args:{path:l.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:l.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:l.boolean().optional().describe("include the param-def summary for the element"),project_dir:l.string().optional().describe("project root (default: session directory)")},async execute(e,o){let i=["read"];if(e.path)i.push(e.path);if(i.push("--depth",String(e.depth??2)),e.schema)i.push("--schema");let r=await w(_(o.directory,e.project_dir),i,o.abort);if(!r.ok)return v(r)+A(r);let{ok:d,engine:c,ms:u,warnings:p,...h}=r;return v(r)+JSON.stringify(h,null,1)}}),ecuc_edit:S({description:"Edit ECUC ARXML in place \u2014 the ONLY way to change configuration (edit/write/sed on .arxml are blocked). "+"Get exact paths and allowed params from ecuc_read (schema=true) first. Ops by AUTOSAR path: {op:'set', path, param, value} (value scalar, '->/ref/path', or a list for multi-valued "+"params), {op:'add', path:<parent>, container:'DefName:ShortName', values:{\u2026ECUC JSON\u2026}}, {op:'add', "+"path, param, value} (append a value), {op:'remove', path[, param[, value]]}. The engine picks the value element type, DEFINITION-REF/DEST and VALUE-REF DEST from the param defs, rejects out-of-range/non-literal values, patches only the changed bytes in the file each element lives in (UUIDs, order, comments preserved), applies all ops or none, and checks ARXML schema validity. Returns the unified diff and the validation of touched files. ALWAYS run ecuc_validate after editing and fix what it reports."+C,args:{ops:E(l.array(E(l.object({op:l.enum(["set","add","remove"]),path:l.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:l.string().optional().describe("parameter/reference def short-name"),value:l.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:l.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:E(l.record(l.string(),l.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:l.boolean().optional().describe("compute and show the diff without writing"),force:l.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:l.string().optional().describe("project root (default: session directory)")},async execute(e,o){let i=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)i.push("--dry-run");if(e.force)i.push("--force");let r=await w(_(o.directory,e.project_dir),i,o.abort);if(!r.ok){let p=r.changes??[],h=(r.notes??[]).map((g)=>`
34
+ `+r:"")}catch{}Z(e.sessionID,e.tool,e.args,String(o?.output??""))},"experimental.chat.system.transform":async(e,o)=>{o.system.push(Qe);let i=me(e?.sessionID);if(i)o.system.push(i)},tool:a=ct(ot({ecuc_read:S({description:`THE way to answer any question about configured ECUC values (use it instead of grep/read on .arxml). Read AUTOSAR ECUC configuration as compact ECUC JSON (keyed by def names: "<DefName>:<ShortName>" for containers, params by def short-name, references as "->/path"; every container carries @path = its AUTOSAR path, @def = its DEFINITION-REF and @at = its file:line; every value is followed by "@at:<Param>" = file:line of its VALUE element, and integers by "@hex:<Param>" ("@dec:<Param>" when the ARXML text is hex)). No path: lists modules and the param-def/config files found. path may be a full AUTOSAR path, a SHORT-NAME ('DoorLatch') or a partial path ('ComConfig/DoorLatch', 'DoorLatch/ComBitPosition'); ambiguous names return candidate paths. A module/container: its values to `+"`depth` levels (merged across split files). A param path (\u2026/Container/Param) or a def path "+"(/AUTOSAR/EcucDefs/\u2026): the value and/or the definition. schema=true adds the def summary (allowed "+"params with type/range/literals/multiplicity, allowed sub-containers) \u2014 read it before ecuc_edit. "+"Never read or edit ARXML by hand."+C,args:{path:l.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:l.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:l.boolean().optional().describe("include the param-def summary for the element"),project_dir:l.string().optional().describe("project root (default: session directory)")},async execute(e,o){let i=["read"];if(e.path)i.push(e.path);if(i.push("--depth",String(e.depth??2)),e.schema)i.push("--schema");let r=await w(_(o.directory,e.project_dir),i,o.abort);if(!r.ok)return v(r)+A(r);let{ok:d,engine:c,ms:u,warnings:p,...h}=r;return v(r)+JSON.stringify(h,null,1)}}),ecuc_edit:S({description:"Edit ECUC ARXML in place \u2014 the ONLY way to change configuration (edit/write/sed on .arxml are blocked). "+"Get exact paths and allowed params from ecuc_read (schema=true) first. Ops by AUTOSAR path: {op:'set', path, param, value} (value scalar, '->/ref/path', or a list for multi-valued "+"params), {op:'add', path:<parent>, container:'DefName:ShortName', values:{\u2026ECUC JSON\u2026}}, {op:'add', "+"path, param, value} (append a value), {op:'remove', path[, param[, value]]}. A NEW module configuration (e.g. adding the Pwm/Icu/Adc driver config): {op:'add', path:'/ActiveEcuC', container:'Pwm'[, file:'Config/Pwm_ecuc.arxml']} creates its file and module element; then add its containers (validation lists the mandatory ones still missing). 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."+C,args:{ops:R(l.array(R(l.object({op:l.enum(["set","add","remove"]),path:l.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:l.string().optional().describe("parameter/reference def short-name"),value:l.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:l.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:R(l.record(l.string(),l.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:l.boolean().optional().describe("compute and show the diff without writing"),force:l.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:l.string().optional().describe("project root (default: session directory)")},async execute(e,o){let i=["edit","--ops",JSON.stringify(e.ops)];if(e.dry_run)i.push("--dry-run");if(e.force)i.push("--force");let r=await w(_(o.directory,e.project_dir),i,o.abort);if(!r.ok){let p=r.changes??[],h=(r.notes??[]).map((g)=>`
35
35
  NOTE: ${g}`).join("");return v(r)+A(r)+h+(p.length?`
36
36
  Rejected diff:
37
37
  ${p.map((g)=>g.diff).join(`
@@ -47,7 +47,7 @@ DOCUMENT CHECK UNAVAILABLE: ${U}`:["","","## VALIDATION 2 \u2014 against the ing
47
47
  `)}}}),ecuc_crosscheck:S({description:"Per-module cross-check table: runs the semantic rule packs for one module (Com, PduR, EcuC, NvM, Fee, Ea, "+"CanIf, Dem, ...) \u2014 config vs System/ECU extract (H2L-SYS), gateway routing vs extract (H2L-PDUR), NvM/Fee "+"length vs the C RAM object's sizeof (H2L-NVM), memory capacity (H2L-MEM), signal packing and handle IDs \u2014 "+"and returns, per element, the findings, plus notes on what could NOT be checked. Run it for every module present after ecuc_validate; a clean validate alone is not proof of a correct configuration."+C,args:{module:l.string().describe("module name, e.g. PduR"),project_dir:l.string().optional()},async execute(e,o){let i=await w(_(o.directory,e.project_dir),["crosscheck",e.module],o.abort);if(!i.ok)return v(i)+A(i);let r=i.rows,d=i.otherFindings??[];return{title:`ecuc_crosscheck ${e.module}: ${i.summary}`,output:[`${e.module}: ${i.summary} (element kinds: ${i.elementKinds.join(", ")||"none"})`,...r.map((c)=>`${c.element} (${c.at})
48
48
  ${c.findings.join(`
49
49
  `)}`),...I(d,50),...(i.notes??[]).map((c)=>`NOTE: ${c}`)].join(`
50
- `)}}}),arxml_edit:S({description:"Structured edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces), including NEW files \u2014 never write "+"that XML by hand. Ops: {op:'new_file', file:'Input/SWC_X.arxml', packages:['/Swcs','/Interfaces']} (empty R25-11 file; later ops in the same call can fill it), {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested} | [list]}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Missing packages along parent_path are created (top level needs `file` when the project has several ARXML files). Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added earlier in the same call), elements are placed and ordered per the AUTOSAR schema, references are re-checked and the file is strictly re-loaded; nothing is written if anything fails. For ECUC values use ecuc_edit; to add a whole signal use autosar_add_signal.",args:{ops:E(l.array(E(l.record(l.string(),l.any()))).min(1)).describe("list of ops"),dry_run:l.boolean().optional(),project_dir:l.string().optional()},async execute(e,o){let i=["arxml-edit","--ops",JSON.stringify(N(e.ops))];if(e.dry_run)i.push("--dry-run");let r=await w(_(o.directory,e.project_dir),i,o.abort),d=r.changes??[];if(!r.ok)return A(r)+(r.problems?.length?`
50
+ `)}}}),arxml_edit:S({description:"Structured edit of NON-ECUC ARXML (System/ECU extract, SWC, interfaces), including NEW files \u2014 never write "+"that XML by hand. Ops: {op:'new_file', file:'Input/SWC_X.arxml', packages:['/Swcs','/Interfaces']} (empty R25-11 file; later ops in the same call can fill it), {op:'add', parent_path, element:'I-SIGNAL', short_name, values:{TAG: value | '->/ref/path' | {nested} | [list]}}, {op:'set', path, element:'START-POSITION', value}, {op:'remove', path}. Missing packages along parent_path are created (top level needs `file` when the project has several ARXML files). Every *-REF gets its DEST from the referenced element's real tag (targets must exist or be added earlier in the same call), elements are placed and ordered per the AUTOSAR schema, references are re-checked and the file is strictly re-loaded; nothing is written if anything fails. For ECUC values use ecuc_edit; to add a whole signal use autosar_add_signal.",args:{ops:R(l.array(R(l.record(l.string(),l.any()))).min(1)).describe("list of ops"),dry_run:l.boolean().optional(),project_dir:l.string().optional()},async execute(e,o){let i=["arxml-edit","--ops",JSON.stringify(N(e.ops))];if(e.dry_run)i.push("--dry-run");let r=await w(_(o.directory,e.project_dir),i,o.abort),d=r.changes??[];if(!r.ok)return A(r)+(r.problems?.length?`
51
51
  ${r.problems.join(`
52
52
  `)}`:"")+(d.length?`
53
53
  Rejected diff:
@@ -61,7 +61,7 @@ NOTE: ${c.join(`
61
61
  NOTE: `)}`:"")+`
62
62
  ${d.map((h)=>h.diff).join(`
63
63
  `)}`+(p||`
64
- Next: ecuc_validate (checks extract/SWC files too).`)}}),autosar_add_like:S({description:"Add a new X LIKE an existing Y, end to end, in one checked step: a new message (frame, I-PDU, triggerings, EcuC Pdu, CanIf PDU, PduR route, Com I-PDU), port, runnable with its event and task mapping, Dem event + DTC, NvM block + Fee block, interface... Projects name a thing's elements after it, so the engine clones every element whose SHORT-NAME contains `like` and that is linked to the others by references, across ECUC and System/SWC ARXML, renamed to `name`; lists that contain the template get the clone too; handle IDs get the lowest free value; the template's own members (a message's signals) are left out unless named in `also`. `set` then sets values on the clones by ECUC parameter name, XML tag, or 'Owner:key'. Schema + references are checked and validation runs before/after: any new finding rolls everything back. PREFER THIS over building a chain op by op with arxml_edit/ecuc_edit. Example: like='BCM_DoorStatus', name='BCM_SettingsAck', also={'DrvDoorOpenSts':'SettingsAckCounter'}, set={'IDENTIFIER':'0x4F8','CanIfTxPduCanId':'0x4F8','PduLength':2,'FRAME-LENGTH':2,'I-SIGNAL-I-PDU:LENGTH':2,'ComTxModeTimePeriod':1.0,'SettingsAckCounter:ComBitSize':8}. Pick as template the existing element most like the new one (same direction/bus/kind); dry_run first.",args:{like:l.string().describe("name stem of the template element, e.g. BCM_DoorStatus (an existing message), DoorLockActuatorFault (a Dem event)"),name:l.string().describe("the new stem, e.g. BCM_SettingsAck"),also:E(l.record(l.string(),l.string())).optional().describe("template members to clone and rename too: {template stem: new stem}"),set:E(l.record(l.string(),l.any())).optional().describe("values on the clones: {ParamDefName | XML-TAG | 'Owner:key': value}"),seed:l.string().optional().describe("one element of the template chain, when several separate chains carry the stem"),keep_members:l.boolean().optional().describe("also copy the template's own members (default false)"),dry_run:l.boolean().optional(),project_dir:l.string().optional()},async execute(e,o){let i=["add-like","--like",e.like,"--name",e.name];if(e.also)i.push("--also",JSON.stringify(N(e.also)));if(e.set)i.push("--set",JSON.stringify(N(e.set)));if(e.seed)i.push("--seed",e.seed);if(e.keep_members)i.push("--keep-members");if(e.dry_run)i.push("--dry-run");let r=await w(_(o.directory,e.project_dir),i,o.abort),d=(u)=>r[u]??[],c=[r.ok?`${r.written?"Written":"Dry run OK"}: ${d("created").length} element(s) like ${e.like} as ${e.name}`:`NOT DONE: ${r.error}`,...d("created").length?["Created:",...d("created").map((u)=>` ${u}`)]:[],...d("set").length?["Set:",...d("set").map((u)=>` ${u}`)]:[],...d("ids").length?["Handle IDs:",...d("ids").map((u)=>` ${u}`)]:[],...d("linked").length?["Linked:",...d("linked").map((u)=>` ${u}`)]:[],...d("notCopied").length?[`Not copied (the template's own members): ${d("notCopied").length}`,...d("notCopied").slice(0,12).map((u)=>` ${u}`)]:[],...d("singleValuedNotLinked").length?["Single-valued references to the template NOT changed (add/point your own if the new element needs one):",...d("singleValuedNotLinked").map((u)=>` ${u}`)]:[],...d("notes").map((u)=>`NOTE: ${u}`),...d("todo").length?["STILL TO DO (completeness, not undone):",...d("todo").map((u)=>` ${u}`)]:[],...(r.problems??[]).map((u)=>`PROBLEM: ${u}`),...(r.diagnostics??[]).length?I(r.diagnostics,20):[]];if(r.ok&&!r.written)c.push("Next: run again without dry_run.");if(r.ok&&r.written)c.push(String(r.next??""));return c.join(`
64
+ Next: ecuc_validate (checks extract/SWC files too).`)}}),autosar_add_like:S({description:"Add a new X LIKE an existing Y, end to end, in one checked step: a new message (frame, I-PDU, triggerings, EcuC Pdu, CanIf PDU, PduR route, Com I-PDU), port, runnable with its event and task mapping, Dem event + DTC, NvM block + Fee block, interface... Projects name a thing's elements after it, so the engine clones every element whose SHORT-NAME contains `like` and that is linked to the others by references, across ECUC and System/SWC ARXML, renamed to `name`; lists that contain the template get the clone too; handle IDs get the lowest free value; the template's own members (a message's signals) are left out unless named in `also`. `set` then sets values on the clones by ECUC parameter name, XML tag, or 'Owner:key'. Schema + references are checked and validation runs before/after: any new finding rolls everything back. PREFER THIS over building a chain op by op with arxml_edit/ecuc_edit. Example: like='BCM_DoorStatus', name='BCM_SettingsAck', also={'DrvDoorOpenSts':'SettingsAckCounter'}, set={'IDENTIFIER':'0x4F8','CanIfTxPduCanId':'0x4F8','PduLength':2,'FRAME-LENGTH':2,'I-SIGNAL-I-PDU:LENGTH':2,'ComTxModeTimePeriod':1.0,'SettingsAckCounter:ComBitSize':8}. Pick as template the existing element most like the new one (same direction/bus/kind); dry_run first.",args:{like:l.string().describe("name stem of the template element, e.g. BCM_DoorStatus (an existing message), DoorLockActuatorFault (a Dem event)"),name:l.string().describe("the new stem, e.g. BCM_SettingsAck"),also:R(l.record(l.string(),l.string())).optional().describe("template members to clone and rename too: {template stem: new stem}"),set:R(l.record(l.string(),l.any())).optional().describe("values on the clones: {ParamDefName | XML-TAG | 'Owner:key': value}"),seed:l.string().optional().describe("one element of the template chain, when several separate chains carry the stem"),keep_members:l.boolean().optional().describe("also copy the template's own members (default false)"),dry_run:l.boolean().optional(),project_dir:l.string().optional()},async execute(e,o){let i=["add-like","--like",e.like,"--name",e.name];if(e.also)i.push("--also",JSON.stringify(N(e.also)));if(e.set)i.push("--set",JSON.stringify(N(e.set)));if(e.seed)i.push("--seed",e.seed);if(e.keep_members)i.push("--keep-members");if(e.dry_run)i.push("--dry-run");let r=await w(_(o.directory,e.project_dir),i,o.abort),d=(u)=>r[u]??[],c=[r.ok?`${r.written?"Written":"Dry run OK"}: ${d("created").length} element(s) like ${e.like} as ${e.name}`:`NOT DONE: ${r.error}`,...d("created").length?["Created:",...d("created").map((u)=>` ${u}`)]:[],...d("set").length?["Set:",...d("set").map((u)=>` ${u}`)]:[],...d("ids").length?["Handle IDs:",...d("ids").map((u)=>` ${u}`)]:[],...d("linked").length?["Linked:",...d("linked").map((u)=>` ${u}`)]:[],...d("notCopied").length?[`Not copied (the template's own members): ${d("notCopied").length}`,...d("notCopied").slice(0,12).map((u)=>` ${u}`)]:[],...d("singleValuedNotLinked").length?["Single-valued references to the template NOT changed (add/point your own if the new element needs one):",...d("singleValuedNotLinked").map((u)=>` ${u}`)]:[],...d("notes").map((u)=>`NOTE: ${u}`),...d("todo").length?["STILL TO DO (completeness, not undone):",...d("todo").map((u)=>` ${u}`)]:[],...(r.problems??[]).map((u)=>`PROBLEM: ${u}`),...(r.diagnostics??[]).length?I(r.diagnostics,20):[]];if(r.ok&&!r.written)c.push("Next: run again without dry_run.");if(r.ok&&r.written)c.push(String(r.next??""));return c.join(`
65
65
  `)}}),autosar_add_signal:S({description:"Add one Com signal end to end, consistently: ComSignal (ComHandleId = lowest unused; existing handles never move) + ComIPduSignalRef in the ECUC, SYSTEM-SIGNAL / I-SIGNAL / I-SIGNAL-TO-I-PDU-MAPPING and the rest of the signal chain (triggering, port, extract references) in the System extract, and the DBC SG_ line when a .dbc carries the frame. Everything is derived by following references from an existing signal of the same I-PDU. It refuses on overlap or out-of-bounds bits, picks the first free byte-aligned position when bit_position is omitted, runs ecuc_validate after writing and rolls back on any new finding. Then run autosar_generate, use ComConf_ComSignal_<name> in the C code, and autosar_build.",args:{name:l.string(),ipdu:l.string().describe("ComIPdu AUTOSAR path or SHORT-NAME"),bit_size:l.number().int().min(1).max(64).optional(),type:l.string().optional().describe("ComSignalType, e.g. UINT8 (default)"),bit_position:l.number().int().min(0).optional().describe("omit for the first free byte-aligned position"),endianness:l.enum(["LITTLE_ENDIAN","BIG_ENDIAN","OPAQUE"]).optional(),init:l.union([l.number(),l.string()]).optional(),transfer_property:l.string().optional().describe("e.g. PENDING, TRIGGERED, TRIGGERED_ON_CHANGE"),dry_run:l.boolean().optional(),project_dir:l.string().optional()},async execute(e,o){let i=["add-signal","--name",e.name,"--ipdu",e.ipdu,"--type",e.type??"UINT8"];if(e.bit_size!==void 0)i.push("--bit-size",String(e.bit_size));if(e.bit_position!==void 0)i.push("--bit-position",String(e.bit_position));if(e.endianness)i.push("--endianness",e.endianness);if(e.init!==void 0)i.push("--init",String(e.init));if(e.transfer_property)i.push("--transfer-property",e.transfer_property);if(e.dry_run)i.push("--dry-run");let r=await w(_(o.directory,e.project_dir),i,o.abort);if(!r.ok)return A(r)+(r.diagnostics?.length?`
66
66
  ${I(r.diagnostics,20).join(`
67
67
  `)}`:"");let{ok:d,engine:c,ms:u,...p}=r;return JSON.stringify(p,null,1)}}),autosar_audit:S({description:"Integration review \u2014 run this FIRST when reviewing C code that uses BSW services. Deterministically scans "+"C files (default: .hydron/autosar.json `audit` paths, else every .c in the project except generated files, "+"recognised by banner or *_Cfg/*_PBcfg/Rte* names \u2014 folder names do not matter) for Com_Send/ReceiveSignal[Group], "+"NvM_Read/WriteBlock/RestoreBlockDefaults, Dem_SetEventStatus/ReportErrorStatus, PduR_*Transmit and Rte_Write_/Rte_Read_ (resolved through the generated Output/ RTE headers), resolves every handle through the ECUC (symbolic name or raw number), and reports: H2L-API-001 raw numeric handle, -002 unknown handle, -003 direction mismatch (send on a RECEIVE I-PDU signal or receive on SEND), -004 C object width vs ComSignalType, -005 NvM NULL_PTR without a permanent RAM block (NULL_PTR WITH one is valid, SWS_NvM_00900). Each finding has file:line, evidence (configured values + AUTOSAR paths) and a spec citation; calls it cannot check are listed as unchecked, never guessed. Confirm findings with ecuc_read / autosar_trace.",args:{paths:D().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:l.string().optional().describe("project root (default: session directory)")},async execute(e,o){let i=_(o.directory,e.project_dir),r=await w(i,["audit",...T(e.paths)??[]],o.abort);if(!r.ok)return v(r)+A(r);let d=(r.notes??[]).map((m)=>`NOTE: ${m}`),{findings:c,unchecked:u,scanned:p}=r,h=[`Scanned ${p.sources.length} C file(s), ${p.calls} BSW call(s): ${c.length?JSON.stringify(r.summary):"no findings"}`,...c.map((m)=>`${m.file}:${m.line} ${m.severity.toUpperCase()} ${m.rule} ${m.call}
@@ -201,6 +201,27 @@ class Editor:
201
201
  names = ", ".join(self.p.defs.by_path[x].name for x in pool) or "none"
202
202
  raise EditError(f"{d.name} has no {want} def '{name}' (allowed: {names})")
203
203
 
204
+ def _infer_container_def(self, e: Elem, name: str, values) -> tuple:
205
+ """A container given by SHORT-NAME only ('McuClockReferencePoint_Fgpt12'): its def is the allowed child def
206
+ the name starts with, else the one whose params cover the given values, else the only one allowed."""
207
+ d = _resolve_def(self.p.defs, e.def_ref)
208
+ if d is None:
209
+ return name, ""
210
+ allowed = [self.p.defs.by_path[x] for x in d.containers if x in self.p.defs.by_path]
211
+ if any(a.name == name for a in allowed):
212
+ return name, ""
213
+ pick = sorted((a for a in allowed if name.startswith(a.name)), key=lambda a: -len(a.name))[:1]
214
+ if not pick and isinstance(values, dict) and values:
215
+ keys = set(values)
216
+ pick = [a for a in allowed if keys <= {self.p.defs.by_path[x].name for x in a.params + a.refs if x in self.p.defs.by_path}]
217
+ pick = pick if len(pick) == 1 else []
218
+ if not pick and len(allowed) == 1:
219
+ pick = allowed
220
+ if not pick:
221
+ return name, ""
222
+ self.note(f"container {name!r} read as '{pick[0].name}:{name}' (the def is the name before ':')")
223
+ return pick[0].name, name
224
+
204
225
  def split_param_path(self, op: dict):
205
226
  path, param = op.get("path", "").rstrip("/"), op.get("param")
206
227
  if param or self.p.find(path):
@@ -311,9 +332,11 @@ class Editor:
311
332
  if not key:
312
333
  raise EditError("add needs 'container' (\"DefName:ShortName\") or 'param' + 'value'")
313
334
  dname, _, sname = key.partition(":")
335
+ g = self.container(path)
336
+ if not sname:
337
+ dname, sname = self._infer_container_def(g[0], dname, op.get("values"))
314
338
  unnamed = not sname
315
339
  sname = sname or dname
316
- g = self.container(path)
317
340
  try:
318
341
  cd = self.child_def(g[0], dname, "container")
319
342
  except EditError as first:
@@ -471,6 +494,64 @@ def canonical_path(project: Project, path: str):
471
494
  return out, (dropped if not rest else None)
472
495
 
473
496
 
497
+ def _def_to_value(project: Project, segs: list):
498
+ """'/AUTOSAR/EcucDefs/Gpt/GptConfigurationOfOptApiServices/X' -> the value path of the one container whose
499
+ definition is the longest def-path prefix, plus the rest; None when no prefix maps to exactly one container."""
500
+ if not project.defs:
501
+ return None
502
+ for k in range(len(segs), 1, -1):
503
+ dpath = "/" + "/".join(segs[:k])
504
+ d = project.defs.get(dpath)
505
+ if d is None:
506
+ continue
507
+ rel = project.defs.rel(dpath)
508
+ hits = sorted({es[0].path for es in project.by_path.values()
509
+ if es[0].def_ref and project.defs.rel(es[0].def_ref) == rel})
510
+ if len(hits) == 1:
511
+ return "/".join([hits[0]] + segs[k:])
512
+ return None
513
+ return None
514
+
515
+
516
+ def _path_forms(project: Project, path, param):
517
+ """Rewrite path forms that name the right element the wrong way; None = leave as is."""
518
+ if not isinstance(path, str) or not path.strip():
519
+ return None
520
+ p = path.strip().rstrip("/")
521
+ if project.find(p):
522
+ return None
523
+ segs = [s for s in p.split("/") if s]
524
+ mods = {e.name: e.path for es in project.by_path.values() for e in es if e.parent is None}
525
+ pkgs = {mp.rsplit("/", 1)[0] for mp in mods.values()}
526
+ # the parameter repeated at the end of the path ({path: .../Container/Param, param: Param})
527
+ if isinstance(param, str) and segs and segs[-1] == param:
528
+ segs = segs[:-1]
529
+ # a definition path (/AUTOSAR/EcucDefs/<Module>/<ContainerDef>/...) instead of the value path
530
+ if project.defs and project.defs.get("/" + "/".join(segs[:3])) is not None and segs[1:2] != [""]:
531
+ v = _def_to_value(project, segs)
532
+ if v:
533
+ return v
534
+ cand = "/" + "/".join(segs)
535
+ if project.find(cand) or (param is None and project.find(cand.rsplit("/", 1)[0]) and len(segs) > 2):
536
+ return cand
537
+ # relative ("ActiveEcuC/Gpt/...") or wrong/missing package ("/Pkg/Gpt/...", "Gpt/...")
538
+ for i, s in enumerate(segs):
539
+ if s in mods:
540
+ alt = mods[s] + ("/" + "/".join(segs[i + 1:]) if segs[i + 1:] else "")
541
+ if project.find(alt) or project.find(alt.rsplit("/", 1)[0]):
542
+ return alt
543
+ break
544
+ if "/" + segs[0] in pkgs:
545
+ return cand
546
+ # a bare SHORT-NAME / partial path that names exactly one element
547
+ if len(segs) <= 3:
548
+ from .ecucjson import resolve
549
+ r = resolve(project, "/".join(segs))
550
+ if isinstance(r, str) and r.startswith("/"):
551
+ return r
552
+ return cand if cand != path else None
553
+
554
+
474
555
  def normalize_ops(project: Project, ops, notes: list):
475
556
  """Accept the shapes agents use for ECUC edits (arxml_edit's keys, SHORT-NAME given separately, refs as objects,
476
557
  params added to a container that does not exist yet) and rewrite them into ecuc_edit's own; note each rewrite."""
@@ -492,6 +573,10 @@ def normalize_ops(project: Project, ops, notes: list):
492
573
  rv = op.pop(rk)
493
574
  op["values"] = {**(op.get("values") if isinstance(op.get("values"), dict) else {}), **rv}
494
575
  notes.append(f"op {i}: '{rk}' merged into values")
576
+ fixed = _path_forms(project, op.get("path"), op.get("param"))
577
+ if fixed is not None and fixed != op.get("path"):
578
+ notes.append(f"op {i}: path {op.get('path')} read as {fixed}")
579
+ op["path"] = fixed
495
580
  pth = op.get("path")
496
581
  if isinstance(pth, str) and pth.startswith("/") and not project.find(pth.rstrip("/")):
497
582
  cp = canonical_path(project, pth)
@@ -628,11 +713,109 @@ def schema_check(path: str, data: bytes) -> Optional[str]:
628
713
  return str(e)
629
714
 
630
715
 
716
+ def _module_text(pkg_segs: list, mod: D.DefNode, variant: str, schema_loc: str) -> bytes:
717
+ ind = lambda n: " " * n # noqa: E731
718
+ lines = ['<?xml version="1.0" encoding="UTF-8"?>',
719
+ f'<AUTOSAR xmlns="http://autosar.org/schema/r4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
720
+ f'xsi:schemaLocation="{schema_loc}">']
721
+ d = 1
722
+ for s in pkg_segs:
723
+ lines += [ind(d) + "<AR-PACKAGES>", ind(d + 1) + "<AR-PACKAGE>", ind(d + 2) + f"<SHORT-NAME>{escape(s)}</SHORT-NAME>"]
724
+ d += 2
725
+ lines += [ind(d) + "<ELEMENTS>", ind(d + 1) + "<ECUC-MODULE-CONFIGURATION-VALUES>",
726
+ ind(d + 2) + f"<SHORT-NAME>{escape(mod.name)}</SHORT-NAME>",
727
+ ind(d + 2) + f'<DEFINITION-REF DEST="ECUC-MODULE-DEF">{escape(mod.path)}</DEFINITION-REF>',
728
+ ind(d + 2) + f"<IMPLEMENTATION-CONFIG-VARIANT>{variant}</IMPLEMENTATION-CONFIG-VARIANT>",
729
+ ind(d + 2) + "<CONTAINERS>", ind(d + 2) + "</CONTAINERS>",
730
+ ind(d + 1) + "</ECUC-MODULE-CONFIGURATION-VALUES>", ind(d) + "</ELEMENTS>"]
731
+ for _ in pkg_segs:
732
+ d -= 2
733
+ lines += [ind(d + 1) + "</AR-PACKAGE>", ind(d) + "</AR-PACKAGES>"]
734
+ lines.append("</AUTOSAR>")
735
+ return ("\n".join(lines) + "\n").encode()
736
+
737
+
738
+ def new_modules(project: Project, ed: "Editor", ops: list) -> list:
739
+ """Create the ECUC module configurations the ops need and the project does not have yet.
740
+
741
+ {"op": "add", "path": "/ActiveEcuC", "container": "Pwm"} creates the Pwm module configuration (also when the
742
+ container is written "Pwm:Pwm"); an op whose path is "/<Pkg>/<Module>/..." for a module that has a definition but
743
+ no configuration creates it first. Optional keys on the creating op: "file" (default: next to the existing
744
+ module files, named like them), "variant" (default: the variant most existing modules use, if the def supports
745
+ it, else the def's first). The new file is created in memory (dry runs show it) and written with the edit."""
746
+ if not project.defs or not isinstance(ops, list):
747
+ return ops
748
+ mods_by_name = {m.name: m for m in project.defs.modules.values()}
749
+ have = {e.name: e for es in project.by_path.values() for e in es if e.parent is None}
750
+ out = []
751
+ for i, op in enumerate(ops):
752
+ if not isinstance(op, dict):
753
+ out.append(op)
754
+ continue
755
+ path = str(op.get("path") or "").rstrip("/")
756
+ segs = [s for s in path.split("/") if s]
757
+ want, creating_op = None, False
758
+ cont = op.get("container")
759
+ cname = cont.split(":")[-1] if isinstance(cont, str) else None
760
+ cdef = cont.split(":")[0] if isinstance(cont, str) else None
761
+ if op.get("op") == "add" and len(segs) >= 1 and not project.find(path) and cname in mods_by_name \
762
+ and cdef in (cname, "ECUC-MODULE-CONFIGURATION-VALUES", "EcucModuleConfigurationValues"):
763
+ want, pkg, creating_op = cname, segs, True
764
+ elif len(segs) >= 2 and segs[1] in mods_by_name and segs[1] not in have and not project.find("/" + "/".join(segs[:2])):
765
+ want, pkg = segs[1], segs[:1]
766
+ if want is None or want in have:
767
+ if creating_op is False or want is None:
768
+ out.append(op)
769
+ continue
770
+ mdef = mods_by_name[want]
771
+ cfgs = sorted({e.file for e in have.values()})
772
+ ref = cfgs[0] if cfgs else None
773
+ if op.get("file"):
774
+ f = op["file"] if os.path.isabs(op["file"]) else os.path.join(project.root, op["file"])
775
+ elif ref:
776
+ base = os.path.basename(ref)
777
+ other = next((e.name for e in have.values() if e.file == ref), "")
778
+ fname = base.replace(other, want, 1) if other and base.startswith(other) else f"{want}_ecuc.arxml"
779
+ f = os.path.join(os.path.dirname(ref), fname)
780
+ else:
781
+ f = os.path.join(project.root, "Config", f"{want}_ecuc.arxml")
782
+ if os.path.exists(f) or f in project.files:
783
+ raise EditError(f"cannot create module {want}: {project.rel(f)} already exists (pass 'file')")
784
+ used = [e.node.child_text("IMPLEMENTATION-CONFIG-VARIANT") if hasattr(e.node, "child_text") else None
785
+ for e in have.values()]
786
+ supported = mdef.variants or []
787
+ variant = op.get("variant") or next((u for u in sorted(set(u for u in used if u), key=used.count, reverse=True)
788
+ if not supported or u in supported), None) \
789
+ or (supported[0] if supported else "VARIANT-PRE-COMPILE")
790
+ schema_loc = "http://autosar.org/schema/r4.0 AUTOSAR_00054.xsd"
791
+ if ref:
792
+ import re as _re
793
+ m = _re.search(rb'xsi:schemaLocation="([^"]+)"', project.files[ref].data[:2000])
794
+ if m:
795
+ schema_loc = m.group(1).decode()
796
+ data = _module_text(pkg, mdef, variant, schema_loc)
797
+ err = schema_check(f, data)
798
+ if err:
799
+ raise EditError(f"internal: new module file for {want} is not schema-valid: {err}")
800
+ ed.original[f] = b""
801
+ project._load(f, data)
802
+ if f not in project.sources.config:
803
+ project.sources.config.append(f)
804
+ project.reindex()
805
+ have = {e.name: e for es in project.by_path.values() for e in es if e.parent is None}
806
+ ed.notes.append(f"op {i}: created module configuration {want} (/{'/'.join(pkg)}/{want}, {variant}) in "
807
+ f"{project.rel(f)}")
808
+ if not creating_op:
809
+ out.append(op)
810
+ return out
811
+
812
+
631
813
  def edit(project: Project, ops: list, dry_run: bool = False, force: bool = False, run_validate: bool = True) -> dict:
632
814
  ed = Editor(project)
633
815
  ed.notes = []
634
816
  try:
635
817
  ops = normalize_ops(project, ops, ed.notes)
818
+ ops = new_modules(project, ed, ops)
636
819
  ed.apply(ops, force=force)
637
820
  for op in ops if isinstance(ops, list) else []:
638
821
  if not isinstance(op, dict):
@@ -650,7 +833,7 @@ def edit(project: Project, ops: list, dry_run: bool = False, force: bool = False
650
833
  res: dict = {"ok": True, "applied": len(ops), "changes": changes, "written": False, "notes": ed.notes}
651
834
  schema = []
652
835
  for f, before in ed.original.items():
653
- if schema_check(f, before) is None:
836
+ if before == b"" or schema_check(f, before) is None:
654
837
  err = schema_check(f, ed.data(f))
655
838
  if err:
656
839
  schema.append({"file": project.rel(f), "error": err})