Repository layout and workspaces¶
Task P0-T07 lays out the monorepo of
04-engineering-standards.md
(ADR-0008) and gives every layer a buildable, tested
package in its own language (ADR-0009). ARCHITECTURE.md
at the repository root maps the directories to the layers; this page explains how each workspace
is built, tested and versioned.
fieldproof/
├── sim/ colcon workspace: fieldproof_worlds, fieldproof_gz_plugins (C++17)
├── bridge/ colcon workspace: fieldproof_bridge (ament_python)
├── robots/ colcon workspace: fieldproof_robots (descriptions, manifests)
├── engine/ uv project: Python package `fieldproof`, the CLI
├── cockpit/ pnpm workspace: apps/cockpit (Vite + React), packages/
├── docker/ images and the compose stack
├── examples/ scenarios/, vehicles/, plugins/
├── assets/ vendored assets with LICENSES/, Fuel lock file
├── tools/ host, ci, ros, docs helpers
├── tests/ cross-layer suites (tooling, ci, docs, layout, host, images)
└── docs/ this site
One command per layer¶
| Command | Toolchain | Runs | Where |
|---|---|---|---|
make test-engine |
uv | uv sync --locked, pytest in engine/ |
host |
make test-cockpit |
pnpm, Node 22 | pnpm install --frozen-lockfile, tsc --noEmit, Vitest |
host |
make test-ros |
colcon, ROS 2 Jazzy, Gazebo Harmonic | colcon build and colcon test in sim/, robots/, bridge/ |
dev container |
make tidy |
clang-tidy 18 | run-clang-tidy over the C++ packages with the compile commands of the last build |
dev container |
make test-worlds |
pytest 7.4, Gazebo Harmonic | tests/worlds: the depot world with the UGV (worlds.md) |
dev container |
make test-assets |
python3 | tests/assets: the asset convention, assets/fuel.lock and the fetch script |
host |
make test |
all of the above plus tooling, CI, docs, layout, host and image tests |
The host runs only what host.md installs (Docker, Node with pnpm, uv, pre-commit).
Everything that needs ROS or Gazebo runs in the dev container of
containers.md: make test-ros starts it (make images or
FIELDPROOF_IMAGE_TAG=main must have provided the image first) and leaves it running for the
next call; make down stops it. ROS_WORKSPACES="sim" limits a run to one workspace.
The engine (engine/)¶
A uv project with a src/ layout: engine/pyproject.toml declares the package fieldproof, its
exact dependencies (Pydantic, Typer, Rich) and the fieldproof console script;
engine/uv.lock pins the resolution. The version is dynamic: hatchling reads it from the
repository's version.txt, and uv's cache-keys rebuild the editable install when that file
changes. The engine also depends on fieldproof-bridge, the bridge ROS package of this
repository, installed editable from bridge/fieldproof_bridge ([tool.uv.sources]): that package
carries a pyproject.toml with a dynamic version (read from fieldproof_bridge/__init__.py), so
fieldproof bridge gen runs the generator the bridge container runs and the lock file survives a
release. uv sync --locked --project engine creates engine/.venv,
and uv run --project engine fieldproof --version runs the CLI (python -m fieldproof works
too). Tests live in engine/tests/ and run with the pytest of the dev dependency group.
Lint and type settings come from the root pyproject.toml: ruff and mypy strict run through the
pre-commit hooks on every Python file in the repository. The mypy hook lists the engine's
dependencies in .pre-commit-config.yaml at the versions of uv.lock, so the hook sees the same
packages the code runs with; tests/layout/test_layout.sh fails when they drift. Add a
dependency in three places: engine/pyproject.toml, uv lock --project engine, and the hook.
The cockpit (cockpit/)¶
A pnpm workspace (pnpm-workspace.yaml: apps/*, packages/*) whose root package.json names
the pnpm release in packageManager (the same release tools/host-setup.sh enables through
corepack; CI reads it with pnpm/action-setup). apps/cockpit is the Vite + React + TypeScript
strict application: pnpm dev serves it on 127.0.0.1:5173, pnpm test runs Vitest,
pnpm typecheck runs tsc --noEmit, pnpm build produces dist/. The version in the app's
package.json is injected at build time as __COCKPIT_VERSION__. Shared packages go under
packages/ as @fieldproof/<name> from P3-T02 on.
Dependencies are exact versions (.npmrc sets save-exact), and pnpm-workspace.yaml sets a
minimumReleaseAge of seven days: pnpm refuses a release younger than that, which keeps a
package hijacked on the registry from landing in the lock file before it is noticed. To add a
dependency: pnpm --filter @fieldproof/cockpit add <name>@<version>, then commit
pnpm-lock.yaml. The root ESLint and Prettier configuration applies through the pre-commit
hooks, with TypeScript at the same version as the app (checked by the layout test).
The ROS workspaces (sim/, robots/, bridge/)¶
Each directory is a colcon workspace whose packages sit at its top level, built with
tools/ros/colcon.sh inside the dev container:
| Package | Type | Tests |
|---|---|---|
sim/fieldproof_worlds |
ament_cmake |
gz sdf --check on every world in worlds/ and every model in models/, pytest on the worlds against assets/fuel.lock, the installed models and the generated map, ament_xmllint on package.xml (worlds.md) |
sim/fieldproof_gz_plugins |
ament_cmake, C++17 |
GoogleTest (ament_add_gtest), ament_xmllint; fieldproof::HelloSystem is the first system plugin |
robots/fieldproof_robots |
ament_cmake |
check_urdf and gz sdf --check on the generated descriptions, pytest through ament_cmake_pytest on the installed layout and the manifest, a launch_testing spawn-and-drive test, ament_xmllint (robots.md) |
bridge/fieldproof_bridge |
ament_python |
pytest in test/: generator and adapter unit tests, and the launch_testing contract test that spawns the UGV and checks the bridged interface (bridge.md) |
tools/ros/colcon.sh build|test|tidy [workspace ...] builds with
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON into <workspace>/build, install and log (ignored by
git), runs colcon test with --return-code-on-test-failure and prints
colcon test-result --verbose. tidy runs run-clang-tidy with the repository .clang-tidy
(warnings are errors) over each package's own translation units, with headers filtered to the
fieldproof_* packages so Gazebo's headers are not diagnosed. The workspaces default to
sim robots bridge, and that order is also the overlay chain: before a workspace is built or
tested, the install space of every workspace earlier in the list is sourced, so robots sees
fieldproof_worlds (its launch test spawns the UGV into the empty world) and bridge sees both
(its contract test spawns the UGV and bridges it). The Python packages
keep a pytest.ini of their own because the distribution pytest in the container (7.4) does not
accept the root pyproject.toml settings written for the engine's pytest 9; for an ament_cmake
package it sits at the workspace root (robots/pytest.ini), above build/, because
ament_add_pytest_test probes pytest from the build directory at configure time and silently
registers no test when that probe rejects the root settings.
The plugin, world and robot packages install environment hooks: sourcing sim/install/setup.bash
in the container puts the plugin library on GZ_SIM_SYSTEM_PLUGIN_PATH and the worlds and
models on GZ_SIM_RESOURCE_PATH, so gz sim -s depot.sdf finds them; robots/install adds
the vehicle models, so model://ugv_ref resolves. The vehicle descriptions are xacro, rendered
to URDF and SDF at build time by scripts/generate_model.py (xacro, then gz sdf -p), which
needs the xacro package of the sim image. The worlds package fetches the Gazebo Fuel models
pinned in assets/fuel.lock at build time (tools/assets/fetch_fuel.py; network on the first
build, the cached archives afterwards) and installs them with its own models/ under
share/fieldproof_worlds/models (worlds.md).
tools/ros/colcon.sh run <command> runs a command with every built workspace's install space
sourced; the cross-layer tests under tests/ use it: make test-worlds builds sim and
robots, then runs python3 -m pytest tests/worlds in the dev container (the distribution
pytest 7.4, which the root pyproject.toml accepts).
One version everywhere¶
version.txt is the repository version, maintained by release-please
(ci.md). The engine reads it directly (dynamic version), so engine/uv.lock,
which records no version for a dynamic project, stays valid across releases. The extra-files
list in release-please-config.json makes the release pull request update the same number in
both cockpit package.json files, the four package.xml files and the bridge module
fieldproof_bridge/__init__.py (through an x-release-please-version marker; the bridge's
pyproject.toml reads it from there); pnpm-lock.yaml records no version for workspace packages
either. fieldproof --version,
the cockpit banner and the Gazebo plugin library all report that number, and
tests/layout/test_layout.sh fails when any file disagrees with version.txt.
Adding a package¶
- ROS package: create it under the workspace directory with
package.xml, aBUILD_TESTINGblock with at leastament_xmllint, and tests intest/; add itspackage.xmlto the release-pleaseextra-files.make test-rospicks it up. - Python package outside ROS: only the engine so far; new modules go under
engine/src/fieldproof/. - Cockpit package:
cockpit/packages/<name>/package.jsonnamed@fieldproof/<name>withtestandtypecheckscripts; the workspace's recursive scripts run them.
Continuous integration¶
ci.yml (ci.md) runs make test-engine (job Engine tests, uv cache keyed on
engine/uv.lock), make test-cockpit plus pnpm build (job Cockpit tests, pnpm store cache
keyed on pnpm-lock.yaml) and make test-layout (in the tooling job) on every pull request. The
ROS workspaces need the dev image, so make test-ros, make tidy and make test-worlds (with
relaxed limits for software rendering) run at the end of the Container images and ROS
workspaces job, which the path filter enables when sim/, bridge/, robots/, tools/ros/,
.clang-tidy, assets/, tools/assets/ or tests/worlds/ change (as well as for image
changes); the job caches the Fuel archives keyed on assets/fuel.lock. make test-assets runs
in the tooling job.
Troubleshooting¶
colcon build fails with PermissionError: [Errno 13] Permission denied: 'log'. The dev
container runs as uid 1000 and writes build/, install/ and log/ into the bind-mounted
workspace, so the checkout must be writable by that uid. Developer accounts prepared by
host.md are uid 1000; CI runs sudo chown -R 1000:1000 sim bridge robots before
make test-ros because the runner user is uid 1001.
make test-ros says the dev image is missing. It never builds: run make images, or pull
the published image with FIELDPROOF_IMAGE_TAG=main.
Tests¶
tests/layout/test_layout.sh (make test-layout) checks the layout against the standards: the
layer directories, READMEs and ARCHITECTURE.md exist; every package has a test; every version
file equals version.txt and is listed in the release-please extra-files; engine/uv.lock and
cockpit/pnpm-lock.yaml match their manifests; the mypy hook's packages, the eslint hook's
TypeScript, the packageManager field, .nvmrc and the Python version agree with their
counterparts; make test runs the layer suites; build output is ignored; and
fieldproof --version prints the repository version.