Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ and this project adheres to semantic versioning once public releases begin.

### Added

- Contract-version discovery command (Ticket 105). A new read-only `bvlos-sim
schema-versions` command (alias `contracts`) prints the resolved `tool_version`
plus every supported output-envelope and input-schema version as canonical JSON
and exits `0` without loading any mission, vehicle, or asset file. A backend can
call it at startup to pin and check contract compatibility instead of running a
full job to read versions off an envelope. Every printed version is sourced from
the same module constant the envelopes emit (a drift test asserts this), so the
map cannot silently diverge from a real run. `--version` is unchanged.
- Atomic output writes and clean cancellation (Ticket 104). Every `--output`
write and every on-disk artifact writer (flight trace, phase segments,
validation report, calibration profile, SITL artifacts) now writes to a sibling
Expand Down
3 changes: 3 additions & 0 deletions adapters/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def _register_commands() -> None:
from adapters.commands.propagate import propagate
from adapters.commands.sample import sample
from adapters.commands.scenario import scenario
from adapters.commands.schema_versions import schema_versions
from adapters.commands.sitl import sitl
from adapters.commands.size_battery import size_battery
from adapters.commands.sora import sora
Expand All @@ -186,6 +187,8 @@ def _register_commands() -> None:
app.command()(validate)
app.command()(calibrate)
app.command()(bump)
app.command("schema-versions")(schema_versions)
app.command("contracts")(schema_versions)


_register_commands()
107 changes: 107 additions & 0 deletions adapters/commands/schema_versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Read-only contract-version discovery command.

Prints the supported input and output contract versions plus the tool version as
canonical JSON, then exits 0 without loading any mission or vehicle file. A
backend can call this at startup to pin and check contract compatibility instead
of inferring versions from a full run's envelope.
"""

from typing import get_args

import typer

import adapters.cli as cli
from adapters.battery_sizing_envelope import BATTERY_SIZING_REPORT_SCHEMA_VERSION
from adapters.canonical_json import render_canonical_json
from adapters.envelope import (
GEOFENCE_SCHEMA_VERSION,
LANDING_ZONE_SCHEMA_VERSION,
MISSION_SCHEMA_VERSION,
POPULATION_SCHEMA_VERSION,
RESULT_ENVELOPE_SCHEMA_VERSION,
TERRAIN_SCHEMA_VERSION,
VEHICLE_SCHEMA_VERSION,
WIND_GRID_SCHEMA_VERSION,
)
from adapters.scenario_envelope import (
SCENARIO_INPUT_SCHEMA_VERSION,
SCENARIO_REPORT_SCHEMA_VERSION,
)
from adapters.sitl.evidence import SITL_EVIDENCE_SCHEMA_VERSION
from adapters.stochastic_envelope import (
STOCHASTIC_ENVELOPE_SCHEMA_VERSION,
STOCHASTIC_INPUT_SCHEMA_VERSION,
)
from adapters.sora_envelope import SORA_ENVELOPE_SCHEMA_VERSION
from adapters.uncertainty_envelope import (
UNCERTAINTY_INPUT_SCHEMA_VERSION,
UNCERTAINTY_REPORT_SCHEMA_VERSION,
)
from adapters.version import tool_version
from schemas.batch import BatchManifest
from schemas.calibration import CALIBRATION_PROFILE_SCHEMA_VERSION
from schemas.flight_log import FLIGHT_TRACE_SCHEMA_VERSION
from schemas.phase_segment import PHASE_SEGMENT_SCHEMA_VERSION
from schemas.sitl_comparison import SITL_COMPARISON_SCHEMA_VERSION
from schemas.sora import SORA_ASSESSMENT_SCHEMA_VERSION
from schemas.validation import VALIDATION_REPORT_SCHEMA_VERSION

# The batch manifest carries its version as a Literal field rather than a named
# constant; source it from the field annotation so the discovery map cannot drift
# from what the loader actually accepts, without re-stating the string here.
BATCH_INPUT_SCHEMA_VERSION = get_args(
BatchManifest.model_fields["format_version"].annotation
)[0]


def _output_envelope_versions() -> dict[str, str]:
return {
"estimator": RESULT_ENVELOPE_SCHEMA_VERSION,
"scenario_report": SCENARIO_REPORT_SCHEMA_VERSION,
"uncertainty_report": UNCERTAINTY_REPORT_SCHEMA_VERSION,
"stochastic_envelope": STOCHASTIC_ENVELOPE_SCHEMA_VERSION,
"sora_envelope": SORA_ENVELOPE_SCHEMA_VERSION,
"battery_sizing_report": BATTERY_SIZING_REPORT_SCHEMA_VERSION,
"sitl_evidence": SITL_EVIDENCE_SCHEMA_VERSION,
"sitl_comparison": SITL_COMPARISON_SCHEMA_VERSION,
"validation_report": VALIDATION_REPORT_SCHEMA_VERSION,
"calibration_profile": CALIBRATION_PROFILE_SCHEMA_VERSION,
"flight_trace": FLIGHT_TRACE_SCHEMA_VERSION,
"phase_segments": PHASE_SEGMENT_SCHEMA_VERSION,
"sora_assessment": SORA_ASSESSMENT_SCHEMA_VERSION,
}


def _input_schema_versions() -> dict[str, str]:
return {
"mission": MISSION_SCHEMA_VERSION,
"vehicle": VEHICLE_SCHEMA_VERSION,
"geofences": GEOFENCE_SCHEMA_VERSION,
"landing_zones": LANDING_ZONE_SCHEMA_VERSION,
"terrain": TERRAIN_SCHEMA_VERSION,
"population": POPULATION_SCHEMA_VERSION,
"wind_grid": WIND_GRID_SCHEMA_VERSION,
"scenario": SCENARIO_INPUT_SCHEMA_VERSION,
"uncertainty": UNCERTAINTY_INPUT_SCHEMA_VERSION,
"stochastic": STOCHASTIC_INPUT_SCHEMA_VERSION,
"batch": BATCH_INPUT_SCHEMA_VERSION,
}


def _contract_versions() -> dict[str, object]:
return {
"tool_version": tool_version(),
"output_envelopes": _output_envelope_versions(),
"input_schemas": _input_schema_versions(),
}


def schema_versions() -> None:
"""Print supported contract versions as canonical JSON and exit 0.

Read-only discovery: loads no mission, vehicle, or asset file. The printed
versions are sourced from the same module constants the envelopes emit, so the
map cannot drift from what a real run would produce.
"""
typer.echo(render_canonical_json(_contract_versions()), nl=False)
raise typer.Exit(code=int(cli.CliExitCode.SUCCESS))
1 change: 1 addition & 0 deletions docs/CLI_EXIT_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Python's default `KeyboardInterrupt` behaviour.
| `convert` | ✓ | | ✓ | | ✓ | A missing/blank `--vehicle-profile` and parse errors are `11`. |
| `sitl` | ✓ | | ✓ | | ✓ | Adapter and asset-load errors are `11`. |
| `bump` | ✓ | | ✓ | | | Developer-only release tool. `11` on drift or a missing version part. |
| `schema-versions` | ✓ | | | | | Read-only contract discovery (alias `contracts`). Loads no input; always `0`. |

## Divergences to branch on carefully

Expand Down
58 changes: 57 additions & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ uv run bvlos-sim --help

## CLI Commands

bvlos-sim exposes fourteen commands:
bvlos-sim exposes fifteen commands:

- `estimate`: run deterministic mission estimation and static feasibility checks
- `size-battery`: compute the minimum battery capacity needed for feasibility
Expand All @@ -38,6 +38,7 @@ bvlos-sim exposes fourteen commands:
- `sora`: run the SORA pre-assessment (Ground Risk, Air Risk, and SAIL)
- `validate`: compare a predicted mission estimate against an observed flight trace
- `calibrate`: fit a calibration profile from a base vehicle and observed flight traces
- `schema-versions` (alias `contracts`): print supported input/output contract versions as JSON
- `bump`: bump the project version and roll the changelog (release tooling)

| Command | Exit 0 | Exit 10 | Exit 11 | Exit 12 | Exit 13 |
Expand All @@ -55,6 +56,7 @@ bvlos-sim exposes fourteen commands:
| sora | success | - | invalid input | - | internal error |
| validate | success | - | invalid input | - | internal error |
| calibrate | success | - | invalid input | - | internal error |
| schema-versions | success | - | - | - | - |
| bump | success / consistent | - | invalid input / drift | - | internal error |

[`CLI_EXIT_CODES.md`](CLI_EXIT_CODES.md) is the authoritative per-command
Expand Down Expand Up @@ -996,6 +998,60 @@ uv run bvlos-sim validate mission.yaml vehicle.yaml trace.json --calibration cal
See `examples/calibration/` for a full ingestion → segmentation → fitting →
apply walkthrough.

## Contract Discovery (`schema-versions`)

`schema-versions` (alias `contracts`) prints the supported input and output
contract versions plus the resolved `tool_version` as canonical JSON, then exits
`0` without loading any mission, vehicle, or asset file. A backend can call it at
startup to pin and check contract compatibility instead of running a full job to
read the versions off an envelope.

```bash
uv run bvlos-sim schema-versions
# alias:
uv run bvlos-sim contracts
```

Sample output (versions sourced from the same constants the envelopes emit, so
they cannot drift from a real run):

```json
{
"input_schemas": {
"batch": "batch.v1",
"geofences": "geofence-geojson.v1",
"landing_zones": "landing-zone-geojson.v1",
"mission": "mission.v6",
"population": "population-grid.v1",
"scenario": "scenario.v1",
"stochastic": "stochastic.v1",
"terrain": "terrain-grid.v1",
"uncertainty": "uncertainty.v1",
"vehicle": "vehicle.v4",
"wind_grid": "wind-grid.v1"
},
"output_envelopes": {
"battery_sizing_report": "battery-sizing-report.v1",
"calibration_profile": "calibration-profile.v1",
"estimator": "estimator-envelope.v7",
"flight_trace": "flight-trace.v1",
"phase_segments": "phase-segments.v1",
"scenario_report": "scenario-report.v2",
"sitl_comparison": "sitl-comparison.v1",
"sitl_evidence": "sitl-evidence.v1",
"sora_assessment": "sora-assessment.v1",
"sora_envelope": "sora-envelope.v1",
"stochastic_envelope": "stochastic-envelope.v1",
"uncertainty_report": "uncertainty-report.v1",
"validation_report": "validation-report.v1"
},
"tool_version": "0.32.0"
}
```

The command is read-only and always exits `0`; `--version` is unchanged and
still prints the plain `bvlos-sim <version>` line.

## Releasing (`bump`)

Cut a release in one reviewed step. `bump` bumps the version and rolls the
Expand Down
70 changes: 69 additions & 1 deletion docs/tickets/105-contract-version-discovery-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Status

Planned.
Implemented.

## Goal

Expand Down Expand Up @@ -59,3 +59,71 @@ version discovery to a successful run.
needs its own golden-fixture and version review: `_input_schema_versions()`
omits `obstacles` (`adapters/envelope.py:504`), and the battery-sizing envelope
carries no input schema-version field. Track them separately if pursued.

## Implementation

### New files

| File | Purpose |
|------|---------|
| `adapters/commands/schema_versions.py` | The `schema_versions` command: builds the `tool_version` / `output_envelopes` / `input_schemas` map from imported constants and prints it as canonical JSON. |
| `tests/test_schema_versions.py` | Exit-code, JSON-shape, drift-guard, alias-equivalence, no-file-argument, determinism, and `--version`-unchanged tests. |

### Command and alias

`schema-versions` is registered in `adapters/cli.py:_register_commands()` with
both its canonical name and the `contracts` alias, mirroring how `size-battery`
is registered with an explicit name:

```python
app.command("schema-versions")(schema_versions)
app.command("contracts")(schema_versions)
```

The command takes no arguments or options. It loads nothing, calls
`tool_version()`, renders the version map with `render_canonical_json` (sorted
keys, stable float precision, trailing newline), and exits `0`. `--version` is
untouched and still prints the plain `bvlos-sim <version>` line.

### Constants sourced (no string is restated)

Every printed version is imported from the module that owns it, so the map cannot
drift from what a real run emits; `tests/test_schema_versions.py` asserts the
printed value equals each imported constant.

- Output envelopes: `RESULT_ENVELOPE_SCHEMA_VERSION`,
`SCENARIO_REPORT_SCHEMA_VERSION`, `UNCERTAINTY_REPORT_SCHEMA_VERSION`,
`STOCHASTIC_ENVELOPE_SCHEMA_VERSION`, `SORA_ENVELOPE_SCHEMA_VERSION`,
`BATTERY_SIZING_REPORT_SCHEMA_VERSION`, `SITL_EVIDENCE_SCHEMA_VERSION`,
`SITL_COMPARISON_SCHEMA_VERSION`.
- Input schemas: `MISSION_SCHEMA_VERSION`, `VEHICLE_SCHEMA_VERSION`,
`GEOFENCE_SCHEMA_VERSION`, `LANDING_ZONE_SCHEMA_VERSION`,
`TERRAIN_SCHEMA_VERSION`, `POPULATION_SCHEMA_VERSION`, `WIND_GRID_SCHEMA_VERSION`,
`SCENARIO_INPUT_SCHEMA_VERSION`, `UNCERTAINTY_INPUT_SCHEMA_VERSION`,
`STOCHASTIC_INPUT_SCHEMA_VERSION`, and `batch.v1`.
- `batch.v1` has no named module constant — it is a `Literal` field on
`schemas.batch.BatchManifest`. Rather than restate the string, the command (and
the test) extract it from the field annotation with
`typing.get_args(BatchManifest.model_fields["format_version"].annotation)[0]`,
so the discovery map stays bound to what the loader actually accepts.

### Beyond the spec's eight output contracts

The spec listed eight core output/envelope contracts. Five more report/artifact
contracts have been added to the repo since this ticket was written; they are
included for completeness, each sourced from its own constant:
`VALIDATION_REPORT_SCHEMA_VERSION` (`validation-report.v1`),
`CALIBRATION_PROFILE_SCHEMA_VERSION` (`calibration-profile.v1`),
`FLIGHT_TRACE_SCHEMA_VERSION` (`flight-trace.v1`),
`PHASE_SEGMENT_SCHEMA_VERSION` (`phase-segments.v1`), and
`SORA_ASSESSMENT_SCHEMA_VERSION` (`sora-assessment.v1`). Including them does not
complicate the drift test (each is just another `imported == printed` assertion),
and it makes the discovery output a complete picture of the published contracts.

### Deliberately left out

The two data-completeness nits in the Notes above — the `obstacles` input-schema
version omitted from the live envelope's `_input_schema_versions()`, and the
missing battery-sizing input schema-version field — are out of scope here. Each
would change a published envelope and needs its own golden-fixture and version
review; the discovery command reports only what the envelopes emit today.
4 changes: 2 additions & 2 deletions docs/tickets/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ticket Backlog

**60 implemented · 18 planned · 1237 tests passing**
**61 implemented · 17 planned · 1245 tests passing**

This directory tracks every capability from idea to implementation. Completed
tickets are kept as historical records. Open tickets describe what to build
Expand Down Expand Up @@ -53,7 +53,6 @@ worker depends on.

| # | Ticket | What it adds |
|---|---|---|
| 105 | [Contract-version discovery command](./105-contract-version-discovery-command.md) | `schema-versions` command printing supported input/output contract versions without running a job |
| 106 | [Machine-readable run progress](./106-machine-readable-run-progress.md) | JSONL progress for `propagate`/`sample`/`batch` so a non-TTY worker can show live progress (extends 067) |
| 107 | [Machine-readable preflight report](./107-machine-readable-preflight-report.md) | JSON `--validate-only` envelope plus GeoJSON asset preflight across run types (composes with 089) |

Expand Down Expand Up @@ -203,3 +202,4 @@ New capabilities should work *with* existing pieces, not alongside them in isola
57. [098](./098-version-bump-and-release-tooling.md) Version bump and release tooling
58. [083](./083-calibration-profile-data-and-fitting.md) Calibration profile data and fitting
59. [104](./104-atomic-output-writes-and-cancellation.md) Atomic output writes and clean cancellation
60. [105](./105-contract-version-discovery-command.md) Contract-version discovery command
Loading
Loading