@anik-2/autosar 0.0.0-stage → 0.1.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/.hydron-plugin/plugin.json +3 -0
- package/README.md +55 -2
- package/agents/autosar-integrator.md +44 -0
- package/commands/autosar-trace.md +11 -0
- package/commands/autosar-validate.md +12 -0
- package/package.json +44 -4
- package/plugins/index.js +21 -0
- package/python/ecuc_engine/__init__.py +2 -0
- package/python/ecuc_engine/__main__.py +5 -0
- package/python/ecuc_engine/cli.py +111 -0
- package/python/ecuc_engine/defs.py +330 -0
- package/python/ecuc_engine/diff.py +111 -0
- package/python/ecuc_engine/ecucjson.py +154 -0
- package/python/ecuc_engine/edit.py +429 -0
- package/python/ecuc_engine/model.py +330 -0
- package/python/ecuc_engine/rules/__init__.py +82 -0
- package/python/ecuc_engine/rules/com.py +107 -0
- package/python/ecuc_engine/rules/handles.py +42 -0
- package/python/ecuc_engine/validate.py +227 -0
- package/python/ecuc_engine/values.py +146 -0
- package/python/ecuc_engine/xmltree.py +141 -0
- package/python/pyproject.toml +13 -0
- package/skills/autosar/SKILL.md +70 -0
- package/skills/autosar/references/config-classes.md +23 -0
- package/skills/autosar/references/ecuc-model.md +42 -0
- package/skills/autosar/references/project-layout.md +29 -0
- package/skills/autosar/references/symbolic-names.md +25 -0
- package/skills/autosar/references/workflow.md +29 -0
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @h2loop/autosar — AUTOSAR Classic pack for Hydron
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Test builds are published as `@anik-2/autosar` (temporary scope). The release name is
|
|
4
|
+
> `@h2loop/autosar`; see PACKAGING.md.
|
|
5
|
+
|
|
6
|
+
Deterministic tools for AUTOSAR Classic integration work, so the agent never hand-writes ARXML:
|
|
7
|
+
|
|
8
|
+
| Tool | What it does | Runs |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `ecuc_read` | ECUC configuration as compact ECUC JSON; def summaries (`schema`) | local (Python engine) |
|
|
11
|
+
| `ecuc_edit` | set / add / remove by AUTOSAR path; in-place byte patches; all-or-nothing; schema check | local |
|
|
12
|
+
| `ecuc_validate` | multiplicity, mandatory, range, enum, type, string, ref target/dest, dangling, duplicates → LSP-like diagnostics | local |
|
|
13
|
+
| `arxml_diff` | semantic diff by AUTOSAR path (ignores UUIDs, order, whitespace, file splits) | local |
|
|
14
|
+
| `autosar_trace` | config ↔ generated C symbol ↔ in-house callers, via the H2Loop code graph | H2Loop backend |
|
|
15
|
+
| `autosar_config_lookup` | element + children + refs in/out + links from the code graph | H2Loop backend |
|
|
16
|
+
| `autosar_generate` / `autosar_build` | run the project's configured generator / build command (asks permission) | local |
|
|
17
|
+
|
|
18
|
+
Plus the `autosar` skill (ECUC model, symbolic-name rule, config classes, project layout, the
|
|
19
|
+
edit → validate → generate → build → trace loop), the `autosar-integrator` subagent, and the
|
|
20
|
+
`/autosar-validate` and `/autosar-trace` commands.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
```bash
|
|
24
|
+
hydron plugin add @h2loop/autosar # or a local path / --project
|
|
25
|
+
```
|
|
26
|
+
Requirements: [uv](https://docs.astral.sh/uv/) (preferred; adds the strict ARXML schema check via
|
|
27
|
+
`autosar-data`) or Python ≥ 3.10 on PATH (`AUTOSAR_PYTHON` overrides). The backend tools use the
|
|
28
|
+
Hydron login and selected H2Loop project (or `workspace_id`); `KILO_API_URL` overrides the backend.
|
|
29
|
+
|
|
30
|
+
## Project configuration
|
|
31
|
+
Param definitions are not bundled. The engine finds them in this order: `.hydron/autosar.json`
|
|
32
|
+
`paramDefs` globs → any project `.arxml` containing `ECUC-MODULE-DEF` (vendor BSWMD) →
|
|
33
|
+
`$AUTOSAR_PARAMDEFS` (path list). Without defs, values are readable but not validated.
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"paramDefs": ["SIP/**/bswmd/*.arxml"],
|
|
38
|
+
"config": ["Config/**/*.arxml"],
|
|
39
|
+
"input": ["Input/**/*.arxml"],
|
|
40
|
+
"generate": {"command": "<vendor-cli> --project Config/Ecu.dpa --generate {module}", "timeoutSec": 3600},
|
|
41
|
+
"build": "make -C Build all"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Engine CLI
|
|
46
|
+
```bash
|
|
47
|
+
cd python && uv run --no-project --with autosar-data==0.17.0 python -m ecuc_engine --root <project> \
|
|
48
|
+
<info|read [path] [--depth N] [--schema]|edit --ops '<json>' [--dry-run]|validate [--module M] [--file F]|diff A B> --json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Develop
|
|
52
|
+
```bash
|
|
53
|
+
bun install && bun run build && bun run typecheck
|
|
54
|
+
bun run test:py # pytest; R25-11 integration tests run when AUTOSAR_R25_DEFS (or the eval_work copy) exists
|
|
55
|
+
hydron plugin validate .
|
|
56
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
mode: subagent
|
|
3
|
+
displayName: AUTOSAR Integrator
|
|
4
|
+
description: >-
|
|
5
|
+
Carries out one AUTOSAR Classic integration task end to end — e.g. "add a TX signal DoorLatch to
|
|
6
|
+
ComIPdu DoorStatus_Tx", "fix the ecuc_validate errors in Com", "what breaks if I rename this
|
|
7
|
+
PduR path?" — through the edit → validate → generate → build → trace loop, and returns a short
|
|
8
|
+
change report with AUTOSAR paths, DEFINITION-REFs, validation and build results. Use for
|
|
9
|
+
multi-step configuration work; for a single read or lookup call the tool directly.
|
|
10
|
+
permission:
|
|
11
|
+
# deny-all first, then allow specifics (last matching rule wins).
|
|
12
|
+
"*": deny
|
|
13
|
+
read: allow
|
|
14
|
+
grep: allow
|
|
15
|
+
glob: allow
|
|
16
|
+
list: allow
|
|
17
|
+
skill: allow
|
|
18
|
+
"ecuc_read": allow
|
|
19
|
+
"ecuc_edit": allow
|
|
20
|
+
"ecuc_validate": allow
|
|
21
|
+
"arxml_diff": allow
|
|
22
|
+
"autosar_trace": allow
|
|
23
|
+
"autosar_config_lookup": allow
|
|
24
|
+
"autosar_generate": ask
|
|
25
|
+
"autosar_build": ask
|
|
26
|
+
---
|
|
27
|
+
You are an AUTOSAR Classic integrator. Load the `autosar` skill first and follow it.
|
|
28
|
+
|
|
29
|
+
Method:
|
|
30
|
+
1. Orient with `ecuc_read` (no path). If no param defs are loaded, stop and report what is needed.
|
|
31
|
+
2. Read every container you will touch with `schema=true`, and the containers its references point to.
|
|
32
|
+
3. Before renaming or removing anything, `autosar_trace` it and list impacted symbols/callers.
|
|
33
|
+
4. Make all related changes in one `ecuc_edit` call. Never write or patch `.arxml` with other tools,
|
|
34
|
+
and never edit generated `Output/` files.
|
|
35
|
+
5. `ecuc_validate` after every edit. Fix every error you introduced; list pre-existing ones separately.
|
|
36
|
+
6. Run `autosar_generate` then `autosar_build` only when they are configured and validation is clean
|
|
37
|
+
of new errors; map generator/compiler errors back to AUTOSAR paths and fix them in config.
|
|
38
|
+
|
|
39
|
+
Report (concise):
|
|
40
|
+
- Changes: `AUTOSAR path` (DEFINITION-REF, file:line): old → new, with config class when known.
|
|
41
|
+
- Validation: counts before/after; any remaining finding with its rule.
|
|
42
|
+
- Generate/build: status and the relevant error lines.
|
|
43
|
+
- Impact: C symbols and callers from `autosar_trace`; assumptions (e.g. handle-ID convention) and
|
|
44
|
+
open questions for the human integrator.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Trace an AUTOSAR config element or C symbol to its generated code and callers (or back).
|
|
3
|
+
---
|
|
4
|
+
Load the `autosar` skill. Call `autosar_trace` with query `$ARGUMENTS` (an AUTOSAR path, a SHORT-NAME,
|
|
5
|
+
or a C symbol). If it returns several matches, list them and pick the one that fits, saying why.
|
|
6
|
+
|
|
7
|
+
Report, for the chosen match: the ARXML element (AUTOSAR path, DEFINITION-REF, module, file:line,
|
|
8
|
+
key values), each linked C symbol with file, kind, link method and score, and the callers in
|
|
9
|
+
in-house code (function, file:line). Relay any NOTE / BACKEND ERROR lines verbatim (e.g. a data source
|
|
10
|
+
that must be re-ingested). If the local checkout has the config, cross-check values with `ecuc_read`.
|
|
11
|
+
End with what would be impacted by changing this element.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Validate the project's ECUC configuration against its param defs and explain the findings.
|
|
3
|
+
---
|
|
4
|
+
Load the `autosar` skill. Run `ecuc_read` with no path to confirm which param defs and config files
|
|
5
|
+
are loaded, then `ecuc_validate`$ARGUMENTS (if arguments name a module, pass it as `module`; if they
|
|
6
|
+
name files, pass them as `files`).
|
|
7
|
+
|
|
8
|
+
Summarise: counts by severity and rule, then group the errors by module and container. For each group
|
|
9
|
+
give the AUTOSAR path, the DEFINITION-REF, file:line, what the def requires, and the concrete fix
|
|
10
|
+
(as the `ecuc_edit` op you would apply). Flag findings that look like a release/vendor param-def
|
|
11
|
+
mismatch (bursts of ECUC-UNKNOWN-DEF) or missing input files (ECUC-REF-UNRESOLVED) separately.
|
|
12
|
+
Do not apply fixes unless asked.
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anik-2/autosar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"engines": {
|
|
9
|
+
"hydron": ">=1.0.0"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
".hydron-plugin",
|
|
13
|
+
"agents",
|
|
14
|
+
"commands",
|
|
15
|
+
"skills",
|
|
16
|
+
"plugins/index.js",
|
|
17
|
+
"python/pyproject.toml",
|
|
18
|
+
"python/ecuc_engine/*.py",
|
|
19
|
+
"python/ecuc_engine/rules/*.py",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"typecheck": "tsc --noEmit -p src",
|
|
24
|
+
"test": "bun test && bun run test:py",
|
|
25
|
+
"test:py": "cd python && uv run --no-project --with autosar-data==0.17.0 --with pytest python -m pytest -q -p no:anyio",
|
|
26
|
+
"build": "bun build.ts",
|
|
27
|
+
"guard": "bun script/guard.ts",
|
|
28
|
+
"prepublishOnly": "bun run build && bun run guard"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org",
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/h2loop/hydron-autosar-pack.git"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@kilocode/plugin": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
44
|
+
"typescript": "^5.5.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/plugins/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import{isAbsolute as q_,join as h,resolve as H_}from"path";import{tool as P}from"@kilocode/plugin";import{homedir as x}from"os";import{join as X}from"path";var d=()=>process.env.HYDRON_TEST_HOME||x(),C=(q,H)=>process.env[q]||X(d(),H),c=()=>X(C("XDG_CONFIG_HOME",".config"),"hydron-cli"),l=()=>X(C("XDG_DATA_HOME",".local/share"),"hydron-cli"),n=()=>X(C("XDG_STATE_HOME",".local/state"),"hydron-cli");async function L(q){let H=Bun.file(q);if(!await H.exists())return;return H.json().catch(()=>{return})}async function O(){let q=process.env.KILO_API_URL?.trim();if(q)return q.replace(/\/+$/,"");let H=await L(X(c(),"server-config.json")),_=typeof H?.apiBaseUrl==="string"?H.apiBaseUrl:void 0;if(!_)throw Error("H2Loop backend URL unknown: set KILO_API_URL or sign in with `hydron auth login`.");return _.replace(/\/+$/,"")}function i(q){let H=q?.split(".")[1];if(!H)return"";try{let _=JSON.parse(Buffer.from(H.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8"));return typeof _.sub==="string"?_.sub:""}catch{return""}}async function g(q){let H=i(q.access);if(H)return H;if(!q.refresh)return"";return new Bun.CryptoHasher("sha256").update(q.refresh).digest("hex").slice(0,12)}async function v(){let H=(await L(X(l(),"auth.json")))?.hydron;if(!H||H.type!=="oauth"||!H.access)throw Error("Not signed in to H2Loop. Run `hydron auth login` (or sign in from the TUI), then retry.");return H}async function u(){let q=await v();if(typeof q.expires==="number"&&q.expires>0&&Date.now()>=q.expires-30000)throw Error("The H2Loop session token has expired. Send any message in Hydron (the CLI refreshes it) or re-login with `hydron auth login`, then retry.");return q.access}async function f(q){let H=await fetch(`${await O()}/api/v1/workspaces`,{headers:{Authorization:`Bearer ${await u()}`},signal:q?AbortSignal.any([q,AbortSignal.timeout(30000)]):AbortSignal.timeout(30000)});if(H.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(!H.ok)throw Error(`could not list H2Loop projects: HTTP ${H.status}`);let _=await H.json();return Array.isArray(_)?_:_?.items??[]}var k=()=>X(n(),"h2loop-project.json");async function S(q,H){let _=q===void 0||q===null?"":String(q).trim();if(/^\d+$/.test(_))return _;if(_){let $=await f(H),D=$.filter((F)=>F.name===_),B=D.length?D:$.filter((F)=>F.name?.toLowerCase()===_.toLowerCase());if(B.length===1)return String(B[0].id);if(B.length>1)throw Error(`workspace "${_}" is ambiguous: ${B.map((F)=>`${F.name} (id ${F.id})`).join(", ")}; pass the numeric id`);let W=$.filter((F)=>F.name?.toLowerCase().includes(_.toLowerCase())).slice(0,10);throw Error(`no H2Loop project named "${_}"`+(W.length?`; similar: ${W.map((F)=>`${F.name} (id ${F.id})`).join(", ")}`:`; you have ${$.length} project(s)`))}let J=await L(k());if(!J)throw Error("No H2Loop project selected (run `hydron projects select <id>`), or pass workspace_id (id or name).");let V=await v();if(J.project&&typeof J.project==="object"){let $=J.project;if(typeof J.account==="string"&&J.account!==await g(V))throw Error("The selected H2Loop project belongs to another account; select one again or pass workspace_id.");if($.id!==void 0)return String($.id)}let Q=J;if(Q.id===void 0)throw Error(`${k()} holds no project id; select a project again or pass workspace_id.`);if(!(await f(H)).some(($)=>String($.id)===String(Q.id)))throw Error(`The selected project ${Q.name??""} (id ${Q.id}) is not accessible to the signed-in account; select another or pass workspace_id.`);return String(Q.id)}async function T(q,H,_,J){let V=`${await O()}${q}?workspace_id=${encodeURIComponent(H)}`,Q=await fetch(V,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${await u()}`},body:JSON.stringify(_),signal:J?AbortSignal.any([J,AbortSignal.timeout(120000)]):AbortSignal.timeout(120000)}),Y=await Q.text();if(Q.status===401)throw Error("H2Loop rejected the session (401). Re-login with `hydron auth login`, then retry.");if(Q.status===404&&Y.includes("Not Found"))throw Error(`${q} is not available on ${await O()} (backend predates the AUTOSAR endpoints).`);if(!Q.ok)throw Error(`H2Loop ${q} failed: HTTP ${Q.status} ${Y.slice(0,800)}`);try{return JSON.parse(Y)}catch($){throw Error(`H2Loop ${q} returned non-JSON (${String($)}): ${Y.slice(0,300)}`)}}var A=/\.arxml\b/i,E="Blocked: .arxml files must not be edited by hand (it breaks DEFINITION-REF/DEST, UUIDs and the round trip). Use ecuc_edit with ops by AUTOSAR path (read paths and allowed params with ecuc_read schema=true), then ecuc_validate.";function z(q,H){if(process.env.AUTOSAR_ALLOW_ARXML_EDIT==="1"||!H)return;let _=typeof H.filePath==="string"?H.filePath:typeof H.file_path==="string"?H.file_path:"";if(["edit","write","multiedit"].includes(q)){let J=Array.isArray(H.edits)?H.edits.some((V)=>typeof V?.filePath==="string"&&A.test(V.filePath)):!1;if(A.test(_)||J)return`${E} (${q} on ${_||"an .arxml file"})`;return}if(q==="apply_patch"||q==="patch"){let J=String(H.patchText??H.patch??""),V=J.match(/^\*\*\* (?:Add|Update|Delete) File: (.+\.arxml)\s*$/im)??J.match(/^\+\+\+ (?:b\/)?(.+\.arxml)/im);return V?`${E} (${q} on ${V[1].trim()})`:void 0}if(q==="bash"){let J=String(H.command??"");if(!A.test(J))return;let V=/\b(?:sed|gsed)\b[^|;&]*\s-(?:[a-zA-Z]*i|-in-place)\b/.test(J)||/\bperl\b[^|;&]*\s-[a-zA-Z]*i/.test(J),Q=/(?:^|[^<>&0-9])>>?\s*['"]?[^\s'"|;&]*\.arxml\b/i.test(J)||/\btee\b[^|;&]*\.arxml\b/i.test(J);if(V||Q)return`${E} (bash: ${J.slice(0,120)})`}return}import{existsSync as o}from"fs";import{dirname as t,join as b}from"path";import{fileURLToPath as a}from"url";var s="autosar-data==0.17.0";function e(){let q=t(a(import.meta.url)),H=[b(q,"..","python"),b(q,"..","..","python")].find((_)=>o(b(_,"ecuc_engine","__main__.py")));if(!H)throw Error(`ECUC engine not found next to ${q} (expected ../python/ecuc_engine)`);return H}async function r(q){try{return await Bun.spawn([...q,"-c","import sys; sys.exit(0 if sys.version_info >= (3, 10) else 1)"],{stdout:"ignore",stderr:"ignore",stdin:"ignore"}).exited===0}catch{return!1}}var I={};async function __(){if(I.cmd!==void 0)return I.cmd;let q=process.env.AUTOSAR_PYTHON?.trim(),_=q?[[q]]:[["uv","run","--no-project","--quiet","--with",s,"--python",">=3.10","python"],["python3"],["python"],["py","-3"]];for(let J of _)if(await r(J))return I.cmd=J,J;return I.cmd=null,null}async function G(q,H,_){let J=await __();if(!J)return{ok:!1,error:"no Python >= 3.10 found. Install uv (https://docs.astral.sh/uv/) or Python 3.10+, or set AUTOSAR_PYTHON to an interpreter path."};let V=e(),Q=Bun.spawn([...J,"-m","ecuc_engine","--root",q,...H,"--json"],{cwd:q,env:{...process.env,PYTHONPATH:[V,process.env.PYTHONPATH].filter(Boolean).join(process.platform==="win32"?";":":")},stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:_}),[Y,$]=await Promise.all([new Response(Q.stdout).text(),new Response(Q.stderr).text()]);await Q.exited;let D=Y.trim().split(`
|
|
3
|
+
`).filter(Boolean).at(-1);if(!D)return{ok:!1,error:`engine produced no output (exit ${Q.exitCode}): ${$.trim().slice(-2000)}`};try{return JSON.parse(D)}catch(B){return{ok:!1,error:`engine output was not JSON (${String(B)}): ${D.slice(0,500)} ${$.slice(-500)}`}}}var Z=P.schema,J_=["# AUTOSAR pack active","Load the `autosar` skill for ARXML/ECUC work. Configured values: use ecuc_read (accepts a SHORT-NAME) or","autosar_config_lookup, not grep/read. Change config only with ecuc_edit (hand edits of .arxml are blocked),","then ecuc_validate. Cite the value's @path (its AUTOSAR path) and its DEFINITION-REF (@def) separately."].join(`
|
|
4
|
+
`);function p(q){if(typeof q!=="string")return q;let H=q.trim();if(!H.startsWith("[")&&!H.startsWith("{")&&!H.startsWith('"'))return q;try{return p(JSON.parse(H))}catch{return q}}var M=(q)=>Z.preprocess(p,q),U=" In answers cite, for every element, its AUTOSAR path (the value's @path, e.g. /Pkg/Com/ComConfig/MySig), its"+" DEFINITION-REF (@def, e.g. /AUTOSAR/EcucDefs/Com/ComConfig/ComSignal) and file:line \u2014 never give the"+" DEFINITION-REF when the path is asked.";function w(q,H=200){let _=q.slice(0,H).map((J)=>`${J.file}:${J.line} ${J.severity.toUpperCase()} ${J.rule} ${J.path} \u2014 ${J.message}${J.defRef?` [def ${J.defRef}]`:""}`);if(q.length>H)_.push(`\u2026 ${q.length-H} more`);return _}function R(q){let H=q.didYouMean?`
|
|
5
|
+
Did you mean: ${q.didYouMean.join(", ")}`:"",_=q.schema?`
|
|
6
|
+
Schema errors: ${JSON.stringify(q.schema)}`:"";return`ERROR: ${q.error}${H}${_}`}function N(q){let H=q.warnings??[];return H.length?H.map((_)=>`WARNING: ${_}`).join(`
|
|
7
|
+
`)+`
|
|
8
|
+
`:""}function K(q,H){if(!H)return q;return q_(H)?H:H_(q,H)}async function Q_(q){let H=Bun.file(h(q,".hydron","autosar.json"));if(!await H.exists())return{};return H.json().catch((_)=>{throw Error(`.hydron/autosar.json is not valid JSON: ${String(_)}`)})}async function V_(q,H){let _=(await Q_(q))[H];if(typeof _==="string")return{command:_};if(_&&typeof _==="object"&&typeof _.command==="string")return _;return}function m(q,H=150){let _=q.split(`
|
|
9
|
+
`);return _.length>H?[`\u2026 (${_.length-H} earlier lines omitted)`,..._.slice(-H)].join(`
|
|
10
|
+
`):q}async function Y_(q,H,_,J){let V=process.platform==="win32"?["cmd","/d","/s","/c",q]:["sh","-c",q],Q=Date.now(),Y=Bun.spawn(V,{cwd:H,stdout:"pipe",stderr:"pipe",stdin:"ignore",signal:AbortSignal.any([J,AbortSignal.timeout(_*1000)])}),[$,D]=await Promise.all([new Response(Y.stdout).text(),new Response(Y.stderr).text()]);return{code:await Y.exited,out:$,err:D,secs:Math.round((Date.now()-Q)/1000)}}var y=(q)=>{let H=q.notes??[],_=q.errors??[];return[...H.map((J)=>`NOTE: ${typeof J==="string"?J:JSON.stringify(J)}`),..._.map((J)=>`BACKEND ERROR: ${typeof J==="string"?J:JSON.stringify(J)}`)]};async function Z_(q,H){try{let _=await H(),J=typeof _==="string"?_:_.output;if(J&&J.trim())return _;return`${q}: no output (this is a bug in the AUTOSAR pack; retry or report it)`}catch(_){return`ERROR (${q}): ${_ instanceof Error?_.message:String(_)}`}}function $_(q){for(let[H,_]of Object.entries(q)){let J=_.execute;_.execute=(V,Q)=>Z_(H,()=>J(V,Q))}return q}var B_=async(q)=>{let H=(_)=>P({description:_==="generate"?"Run the project's AUTOSAR code generator (e.g. the vendor's configurator CLI) as configured in .hydron/autosar.json `generate` ({command, cwd?, timeoutSec?} or a string; `{module}` and `{args}` placeholders are substituted). Run it only after ecuc_validate reports no errors. Asks the user for permission. Returns the exit code and the output tail; follow with arxml_diff/autosar_build.":"Run the project's build command as configured in .hydron/autosar.json `build` ({command, cwd?, timeoutSec?} or a string; `{module}`/`{args}` substituted). Asks the user for permission. Returns the exit code and the output tail (compiler/linker errors). Use after autosar_generate.",args:{module:Z.string().optional().describe("module name substituted for {module} in the command, if used"),args:Z.string().optional().describe("extra arguments substituted for {args} (or appended if no placeholder)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(J,V){let Q=K(V.directory,J.project_dir),Y=await V_(Q,_);if(!Y)return`No \`${_}\` command configured. Add it to ${h(Q,".hydron","autosar.json")}, e.g.
|
|
11
|
+
{"${_}": {"command": "${_==="generate"?"<vendor-cli> -p Config/Ecu.dpa -g {module}":"make -C Build all"}", "cwd": ".", "timeoutSec": 1800}}`;let $=J.args??"",D=Y.command.replaceAll("{module}",J.module??""),B=D.includes("{args}")?D.replaceAll("{args}",$):`${D}${$?` ${$}`:""}`,W=K(Q,Y.cwd);await V.ask({permission:`autosar_${_}`,patterns:[B],always:[Y.command],metadata:{command:B,cwd:W}}),V.metadata({title:`${_}: ${B}`});let F=await Y_(B,W,Y.timeoutSec??3600,V.abort),j=F.code===0?"OK":`FAILED (exit ${F.code})`;return{title:`autosar_${_} ${j}`,output:[`$ ${B} (cwd ${W}, ${F.secs}s) -> ${j}`,F.out.trim()?`--- stdout (tail) ---
|
|
12
|
+
${m(F.out)}`:"",F.err.trim()?`--- stderr (tail) ---
|
|
13
|
+
${m(F.err,80)}`:""].filter(Boolean).join(`
|
|
14
|
+
`),metadata:{exit:F.code}}}});return{"tool.execute.before":async(_,J)=>{let V=z(_.tool,J.args);if(V)throw Error(V)},"experimental.chat.system.transform":async(_,J)=>{J.system.push(J_)},tool:$_({ecuc_read:P({description:`THE way to answer any question about configured ECUC values (use it instead of grep/read on .arxml). Read AUTOSAR ECUC configuration as compact ECUC JSON (keyed by def names: "<DefName>:<ShortName>" for containers, params by def short-name, references as "->/path"; every container carries @path = its AUTOSAR path, and @def = its DEFINITION-REF). No path: lists modules and the param-def/config files found. path may be a full AUTOSAR path, a SHORT-NAME ('DoorLatch') or a partial path ('ComConfig/DoorLatch', 'DoorLatch/ComBitPosition'); ambiguous names return candidate paths. A module/container: its values to `+"`depth` levels (merged across split files). A param path (\u2026/Container/Param) or a def path "+"(/AUTOSAR/EcucDefs/\u2026): the value and/or the definition. schema=true adds the def summary (allowed "+"params with type/range/literals/multiplicity, allowed sub-containers) \u2014 read it before ecuc_edit. "+"Never read or edit ARXML by hand."+U,args:{path:Z.string().optional().describe("AUTOSAR path, SHORT-NAME or partial path of a module/container/parameter, or a def path"),depth:Z.number().int().min(0).max(20).optional().describe("sub-container levels to expand (default 2)"),schema:Z.boolean().optional().describe("include the param-def summary for the element"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=["read"];if(_.path)V.push(_.path);if(V.push("--depth",String(_.depth??2)),_.schema)V.push("--schema");let Q=await G(K(J.directory,_.project_dir),V,J.abort);if(!Q.ok)return N(Q)+R(Q);let{ok:Y,engine:$,ms:D,warnings:B,...W}=Q;return N(Q)+JSON.stringify(W,null,1)}}),ecuc_edit:P({description:"Edit ECUC ARXML in place \u2014 the ONLY way to change configuration (edit/write/sed on .arxml are blocked). "+"Get exact paths and allowed params from ecuc_read (schema=true) first. Ops by AUTOSAR path: {op:'set', path, param, value} (value scalar, '->/ref/path', or a list for multi-valued "+"params), {op:'add', path:<parent>, container:'DefName:ShortName', values:{\u2026ECUC JSON\u2026}}, {op:'add', "+"path, param, value} (append a value), {op:'remove', path[, param[, value]]}. The engine picks the value element type, DEFINITION-REF/DEST and VALUE-REF DEST from the param defs, rejects out-of-range/non-literal values, patches only the changed bytes in the file each element lives in (UUIDs, order, comments preserved), applies all ops or none, and checks ARXML schema validity. Returns the unified diff and the validation of touched files. ALWAYS run ecuc_validate after editing and fix what it reports."+U,args:{ops:M(Z.array(M(Z.object({op:Z.enum(["set","add","remove"]),path:Z.string().describe("AUTOSAR path of the target (or parent for add-container)"),param:Z.string().optional().describe("parameter/reference def short-name"),value:Z.any().optional().describe("value: number | boolean | string | '->/path' | list"),container:Z.string().optional().describe("for add: 'DefName:ShortName' (or 'Name' if equal)"),values:M(Z.record(Z.string(),Z.any()).optional()).describe("for add: ECUC JSON of the new container")}))).min(1)).describe("list of ops (a JSON array; a JSON-encoded string is also accepted)"),dry_run:Z.boolean().optional().describe("compute and show the diff without writing"),force:Z.boolean().optional().describe("write values that fail def checks (avoid; explain why if used)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=["edit","--ops",JSON.stringify(_.ops)];if(_.dry_run)V.push("--dry-run");if(_.force)V.push("--force");let Q=await G(K(J.directory,_.project_dir),V,J.abort);if(!Q.ok){let B=Q.changes??[];return N(Q)+R(Q)+(B.length?`
|
|
15
|
+
Rejected diff:
|
|
16
|
+
${B.map((W)=>W.diff).join(`
|
|
17
|
+
`)}`:"")}let Y=Q.changes??[],$=Q.validation,D=[`${Q.written?"Written":"Dry run (nothing written)"}: ${Q.applied} op(s), ${Y.length} file(s) changed.`,...(Q.notes??[]).map((B)=>`NOTE: ${B}`),...Y.map((B)=>B.diff)];if($)D.push($.validated?`Validation of touched files: ${JSON.stringify($.summary)}`:"Validation skipped: no param defs loaded.",...w($.diagnostics,50));return{title:`ecuc_edit ${Y.map((B)=>B.file).join(", ")}`,output:N(Q)+D.join(`
|
|
18
|
+
`)}}}),ecuc_validate:P({description:"Validate ECUC configuration against the param defs (project .hydron/autosar.json paramDefs, auto-discovered ECUC-MODULE-DEF files, or $AUTOSAR_PARAMDEFS): multiplicity, mandatory params/containers, integer/float ranges, enum literals, booleans, string length/regex, reference targets and destination types, dangling refs, duplicate paths, unknown definitions, plus semantic rule packs (H2L-*: Com signals inside their I-PDU and non-overlapping incl. Motorola bit order, unique handle IDs) that run even without defs. Returns LSP-like diagnostics "+"file:line severity rule path \u2014 message [def DEFINITION-REF]. Run after EVERY ecuc_edit and before "+"autosar_generate. Without param defs it only warns."+U,args:{files:M(Z.array(Z.string()).optional()).describe("only report diagnostics in these files"),module:Z.string().optional().describe("only validate this module (e.g. Com)"),max:Z.number().int().min(1).optional().describe("cap the diagnostics listed (default 200)"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=K(J.directory,_.project_dir),Q=["validate"];for(let B of _.files??[])Q.push("--file",K(V,B));if(_.module)Q.push("--module",_.module);let Y=await G(V,Q,J.abort);if(!Y.ok)return N(Y)+R(Y);let $=Y.diagnostics,D=Y.validated?`Validated ${Y.modules.join(", ")||"(no modules)"} against ${Y.defsLoaded} module def(s): ${$.length?JSON.stringify(Y.summary):"no findings"}`:"NOT validated: no param definitions loaded.";return{title:`ecuc_validate ${JSON.stringify(Y.summary)}`,output:N(Y)+[D,...w($,_.max??200)].join(`
|
|
19
|
+
`)}}}),arxml_diff:P({description:"Semantic ARXML diff by AUTOSAR path between two files or directories (e.g. a git checkout of the old Config/ vs the current one, or Output/ before/after generation inputs). Ignores UUIDs, whitespace, comments, element order and file splits; ECUC values are compared per parameter. Returns added/removed/changed elements with the changed parameter values. Use it to review edits and PRs instead of a textual diff.",args:{a:Z.string().describe("old file or directory"),b:Z.string().describe("new file or directory"),project_dir:Z.string().optional().describe("project root (default: session directory)")},async execute(_,J){let V=K(J.directory,_.project_dir),Q=await G(V,["diff",K(V,_.a),K(V,_.b)],J.abort);if(!Q.ok)return R(Q);let{ok:Y,engine:$,ms:D,...B}=Q;return JSON.stringify(B,null,1)}}),autosar_trace:P({description:"Trace AUTOSAR configuration to code through the H2Loop code graph of the selected project: query with an AUTOSAR path, a SHORT-NAME, or a C symbol (reverse lookup). Returns the ARXML element (path, module, DEFINITION-REF, file:line, values), the C symbols it configures (generated macros/const tables via the symbolic-name rule or token/value matching, with method + score) and their callers in in-house code. Use for impact analysis ('what code changes if I change X?') and 'where does this symbol come from?'. Requires the project's code to be ingested."+U,args:{query:Z.string().describe("AUTOSAR path, SHORT-NAME, or C symbol"),data_source_id:Z.number().int().optional().describe("restrict to one code data source"),limit:Z.number().int().min(1).max(100).optional().describe("max matches (default server-side)"),workspace_id:Z.union([Z.string(),Z.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)")},async execute(_,J){let V=await S(_.workspace_id,J.abort),Q={query:_.query};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;if(_.limit!==void 0)Q.limit=_.limit;let Y=await T("/api/v1/mcp/autosar/trace",V,Q,J.abort),$=Y.matches??[],D=[`${$.length} match(es) for "${_.query}" in workspace ${V}`+(Y.data_sources_queried!==void 0?` (data sources queried: ${JSON.stringify(Y.data_sources_queried)})`:""),...y(Y),JSON.stringify($,null,1)];if(!$.length)D.push("No match: try a SHORT-NAME, a full AUTOSAR path from ecuc_read, or the C symbol.");return{title:`autosar_trace ${_.query}`,output:D.join(`
|
|
20
|
+
`)}}}),autosar_config_lookup:P({description:"Look up one ARXML element in the H2Loop code graph by AUTOSAR path: the element (module, DEFINITION-REF, file:line, values), its direct children, references out and in (who points at it), and linked C symbols. Use it to navigate config relations across the ingested repo (including files not in the local checkout); for local edits use ecuc_read/ecuc_edit."+U,args:{path:Z.string().describe("AUTOSAR path, e.g. /Pkg/Com/ComConfig/MySignal"),data_source_id:Z.number().int().optional().describe("restrict to one code data source"),workspace_id:Z.union([Z.string(),Z.number()]).optional().describe("H2Loop project id or name (default: the project selected with `hydron projects select`)")},async execute(_,J){let V=await S(_.workspace_id,J.abort),Q={path:_.path};if(_.data_source_id!==void 0)Q.data_source_id=_.data_source_id;let Y=await T("/api/v1/mcp/autosar/config-lookup",V,Q,J.abort),$=Y.matches??[];return{title:`autosar_config_lookup ${_.path}`,output:[`${$.length} match(es) for ${_.path} in workspace ${V}`,...y(Y),JSON.stringify($,null,1)].join(`
|
|
21
|
+
`)}}}),autosar_generate:H("generate"),autosar_build:H("build")})}},D_=B_;export{D_ as Autosar};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""CLI: python -m ecuc_engine [--root DIR] <info|read|edit|validate|diff> ... --json
|
|
2
|
+
|
|
3
|
+
Always prints one JSON object on stdout. {"ok": false, "error": ...} on failure (exit code 0 for
|
|
4
|
+
handled errors, so callers parse stdout rather than the status).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from . import ENGINE_VERSION
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ops(arg: str):
|
|
18
|
+
if arg.startswith("@"):
|
|
19
|
+
with open(arg[1:]) as f:
|
|
20
|
+
arg = f.read()
|
|
21
|
+
if arg == "-":
|
|
22
|
+
arg = sys.stdin.read()
|
|
23
|
+
ops = json.loads(arg)
|
|
24
|
+
for _ in range(3): # tolerate JSON that was encoded more than once ("[{...}]" as a string)
|
|
25
|
+
if isinstance(ops, str):
|
|
26
|
+
ops = json.loads(ops)
|
|
27
|
+
if isinstance(ops, dict) and isinstance(ops.get("ops"), (list, str)):
|
|
28
|
+
return _ops(json.dumps(ops["ops"]))
|
|
29
|
+
return ops if isinstance(ops, list) else [ops]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(argv=None) -> int:
|
|
33
|
+
ap = argparse.ArgumentParser(prog="ecuc_engine")
|
|
34
|
+
ap.add_argument("--root", default=os.getcwd(), help="project root (default: cwd)")
|
|
35
|
+
ap.add_argument("--defs", action="append", help="param-def file/dir/glob (overrides discovery)")
|
|
36
|
+
ap.add_argument("--config", action="append", help="ECUC value file/dir/glob (overrides discovery)")
|
|
37
|
+
ap.add_argument("--json", action="store_true", help="JSON output (always on; accepted for clarity)")
|
|
38
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
39
|
+
sub.add_parser("info")
|
|
40
|
+
r = sub.add_parser("read")
|
|
41
|
+
r.add_argument("path", nargs="?")
|
|
42
|
+
r.add_argument("--depth", type=int, default=2)
|
|
43
|
+
r.add_argument("--schema", action="store_true")
|
|
44
|
+
e = sub.add_parser("edit")
|
|
45
|
+
e.add_argument("--ops", required=True, help="JSON op or list, @file, or - for stdin")
|
|
46
|
+
e.add_argument("--dry-run", action="store_true")
|
|
47
|
+
e.add_argument("--force", action="store_true")
|
|
48
|
+
e.add_argument("--no-validate", action="store_true")
|
|
49
|
+
v = sub.add_parser("validate")
|
|
50
|
+
v.add_argument("--file", action="append")
|
|
51
|
+
v.add_argument("--module")
|
|
52
|
+
v.add_argument("--max", type=int, default=0, help="cap the number of diagnostics returned (0 = all)")
|
|
53
|
+
d = sub.add_parser("diff")
|
|
54
|
+
d.add_argument("a")
|
|
55
|
+
d.add_argument("b")
|
|
56
|
+
for p in (r, e, v, d): # allow --json after the subcommand too
|
|
57
|
+
p.add_argument("--json", action="store_true", help=argparse.SUPPRESS)
|
|
58
|
+
a = ap.parse_args(argv)
|
|
59
|
+
t0 = time.time()
|
|
60
|
+
try:
|
|
61
|
+
res = run(a)
|
|
62
|
+
except Exception as ex: # noqa: BLE001
|
|
63
|
+
res = {"ok": False, "error": f"{type(ex).__name__}: {ex}"}
|
|
64
|
+
res.setdefault("ok", "error" not in res)
|
|
65
|
+
res["engine"] = ENGINE_VERSION
|
|
66
|
+
res["ms"] = int((time.time() - t0) * 1000)
|
|
67
|
+
sys.stdout.write(json.dumps(res, ensure_ascii=False, indent=None if a.json else 1, default=str) + "\n")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run(a) -> dict:
|
|
72
|
+
if a.cmd == "edit":
|
|
73
|
+
try:
|
|
74
|
+
a.ops = _ops(a.ops)
|
|
75
|
+
except (ValueError, OSError) as ex:
|
|
76
|
+
return {"error": f"ops is not valid JSON: {ex}"}
|
|
77
|
+
if a.cmd == "diff":
|
|
78
|
+
from .diff import diff
|
|
79
|
+
for x in (a.a, a.b):
|
|
80
|
+
if not os.path.exists(x):
|
|
81
|
+
return {"error": f"{x} does not exist"}
|
|
82
|
+
root = os.path.abspath(a.root)
|
|
83
|
+
rel = lambda f: os.path.relpath(f, root) if not os.path.relpath(f, root).startswith("..") else f # noqa: E731
|
|
84
|
+
return diff(a.a, a.b, rel)
|
|
85
|
+
from .model import Project
|
|
86
|
+
project = Project(a.root, config_files=a.config, def_files=a.defs, load_input=a.cmd in ("validate", "edit"))
|
|
87
|
+
if a.cmd == "info":
|
|
88
|
+
s = project.sources
|
|
89
|
+
return {"root": s.root, "origin": s.origin, "config": [project.rel(f) for f in s.config],
|
|
90
|
+
"paramDefs": [project.rel(f) for f in s.defs], "input": [project.rel(f) for f in s.input][:200],
|
|
91
|
+
"defModules": len(project.defs.modules), "modules": sorted({m.name for m in project.modules}),
|
|
92
|
+
"warnings": project.warnings}
|
|
93
|
+
if a.cmd == "read":
|
|
94
|
+
from .ecucjson import read
|
|
95
|
+
return read(project, a.path, depth=a.depth, schema=a.schema)
|
|
96
|
+
if a.cmd == "edit":
|
|
97
|
+
from .edit import edit
|
|
98
|
+
res = edit(project, a.ops, dry_run=a.dry_run, force=a.force, run_validate=not a.no_validate)
|
|
99
|
+
if project.warnings:
|
|
100
|
+
res["warnings"] = project.warnings
|
|
101
|
+
return res
|
|
102
|
+
if a.cmd == "validate":
|
|
103
|
+
from .validate import validate
|
|
104
|
+
res = validate(project, files=a.file, module=a.module)
|
|
105
|
+
if a.max and len(res["diagnostics"]) > a.max:
|
|
106
|
+
res["truncated"] = len(res["diagnostics"]) - a.max
|
|
107
|
+
res["diagnostics"] = res["diagnostics"][: a.max]
|
|
108
|
+
if project.warnings:
|
|
109
|
+
res["warnings"] = project.warnings
|
|
110
|
+
return res
|
|
111
|
+
return {"error": f"unknown command {a.cmd}"}
|