From b6e4dba963413e1b74c86926901c2d51496fed79 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 20 Aug 2026 16:46:58 -0400 Subject: [PATCH 1/6] P1(rfc-379): module.yaml manifest schema, validator, fixture module, contract tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common/module_schema/module.schema.json: thin-manifest schema per RFC #379 §2 (deps/identity/tests only — wiring deliberately absent), draft-07 subset with x-airstack-* annotations for semver-range/path-safety/existence checks - tools/validate_module.py: stdlib+PyYAML generic schema walker (no jsonschema dep); JSON verdict on stdout, exit 0/1; dir mode adds cross-file checks - tests/fixtures/modules/hello_module: minimal valid module (manifest + buildable ament_python package) for overlay/build phases - tests/meta/test_module_manifest_contract.py: 24 unit-mark contract tests - .agents/skills/create-module: by-hand module authoring skill (scaffolding CLI lands in a later phase) RFC: https://github.com/castacks/AirStack/discussions/379 Co-Authored-By: Claude Fable 5 --- .agents/skills/create-module/SKILL.md | 134 ++++++++ common/module_schema/README.md | 96 ++++++ common/module_schema/module.schema.json | 198 ++++++++++++ .../hello_module/hello_module/__init__.py | 3 + .../hello_module/hello_module/hello_node.py | 22 ++ .../hello_module/hello_module/package.xml | 17 ++ .../hello_module/resource/hello_module | 0 .../hello_module/hello_module/setup.cfg | 4 + .../hello_module/hello_module/setup.py | 25 ++ .../hello_module/test/test_import.py | 32 ++ .../fixtures/modules/hello_module/module.yaml | 13 + tests/meta/test_module_manifest_contract.py | 241 +++++++++++++++ tools/validate_module.py | 289 ++++++++++++++++++ 13 files changed, 1074 insertions(+) create mode 100644 .agents/skills/create-module/SKILL.md create mode 100644 common/module_schema/README.md create mode 100644 common/module_schema/module.schema.json create mode 100644 tests/fixtures/modules/hello_module/hello_module/hello_module/__init__.py create mode 100644 tests/fixtures/modules/hello_module/hello_module/hello_module/hello_node.py create mode 100644 tests/fixtures/modules/hello_module/hello_module/package.xml create mode 100644 tests/fixtures/modules/hello_module/hello_module/resource/hello_module create mode 100644 tests/fixtures/modules/hello_module/hello_module/setup.cfg create mode 100644 tests/fixtures/modules/hello_module/hello_module/setup.py create mode 100644 tests/fixtures/modules/hello_module/hello_module/test/test_import.py create mode 100644 tests/fixtures/modules/hello_module/module.yaml create mode 100644 tests/meta/test_module_manifest_contract.py create mode 100755 tools/validate_module.py diff --git a/.agents/skills/create-module/SKILL.md b/.agents/skills/create-module/SKILL.md new file mode 100644 index 000000000..0874fdd34 --- /dev/null +++ b/.agents/skills/create-module/SKILL.md @@ -0,0 +1,134 @@ +--- +name: create-module +description: Create a new AirStack module repository by hand — author the thin module.yaml manifest, lay out colcon packages with canonical-default launch args (never remaps), add test_stack/ and CI, and validate with tools/validate_module.py. Use when packaging a capability (planner, estimator, sim extension, vehicle data) as a standalone module repo per RFC #379. +license: Apache-2.0 +metadata: + author: AirLab CMU + repository: AirStack +--- + +# Skill: Create an AirStack Module Repo + +## When to Use + +Creating a **new AirStack module repo** — a thin, standalone repository that +distributes an optional capability (a planner, state estimator, world model, Isaac +Sim extension, vehicle definition, …) without forking AirStack. Defined by +[RFC #379](https://github.com/castacks/AirStack/discussions/379) §2 (manifest) and +[RFC #385](https://github.com/castacks/AirStack/discussions/385) §2 (repo anatomy). + +Not this skill: adding a trunk-resident package (use `add-ros2-package`), or wiring +an existing module into a bringup (use `integrate-module-into-layer`). + +> **Status:** `airstack module create` scaffolding arrives in a later phase. Until +> then this skill documents the **by-hand procedure**; only the manifest schema and +> validator (`tools/validate_module.py`) exist today. + +## Module Repo Anatomy (RFC #385 §2) + +A module is a *thin* repo: ordinary colcon packages, a small manifest carrying deps +and identity only, and a `test_stack/` that is both its CI target and its living +install documentation. + +``` +my-module/ +├── module.yaml # the thin manifest — deps, identity, tests; NO wiring +├── my_module/ # ordinary colcon package(s) +│ ├── package.xml # rosdep keys live here (dep tier 1) +│ ├── src/ config/ test/ # co-located unit tests, standard colcon convention +│ └── launch/ +│ └── my_module.launch.xml # topic args DEFAULT to canonical names; never remaps +├── test_stack/ # reference-stack copy with this module wired in +│ ├── modules.repos # PINNED to tags/commits — never branches +│ ├── launch/stack.launch.xml # the ONE place this module is wired +│ ├── docker-compose.yaml +│ ├── wiring.md # generated in CI from the running graph +│ └── README.md +├── Dockerfile.module # optional dep tier 2 — against ARG BASE_IMAGE only +├── .github/workflows/ +│ └── ci.yml # ~10 lines: uses castacks/AirStack/.github/workflows/ +│ # module-system-tests.yml@vX.Y.Z (later phase) +└── README.md +``` + +## The Manifest (`module.yaml`) + +Schema + full field reference: [`common/module_schema/`](../../../common/module_schema/README.md). +Summary: + +- **Required:** `name` (snake_case), `description`, `maintainer` (email), + `license`, `type` (`isaac_extension` | `ros_package` | `data` | `platform`), + `airstack_compat` (semver range vs trunk `.env` `VERSION`, e.g. + `">=0.19.0 <0.21.0"` — never a branch name), `targets` + (non-empty: `robot` | `gcs` | `isaac-sim` | `ms-airsim`). +- **Optional:** `deps` `{apt, pip}`, `dockerfile` (path ending + `Dockerfile.module`), `overlay_image`, `compose`, `assets` + (`[{url (https), sha256, dest}]` — no Git LFS), `docs`, `foxglove`, `hooks` + (`host_setup`: idempotent, no sudo, writes only inside the module checkout), + `tests` (`packages` + `marks` from the known mark set). +- **Deliberately absent: wiring.** No slot, role, or topic metadata — unknown keys + are rejected. A module's interface is its launch file's declared args + (`ros2 launch --show-args`). + +Minimal working example: [`tests/fixtures/modules/hello_module/module.yaml`](../../../tests/fixtures/modules/hello_module/module.yaml). + +## The Canonical-Defaults Launch Rule + +The one interface convention that does the plug-and-play work (RFC #379 §2, §4): + +- Expose **every topic endpoint as a launch arg**, and **default it to the + canonical name** from the interface conventions spec (today: + `docs/robot/autonomy/integration_checklist.md`). +- **NEVER put `` in a module launch file**, and never hardcode a topic in + node code. All cross-module remaps live in the *stack's* entry launch file — + the single-locus wiring rule. + +```xml + + + + + + + + +``` + +(The `` *inside* the node block binds the node's internal name to the +declared arg — that is the mechanism, not a cross-module rewire. What is forbidden +is remapping other modules' topics or overriding canonical names in module launch +files: in a conventional stack, including the module must require **zero** remaps, +so only deviations appear in stack files.) + +## Steps (by hand, until `airstack module create` lands) + +1. Create the repo with the anatomy above; write `module.yaml` first. +2. Write the package(s) following `add-ros2-package` conventions (package.xml + format 3, co-located `test/`). +3. Author the module launch file under the canonical-defaults rule. +4. Validate: + + ```bash + python3 tools/validate_module.py path/to/my-module + ``` + + Exit 0 and `{"valid": true, "errors": []}` on stdout is the gate. Dir mode also + checks that declared `dockerfile`/`compose`/`hooks`/`docs` paths exist and warns + when `tests.packages` entries match no directory. +5. Copy a trunk reference stack into `test_stack/` and wire the module in its + `stack.launch.xml` (reference stacks land in a later phase; until then model it + on the bringup you tested against). +6. Add `ci.yml` calling the reusable `module-system-tests.yml` workflow (later + phase) with pinned `airstack_ref` and the marks for your module category + (RFC #379 §5: global planner → `waypoint_flight`,`autonomy`; state estimator → + `liveliness`,`sensors`,`takeoff_hover_land`; world model/perception → + `liveliness`,`sensors`; sim extension → `liveliness`). + +## References + +- Manifest schema + validator: [`common/module_schema/`](../../../common/module_schema/README.md) +- Fixture module: [`tests/fixtures/modules/hello_module/`](../../../tests/fixtures/modules/hello_module/) +- Contract tests: [`tests/meta/test_module_manifest_contract.py`](../../../tests/meta/test_module_manifest_contract.py) +- RFC #379 (design), RFC #385 (directory atlas) +- Related skills: [add-ros2-package](../add-ros2-package), [write-launch-file](../write-launch-file), [run-system-tests](../run-system-tests) diff --git a/common/module_schema/README.md b/common/module_schema/README.md new file mode 100644 index 000000000..533829f8b --- /dev/null +++ b/common/module_schema/README.md @@ -0,0 +1,96 @@ +# AirStack Module Manifest Schema + +The schema for `module.yaml` — the **thin module manifest** from +[RFC #379 §2](https://github.com/castacks/AirStack/discussions/379): dependencies, +identity, and test metadata only. **Wiring deliberately does not live here.** A +module's interface is its launch file's declared args (with canonical-name defaults) +plus the interface conventions spec; its wiring lives in whatever stack includes it. + +- Schema: [`module.schema.json`](module.schema.json) +- Validator: [`tools/validate_module.py`](../../tools/validate_module.py) +- Authoring workflow: [`.agents/skills/create-module`](../../.agents/skills/create-module/SKILL.md) + +## Validate a module + +```bash +# Module directory (schema + cross-file checks): +python3 tools/validate_module.py path/to/module_repo + +# Bare manifest (schema only): +python3 tools/validate_module.py path/to/module.yaml +``` + +Human-readable errors go to stderr; a JSON verdict goes to stdout for scripts: + +```json +{"valid": false, "errors": [{"path": "maintainer", "message": "required property is missing"}]} +``` + +Exit code 0 when valid, 1 otherwise. Contract tests: +[`tests/meta/test_module_manifest_contract.py`](../../tests/meta/test_module_manifest_contract.py). + +## Fields + +### Required + +| Field | Shape | Notes | +|---|---|---| +| `name` | `^[a-z][a-z0-9_]*$` | module identity, snake_case | +| `description` | string, ≥ 8 chars | one-liner for the registry catalog | +| `maintainer` | email | required by governance (RFC #379 §8) | +| `license` | non-empty string | checked at registration | +| `type` | `isaac_extension` \| `ros_package` \| `data` \| `platform` | `platform` is future (RFC #380 §6) | +| `airstack_compat` | semver range | vs trunk `.env` `VERSION`, e.g. `">=0.19.0 <0.21.0"`, `">=0.19.0-alpha.18 <0.20.0"`. Branch names (`main`) and partial versions (`0.19`) are invalid. | +| `targets` | non-empty array of `robot` \| `gcs` \| `isaac-sim` \| `ms-airsim` | host containers touched | + +### Optional (with defaults) + +| Field | Default | Notes | +|---|---|---| +| `deps` | `{apt: [], pip: []}` | dependency tier 1 (RFC #379 §6); rosdep keys stay in `package.xml` | +| `dockerfile` | `null` | tier 2: repo-relative path ending `Dockerfile.module`, written against `ARG BASE_IMAGE` | +| `overlay_image` | `null` | tier 3: prebuilt overlay image reference | +| `compose` | `null` | repo-relative compose fragment (mounts, env) | +| `assets` | `[]` | `[{url, sha256, dest}]`: `url` must be `https://`, `sha256` is 64 lowercase hex chars, `dest` is a relative path (no `..`) | +| `docs` | `{readme: README.md}` | or `{dir: , nav: }` | +| `foxglove` | `null` | layout/panel fragment so operators can see the module | +| `hooks` | — | `{host_setup: