Skip to content

fix: simulator audit remediation - #40

Merged
Monotox merged 12 commits into
mainfrom
fix/simulator-audit-remediation
Jul 28, 2026
Merged

fix: simulator audit remediation#40
Monotox merged 12 commits into
mainfrom
fix/simulator-audit-remediation

Conversation

@Monotox

@Monotox Monotox commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Test Plan

  • uv run pytest passes
  • uv run ruff check . passes
  • Documentation updated, if user-facing behavior changed
  • Golden fixtures updated, if a public JSON or Markdown contract changed

Contract Impact

  • No public contract surfaces changed
  • Public contract changes are intentional and versioned per CONTRIBUTING.md (Public contracts)

Monotox added 12 commits July 28, 2026 12:33
Four inputs reached a GO the evidence did not support.
accepted_warning_codes could waive the zero-zone and coverage-missing
codes, so the file supplying an asset was the file waiving the check on
it. A NaN geofence bound disarmed the zone rather than failing, because
every comparison against NaN is false. The relevance envelope was
derived from the vehicle's own energy figures, so a 3 W cruise power
stretched it to 19 440 km and certified assets on another continent,
and a small pack shrank it enough to reject a real mast 6.45 km away.
Out-of-coverage wind clamped to the nearest edge, so a forecast for
Kansas answered a route over the Netherlands.

Calibration provenance was self-asserted the same way: one line of YAML,
or a profile naming no real flight, cleared ENERGY_MODEL_UNCALIBRATED.
Credit now comes only from supplied traces that reproduce the profile's
dataset id and refit its parameters. Without them the profile still
applies and the artifact records the lineage as unverified, so the run
states what it did not check.

Deeply nested YAML took the process down with SIGSEGV before any handler
ran, because libyaml's parser recurses in C. Depth is bounded with the
scanner, which tokenises iteratively and costs less than the load it
guards.
An omitted descent_power_w fell back to cruise power, which bills one of
the most expensive phases at the cheapest number in the profile for an
aircraft that comes down on its lift rotors. No shipped profile declares
the field, so this was the path that ran: worst-case RTH energy was
understated by 37 %.

usable_capacity_curve was read only at soc=1.0, and the ordering rule
made that the curve's maximum, so every natural sag curve was an exact
no-op. It is a scalar usable_capacity_fraction in vehicle.v6.

Air temperature never reached the density model, sustained turns and
fixed-wing orbits were billed at level cruise power despite holding a
bank, and the reserve percentage was a share of deliverable capacity
while its threshold was a share of nameplate, so a report could print a
landing reserve above the bar it had just failed.
A fixed-wing orbit contributed one population sample, at its centre,
while the report claimed a 160 m corridor. On a worked case that filed
iGRC 3 where the real ground track gives 6. The orbit is walked now, and
a missing turn radius fails closed.

The 1:1 ground risk buffer was validated against terrain under the route
centreline, so ground falling away inside the buffer went unseen; the
sampler takes a corridor width. effective_resolution_m was inert
metadata, and the shipped grid filed 100 m for cells 1113 m by 685 m.
over_urban_area and airspace_class were operator booleans the run never
questioned, though the fetched data can contradict them. The containment
column is the leftmost meeting the required robustness, as in the spec's
worked example.

The filed artifact now also says airspace currency was never assessed,
which its banner previously implied by naming only two omissions.
The projection depends only on the segment but was constructed inside
the obstacle loop, so a 50-obstacle mission spent 92 % of its runtime
rebuilding pyproj transformers it already had. Obstacle evidence is
mandatory for a GO, so this was the only path to an operational verdict:
100 obstacles took 26.8 s and now take 0.7 s, with unchanged results.

ListObstacleProvider also never set provider_id, because slots plus a
custom __init__ left the declared default off the instance, so
provenance recorded the class name instead of static_list.
drift_var_per_s was a deg² quantity used as deg²/s, so process noise
came from the measurement parameter. A receiver with availability 0 and
a 50 m accuracy figure produced 1453 m of unaided drift where 2.5 m gave
73 m, which is a category error rather than a tuning choice.
Affects the stochastic diagnostic only; no shipped profile changes.
--source openaip never worked: api.openaip.net does not resolve, and the
host alone was not the problem. The Core API returns an items envelope
rather than features, its records are flat, and icaoClass and type are
integer enums the string comparison could never match, so a corrected
request would still have written no zones. Pagination was unhandled.

openAIP airspace was attributed to OpenStreetMap under ODbL. It is
CC BY-NC-SA, and Open-Meteo's free tier is likewise non-commercial,
which NOTICE.md never said. Both now state the restriction and can be
pointed at a paid endpoint.

fetch_wind sampled one point and wrote a grid 0.22 m wide, which the
coverage check rejects; it takes a bounding box now, stepped to match
the model's own resolution. Fetched assets carry fetched_at, source and
licence, obstacle heights can follow a terrain grid instead of one
scalar base altitude, fetch_all only advertises assets it wrote, and the
Overpass endpoint is no longer hard-coded.

SECURITY.md pointed at a private reporting channel that was switched
off; it is enabled, and malformed-input crashes are in scope.
Nothing type-checked the package and no py.typed shipped, so downstream
importers got none of the annotations the code already carried. The
gate is a ratchet: everything is checked by default and the modules
still carrying findings are listed as exclusions, so a new module is
covered the moment it exists.

All six runtime dependencies were unbounded, so a future pydantic 3 or
shapely 3 would install cleanly into a fresh pip install and break at
runtime. They are capped at the next major, matching the reasoning
already applied to the pinned ruff rule set.
The tutorial's GO block quoted a reserve figure that had moved twice and
nothing pinned it, which is why the drift landed there. Both fetch
commands in data/README.md used flags that do not exist and exited 2.
sitl.md told Docker users to substitute docker for podman when sitl/
ships only a Containerfile. CONTRIBUTING pointed at a main module that
has never existed. The ticket index disagreed with itself three ways and
left six tickets unlisted.

Extends the doc test harness to re-run and pin getting-started, which
carries the numbers and was the only major page no test covered.
A calibration fitting cruise power alone vouched for coefficients it
never fitted: with the reference mission, real traces and hover/descent
edited to 1 W, the run printed GO with no warnings and 9.93 Wh against
an honest 24.92 Wh. The estimator now derives what each mission bills
against and names both the unfitted coefficient and the value the
vehicle declares for it, so an acknowledged waiver stays reviewable.

batch and scenario dropped the non-waivable floor under
--engineering-only, so a mission estimate refused at exit 10 came back
feasible at exit 0. The wind grid's below-floor clamp was unbounded,
letting a 3000-5000 m forecast certify a route at 12-132 m. Geofence
bounds still accepted the finite sentinels that disarm a zone the way
NaN did.

The output envelopes had gained a field, new enum members and a changed
Literal while their version strings stood still, so a consumer pinned on
the published version could not parse them.
The SORA footprint check measured a fixed-wing orbit at its centre, so
terrain under the flown ring never reached the buffer filed to an
authority: a gorge under the ring moved the reported maximum AGL from
132 m to 432 m. Return-to-home and divert paths flew Dubins arcs at the
turn radius but paid flat cruise power while the mission's own arcs
paid the bank load factor, leaving the contingency budget optimistic.

ICAO Class A and B never became keep-out geofences, so an uncoordinated
route through Class B got no forbidden zone and the file looked
complete. Fetched geofences also carried openAIP activation windows in
the fields that drop a zone from conflict checks, so a lapsed window
silently stopped a restricted area blocking the route.

Landing-zone states printed one reserve against deliverable capacity
beside a threshold against nameplate. The descent fallback claimed
hover power bounds descent from above without enforcing it, the bank
factor ran unbounded on a small declared turn radius, and GeoJSON
assets skipped the nesting guard every other input goes through.
The nesting guard reached the estimator's loaders and nothing else. A
QGroundControl .plan comes from a third party and a legacy mission comes
from wherever the operator kept it, yet both parsed raw, so a deeply
nested file surfaced as an internal error rather than the bad input it
is.

A mission.v6 file accepting GEOFENCE_ZERO_ZONES was valid against the
released tool and now has no field-for-field upgrade. Stripping the
entry would delete the operator's written record that they reviewed and
accepted that state, so the migration refuses it and names the codes and
the remedy. Failing through mission.v7 validation instead reported an
exceedance of a vehicle limit the file never exceeded; each non-waivable
code now carries its own reason.
The mypy step was the only one running unpinned, so it resolved its own
tool and stubs at job time and failed on missing `requests` stubs that a
developer with the scripts extra installed never sees. mypy and
types-PyYAML move into the dev group, the step runs `--locked` like
every other, and `requests` joins the stubless third-party libraries
already listed — it is only imported from modules the gate excludes.

The Overpass query test guarded nothing, so it raised on the module's
own `requests` check instead of exercising the query. Every other test
in that file patches the attribute for exactly this reason.

Both failures needed CI's own dependency set to reproduce: a local venv
carrying the scripts extra hides them.
@Monotox
Monotox merged commit 5ecd654 into main Jul 28, 2026
7 checks passed
@Monotox
Monotox deleted the fix/simulator-audit-remediation branch July 28, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant