diff --git a/README.md b/README.md index 7a08b8a..9333459 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,11 @@ -# EdgeMind - Verifiable Onboard IR (initial skeleton) +# EdgeMind EnergiBridge IR (Schemas) -This repository contains an initial Python skeleton for the EdgeMind IR (Intermediate Representation) and a small set of Pydantic models representing the canonical IR types used by the EnergiBridge and EdgeMind runtime. +This package contains Pydantic models and example JSON/YAML specs for the EnergiBridge canonical interop IR described in the EdgeMind idea: LocalProblem, SharedSignals, PlanDelta, SafetyContract, AuditLog, and AdapterContract. -What is included in this change: -- pyproject.toml with packaging metadata and dependency on pydantic -- specs/ir.py: Pydantic models for LocalProblem, SharedSignals, PlanDelta, SafetyContract, AuditLog, AdapterContract -- tests/test_ir_models.py: unit tests that validate model instantiation and JSON schema generation -- test.sh: convenience script to run tests and build +What this commit provides: +- Python package `edgemind` with Pydantic models. +- Example YAML specs in `/specs`. +- Unit tests that validate model construction and serialization. +- `test.sh` which runs `pytest` and `python3 -m build` to verify packaging. -How to run tests and build (also invoked by the CI harness): - - bash test.sh - -Notes: -- This is a focused, well-scoped first chunk of the overall EdgeMind idea: it provides canonical, typed IR models and tests that the models behave and can produce JSON Schema. Next steps would be to export JSON Schema files, implement adapter skeletons, and add delta-sync/planner pieces. +Next steps for contributors: implement adapters, delta-sync engine, and runtime enforcement components. diff --git a/edgemind/__init__.py b/edgemind/__init__.py new file mode 100644 index 0000000..0c26a64 --- /dev/null +++ b/edgemind/__init__.py @@ -0,0 +1,4 @@ +"""EdgeMind EnergiBridge IR package""" +__all__ = [ + "schema", +] diff --git a/edgemind/schema.py b/edgemind/schema.py new file mode 100644 index 0000000..46af3c8 --- /dev/null +++ b/edgemind/schema.py @@ -0,0 +1,67 @@ +from typing import Dict, List, Optional, Any +from pydantic import BaseModel, Field, validator +from datetime import datetime + + +class SafetyContract(BaseModel): + id: str + description: Optional[str] + # Predicate expressed as a simple string (to evaluate in a sandboxed evaluator later) + predicate: str + fail_safe: Optional[str] = Field( + default="halt", description="Action to take on violation: halt|safe-mode|notify" + ) + + +class LocalProblem(BaseModel): + id: str + goal_type: str + state_vars: Dict[str, Any] + hard_constraints: Optional[List[str]] = None + soft_costs: Optional[Dict[str, float]] = None + energy_budget: Optional[float] = None + time_window: Optional[Dict[str, str]] = None + actuator_interface: Optional[str] = None + + +class SharedSignal(BaseModel): + name: str + # renamed from `schema` to avoid colliding with BaseModel.schema() + signal_schema: Dict[str, Any] + privacy_budget: Optional[float] = None + + +class PlanDelta(BaseModel): + id: str + base_plan_id: Optional[str] + patches: List[Dict[str, Any]] + timestamp: datetime + safety_tags: Optional[List[str]] = None + + +class AuditLog(BaseModel): + entry_id: str + timestamp: datetime + actor: str + action: str + metadata: Optional[Dict[str, Any]] = None + + +class AdapterContract(BaseModel): + adapter_id: str + supported_domains: List[str] + contract_version: str + metadata: Optional[Dict[str, Any]] = None + + +class EnergiBridgeIR(BaseModel): + local_problem: Optional[LocalProblem] = None + shared_signals: Optional[List[SharedSignal]] = None + plan_delta: Optional[PlanDelta] = None + safety_contracts: Optional[List[SafetyContract]] = None + audit_logs: Optional[List[AuditLog]] = None + adapter: Optional[AdapterContract] = None + + @validator("shared_signals", pre=True, always=True) + def ensure_list(cls, v): + return v or [] diff --git a/pyproject.toml b/pyproject.toml index 03783e7..8fe2f47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,3 @@ [build-system] -requires = ["setuptools>=40.8.0", "wheel"] +requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" - -[project] -name = "edgemind-verifiable-onboard" -version = "0.1.0" -description = "EdgeMind: Verifiable Onboard AI Planning Runtime (IR schema + small spec)" -readme = "README.md" -requires-python = ">=3.8" -dependencies = [ - "pydantic>=1.10", -] - -[tool.setuptools.packages.find] -where = ["."] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..6e2b35d --- /dev/null +++ b/setup.cfg @@ -0,0 +1,17 @@ +[metadata] +name = edgemind +version = 0.1.0 +description = EdgeMind EnergiBridge IR schemas and tools +long_description = file: README.md +long_description_content_type = text/markdown +author = EdgeMind Contributors +license = MIT + +[options] +packages = find: +install_requires = + pydantic>=1.10 + PyYAML>=6.0 + +[options.packages.find] +where = . diff --git a/specs/AdapterContract.example.yaml b/specs/AdapterContract.example.yaml new file mode 100644 index 0000000..8b119cc --- /dev/null +++ b/specs/AdapterContract.example.yaml @@ -0,0 +1,7 @@ +adapter_id: adapter_ros2_sensor_gateway +supported_domains: + - mobility + - perception +contract_version: "0.1.0" +metadata: + tls: required diff --git a/specs/LocalProblem.example.yaml b/specs/LocalProblem.example.yaml new file mode 100644 index 0000000..1e9b62f --- /dev/null +++ b/specs/LocalProblem.example.yaml @@ -0,0 +1,15 @@ +id: local_problem_001 +goal_type: energy_aware_path +state_vars: + start: {x: 0, y: 0} + goal: {x: 10, y: 2} +hard_constraints: + - "avoid_obstacles" +soft_costs: + time: 1.0 + energy: 5.0 +energy_budget: 100.0 +time_window: + start: "2026-01-01T00:00:00Z" + end: "2026-01-01T01:00:00Z" +actuator_interface: ros2_motion_controller diff --git a/specs/SafetyContract.example.yaml b/specs/SafetyContract.example.yaml new file mode 100644 index 0000000..3bc8182 --- /dev/null +++ b/specs/SafetyContract.example.yaml @@ -0,0 +1,4 @@ +id: sc_001 +description: "Battery must not fall below 10% during maneuver" +predicate: "battery_level >= 0.10" +fail_safe: safe-mode diff --git a/test.sh b/test.sh index ec33b56..5d3434e 100644 --- a/test.sh +++ b/test.sh @@ -1,10 +1,8 @@ #!/usr/bin/env bash set -euo pipefail - -echo "Running pytest..." +echo "Running unit tests..." +export PYTHONPATH="$(pwd)" pytest -q - -echo "Building package to verify pyproject metadata..." +echo "Running build verification..." python3 -m build - echo "All checks passed." diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 0000000..20691ff --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,32 @@ +import yaml +from edgemind import schema +from datetime import datetime + + +def test_local_problem_from_yaml(): + with open("specs/LocalProblem.example.yaml", "r") as f: + data = yaml.safe_load(f) + lp = schema.LocalProblem(**data) + assert lp.id == "local_problem_001" + assert lp.goal_type == "energy_aware_path" + + +def test_safety_contract_from_yaml(): + with open("specs/SafetyContract.example.yaml", "r") as f: + data = yaml.safe_load(f) + sc = schema.SafetyContract(**data) + assert sc.predicate == "battery_level >= 0.10" + + +def test_plan_delta_and_auditlog(): + now = datetime.utcnow() + pd = schema.PlanDelta( + id="pd1", + base_plan_id=None, + patches=[{"op": "replace", "path": "/plan/steps/0", "value": {}}], + timestamp=now, + ) + al = schema.AuditLog(entry_id="a1", timestamp=now, actor="tester", action="create", metadata={}) + ir = schema.EnergiBridgeIR(local_problem=None, plan_delta=pd, audit_logs=[al]) + assert ir.plan_delta.id == "pd1" + assert ir.audit_logs[0].actor == "tester"