aero-agent-roles 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/NOTICE +18 -0
  2. package/README.md +49 -0
  3. package/bin/aero-agent-roles.js +127 -0
  4. package/lib/catalog.js +97 -0
  5. package/lib/install.js +48 -0
  6. package/lib/mcp.js +199 -0
  7. package/manifest.json +516 -0
  8. package/package.json +44 -0
  9. package/roles/aerodynamics-engineer/ROLE.md +153 -0
  10. package/roles/aerodynamics-engineer/SOURCES.md +12 -0
  11. package/roles/aerodynamics-engineer/cli.py +143 -0
  12. package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
  13. package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
  14. package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
  15. package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
  16. package/roles/aircraft-design-engineer/ROLE.md +138 -0
  17. package/roles/aircraft-design-engineer/SOURCES.md +12 -0
  18. package/roles/aircraft-design-engineer/cli.py +152 -0
  19. package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
  20. package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
  21. package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
  22. package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
  23. package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
  24. package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
  25. package/roles/airworthiness-compliance-engineer/cli.py +146 -0
  26. package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
  27. package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
  28. package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
  29. package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
  30. package/roles/as9100-quality-auditor/ROLE.md +128 -0
  31. package/roles/as9100-quality-auditor/SOURCES.md +16 -0
  32. package/roles/as9100-quality-auditor/cli.py +136 -0
  33. package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
  34. package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
  35. package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
  36. package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
  37. package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
  38. package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
  39. package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
  40. package/roles/data-bus-avionics-engineer/cli.py +311 -0
  41. package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
  42. package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
  43. package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
  44. package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
  45. package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
  46. package/roles/do160-environmental-engineer/ROLE.md +136 -0
  47. package/roles/do160-environmental-engineer/SOURCES.md +30 -0
  48. package/roles/do160-environmental-engineer/cli.py +147 -0
  49. package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
  50. package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
  51. package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
  52. package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
  53. package/roles/do178c-cert-engineer/ROLE.md +120 -0
  54. package/roles/do178c-cert-engineer/SOURCES.md +16 -0
  55. package/roles/do178c-cert-engineer/cli.py +129 -0
  56. package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
  57. package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
  58. package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
  59. package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
  60. package/roles/do254-hardware-engineer/ROLE.md +127 -0
  61. package/roles/do254-hardware-engineer/SOURCES.md +19 -0
  62. package/roles/do254-hardware-engineer/cli.py +308 -0
  63. package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
  64. package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
  65. package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
  66. package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
  67. package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
  68. package/roles/engineering-analysis-engineer/ROLE.md +154 -0
  69. package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
  70. package/roles/engineering-analysis-engineer/cli.py +151 -0
  71. package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
  72. package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
  73. package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
  74. package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
  75. package/roles/flight-management-engineer/ROLE.md +160 -0
  76. package/roles/flight-management-engineer/SOURCES.md +30 -0
  77. package/roles/flight-management-engineer/cli.py +258 -0
  78. package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
  79. package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
  80. package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
  81. package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
  82. package/roles/flight-mechanics-engineer/ROLE.md +162 -0
  83. package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
  84. package/roles/flight-mechanics-engineer/cli.py +149 -0
  85. package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
  86. package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
  87. package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
  88. package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
  89. package/roles/flight-test-engineer/ROLE.md +137 -0
  90. package/roles/flight-test-engineer/SOURCES.md +24 -0
  91. package/roles/flight-test-engineer/cli.py +135 -0
  92. package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
  93. package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
  94. package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
  95. package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
  96. package/roles/flight-test-performance-engineer/ROLE.md +165 -0
  97. package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
  98. package/roles/flight-test-performance-engineer/cli.py +319 -0
  99. package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
  100. package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
  101. package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
  102. package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
  103. package/roles/gnc-engineer/ROLE.md +141 -0
  104. package/roles/gnc-engineer/SOURCES.md +35 -0
  105. package/roles/gnc-engineer/cli.py +141 -0
  106. package/roles/gnc-engineer/core/gnc_core.py +1074 -0
  107. package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
  108. package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
  109. package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
  110. package/roles/guidance-engineer/ROLE.md +156 -0
  111. package/roles/guidance-engineer/SOURCES.md +43 -0
  112. package/roles/guidance-engineer/cli.py +280 -0
  113. package/roles/guidance-engineer/core/guidance_core.py +820 -0
  114. package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
  115. package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
  116. package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
  117. package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
  118. package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
  119. package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
  120. package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
  121. package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
  122. package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
  123. package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
  124. package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
  125. package/roles/ndt-engineer/ROLE.md +157 -0
  126. package/roles/ndt-engineer/SOURCES.md +18 -0
  127. package/roles/ndt-engineer/cli.py +316 -0
  128. package/roles/ndt-engineer/core/ndt_core.py +1590 -0
  129. package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
  130. package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
  131. package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
  132. package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
  133. package/roles/numerical-analysis-engineer/ROLE.md +158 -0
  134. package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
  135. package/roles/numerical-analysis-engineer/cli.py +414 -0
  136. package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
  137. package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
  138. package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
  139. package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
  140. package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
  141. package/roles/propulsion-engineer/ROLE.md +163 -0
  142. package/roles/propulsion-engineer/SOURCES.md +15 -0
  143. package/roles/propulsion-engineer/cli.py +168 -0
  144. package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
  145. package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
  146. package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
  147. package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
  148. package/roles/space-systems-engineer/ROLE.md +161 -0
  149. package/roles/space-systems-engineer/SOURCES.md +49 -0
  150. package/roles/space-systems-engineer/cli.py +173 -0
  151. package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
  152. package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
  153. package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
  154. package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
  155. package/roles/state-estimation-engineer/ROLE.md +154 -0
  156. package/roles/state-estimation-engineer/SOURCES.md +27 -0
  157. package/roles/state-estimation-engineer/cli.py +326 -0
  158. package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
  159. package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
  160. package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
  161. package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
  162. package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
  163. package/roles/structures-loads-engineer/ROLE.md +171 -0
  164. package/roles/structures-loads-engineer/SOURCES.md +44 -0
  165. package/roles/structures-loads-engineer/cli.py +219 -0
  166. package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
  167. package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
  168. package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
  169. package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
  170. package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
@@ -0,0 +1,76 @@
1
+ # Navigation State Estimator Design Report
2
+
3
+ **Item:** UAV navigation state estimator (INS/GNSS + AHRS)
4
+ **Estimator class:** attitude (complementary filter) + loosely-coupled GNSS/INS
5
+ **Vehicle / platform:** small fixed-wing UAV (autopilot reference design)
6
+ **Intended use:** design analysis for the navigation state estimator before implementation on the flight computer
7
+ **Status:** draft-for-review
8
+
9
+ ## 1. Scope
10
+
11
+ This report produces the navigation state estimator design for UAV navigation state estimator (INS/GNSS + AHRS). Loosely-coupled GNSS/INS navigation filter with a complementary-filter attitude channel for a small fixed-wing UAV autopilot. The estimator architecture is attitude (complementary filter) + loosely-coupled GNSS/INS: an attitude channel (complementary filter) plus a loosely-coupled GNSS position/velocity channel (linear constant-velocity Kalman recursion with an EKF nonlinear-update option). Outputs are DRAFT design numbers for a human navigation engineer to review before implementation on the target computer.
12
+
13
+ ## 2. Design inputs (noise model)
14
+
15
+ - Navigation update interval dt = 1 s over a 60-step design horizon.
16
+ - Process noise intensity q = 0.01 m^2/s^3 -> sigma_w = 0.1 m/s^2.
17
+ - Measurement variance r = 4 m^2 (GNSS position, sigma_v = 2.0 m).
18
+ - Initial covariance P0 = diag(100 m^2, 25 (m/s)^2).
19
+ - IMU/attitude rate dt = 0.01 s; attitude design horizon 100 s.
20
+ - Particle fallback count n = 1000.
21
+
22
+ ## 3. Attitude channel (complementary filter)
23
+
24
+ - Filter: Mahony-style explicit complementary filter (SO(3)).
25
+ - Gains: k_p = 2 1/s, k_i = 0.4 1/s^2 (leaf module defaults); corrected rate omega_c = omega_m - b + k_p*e with bias step b <- b - k_i*e*dt.
26
+ - Gyro bias input b = [0.001, -0.0008, 0.0006] rad/s per axis, |b| = 0.001414 rad/s.
27
+ - Drift anchor: uncompensated bias integrates to |b|*t = 0.014142 rad after 10 s and 0.141421 rad (8.103 deg) after 100 s - the error the bias estimate removes.
28
+ - Discrete-integration lag bound |omega|*dt = 0.000229 rad/step (0.013121 deg/step) at the example body rate.
29
+ - Steady-state acceptance: trailing-window innovation norm below 0.001 rad (leaf steady_state_verdict).
30
+ - Observability note: gyro bias is observable when at least two non-parallel reference vectors (accelerometer + magnetometer) are fused; a single reference leaves the bias along its axis unobservable (Mahony et al. 2008 nonlinear complementary filter on SO(3)).
31
+
32
+ ## 4. Navigation channel (Kalman recursion)
33
+
34
+ - Model: discrete constant-velocity, H = [1 0], per axis.
35
+ - Transition F = [1.0, 1.0] / [0.0, 1.0]; discretized process covariance Q = [0.0025, 0.005] / [0.005, 0.01].
36
+ - Alpha-beta design companion (fixed-gain alternative for the smoothing stage): alpha = 0.5, Benedict-Bordner critical-damping beta = alpha^2/(2-alpha) = 0.166667; tracking index lambda = sigma_w*dt^2/sigma_v = 0.05 gives Kalata gains alpha = 0.270867, beta = 0.042695.
37
+ - First update: gain K = [0.968992849, 0.193833453], innovation variance S = 129.0025, position variance after 3.875971396 m^2.
38
+ - Mid-horizon (step 30): gain K = [0.270963014, 0.042713764], S = 5.486690082, position variance 1.083852057 m^2.
39
+ - Final (step 60): gain K = [0.270867122, 0.042694639], S = 5.4859685, position variance 1.083468489 m^2, velocity variance variance 0.058442889 (m/s)^2 - the steady-state covariance the design reports. The converged gain equals the Kalata alpha-beta pair above within the recursion tolerance, confirming the fixed-gain design is a faithful steady-state approximation of the full recursion.
40
+ - Residual RMS over the deterministic design run: 0.087482 m.
41
+
42
+ ## 5. Nonlinear update design (EKF linearization)
43
+
44
+ - Measurement model: range + bearing (nonlinear) h(x) = [hypot(px,py), atan2(py,px)].
45
+ - Operating point x = [300.0, 40.0, 12.0, 0.0] (true measurement [302.6549, 0.132552]).
46
+ - Linearized measurement Jacobian (central differences) H = [0.991227893, 0.132163706, 0.0, 0.0] / [-0.000436681, 0.003275109, 0.0, 0.0].
47
+ - Deterministic measurement z = [305.6549, 0.132552] (range biased +3 m input).
48
+ - Innovation y = [3.0, 0.0]; innovation covariance diag(S) = [108.99999803, 0.00139632].
49
+ - Gain first row K = [0.909383403, -31.273707682].
50
+ - Corrected state x+ = [302.72815, 40.363753, 12.0, 0.0].
51
+
52
+ ## 6. Consistency and fallback verification
53
+
54
+ - NEES = 0.9174 against the 2-state position subspace (expected value n = 2 for a consistent filter).
55
+ - SIR particle fallback: uniform ensemble of 1000 particles gives ESS = 1000; resample when ESS < n/2 (SIR, particle-filter leaf) (threshold 500).
56
+ - IMM two-mode bank (two-mode CV/CA IMM bank with Markov mode mixing for maneuvering flight).
57
+ - UKF option (scaled-unscented-transform alternative to EKF linearization for strongly nonlinear updates).
58
+
59
+ ## 7. Offline post-processing (RTS smoothing)
60
+
61
+ - Method: fixed-interval RTS smoother over stored forward run.
62
+ - Smoothed position variance at the final boundary equals the filtered value (True); every interior smoothed variance at or below the filtered (True).
63
+ - Maximum covariance reduction: 82.946% (smoothed final position variance 1.083468489 m^2).
64
+
65
+ ## 8. Observability analysis
66
+
67
+ - Discrete observability matrix rows [1.0, 0.0] / [1.0, 1.0] with determinant 1.0.
68
+ - Verdict: rank 2, fully observable for dt > 0: the velocity state is observable through the dynamics between position samples.
69
+
70
+ ## 9. Verification evidence and open items
71
+
72
+ - Evidence gates (see cli.py check): architecture stated, attitude + navigation channels designated, alpha-beta and Kalman/EKF numbers present, observability checked, consistency metrics present.
73
+ - Open items for the human navigation engineer: final sensor datasheet noise values, lever-arm and time-tagging alignment, flight-computer numerical format, and the in-flight consistency monitoring policy.
74
+
75
+ ---
76
+ *Generated by Aero Agent Roles state-estimation-engineer core (2026-09-05). DRAFT for human navigation engineer review. Not an approval document. Not flight software release.*
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Test the evidence bundle + skill-dispatch protocol (PROTOCOL.md v1).
3
+
4
+ Verifies:
5
+ - build --bundle emits evidence/{model,gates,provenance}.json
6
+ - bundle JSON parses, model contains computed numbers, gates all_pass
7
+ - skill dispatch cross-checks the estimator computations against the
8
+ bound estimation-filtering leaf logic (agreeing within tolerance)
9
+ when AeroSkills is present
10
+ - standalone (no AeroSkills) emits the bundle honestly with
11
+ cross_checked=false
12
+ - check --file passes the gate vocabulary
13
+ """
14
+ import json
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import tempfile
20
+ import unittest
21
+
22
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ CLI = os.path.join(ROLE_DIR, "cli.py")
24
+
25
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
26
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
27
+
28
+ EXPECTED_DISPATCH_FNS = ["steady_state_gains", "gains_from_tracking_index",
29
+ "forward_kalman", "jacobian_h", "nees",
30
+ "effective_sample_size"]
31
+
32
+
33
+ def run_build(outdir, env_extra=None):
34
+ env = dict(os.environ)
35
+ if env_extra:
36
+ env.update(env_extra)
37
+ args = [sys.executable, CLI, "build", "--out",
38
+ os.path.join(outdir, "report.md"), "--bundle"]
39
+ return subprocess.run(args, capture_output=True, text=True, env=env)
40
+
41
+
42
+ class TestBundleProtocol(unittest.TestCase):
43
+
44
+ def setUp(self):
45
+ self.tmp = tempfile.mkdtemp()
46
+
47
+ def tearDown(self):
48
+ shutil.rmtree(self.tmp, ignore_errors=True)
49
+
50
+ def test_bundle_emits_three_files(self):
51
+ r = run_build(self.tmp)
52
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
53
+ ev = os.path.join(self.tmp, "evidence")
54
+ for f in ("model.json", "gates.json", "provenance.json"):
55
+ self.assertTrue(os.path.exists(os.path.join(ev, f)),
56
+ f + " missing")
57
+
58
+ def test_model_has_numbers_and_status(self):
59
+ run_build(self.tmp)
60
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
61
+ self.assertIn("kalman", m)
62
+ self.assertIn("final", m["kalman"])
63
+ self.assertIn("gain", m["kalman"]["final"])
64
+ self.assertEqual(m.get("status"), "draft-for-review")
65
+ self.assertIn("role", m)
66
+ self.assertIn("ekf", m)
67
+
68
+ def test_gates_all_pass_exit_zero(self):
69
+ run_build(self.tmp)
70
+ g = json.load(open(os.path.join(self.tmp, "evidence", "gates.json")))
71
+ self.assertTrue(g["all_pass"])
72
+ self.assertEqual(g["exit_code"], 0)
73
+ self.assertGreaterEqual(len(g["gates"]), 5)
74
+
75
+ def test_dispatch_crosschecks_when_skills_present(self):
76
+ if not HAS_SKILLS:
77
+ self.skipTest("AeroSkills not present")
78
+ r = run_build(self.tmp)
79
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
80
+ p = json.load(open(os.path.join(self.tmp, "evidence",
81
+ "provenance.json")))
82
+ self.assertTrue(p["cross_checked"])
83
+ self.assertGreaterEqual(len(p["skills"]), 5)
84
+ fns = [row["function"] for row in p["skills"]]
85
+ for fn in EXPECTED_DISPATCH_FNS:
86
+ self.assertIn(fn, fns, f"dispatch row missing: {fn}")
87
+ for row in p["skills"]:
88
+ self.assertTrue(row["dispatched"])
89
+ self.assertTrue(row["agrees"], "core/skill disagree: %s" % row)
90
+ self.assertLessEqual(row["delta"], 1e-6)
91
+
92
+ def test_standalone_honest_no_skills(self):
93
+ r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
94
+ self.assertEqual(r.returncode, 0)
95
+ p = json.load(open(os.path.join(self.tmp, "evidence",
96
+ "provenance.json")))
97
+ self.assertFalse(p["cross_checked"])
98
+ self.assertEqual(len(p["skills"]), 0)
99
+
100
+ def test_check_still_works(self):
101
+ run_build(self.tmp)
102
+ md = os.path.join(self.tmp, "report.md")
103
+ c = subprocess.run([sys.executable, CLI, "check", "--file", md],
104
+ capture_output=True, text=True)
105
+ self.assertEqual(c.returncode, 0, c.stdout)
106
+ self.assertIn("RESULT: PASS", c.stdout)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ unittest.main()
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Role test: State Estimation Engineer.
3
+
4
+ Verifies (offline, no network):
5
+ 1. Every skill bound in ROLE.md frontmatter RESOLVES to a real leaf in
6
+ the aero-agent-skills repo (the pinned skills_release band).
7
+ 2. The workflow stage order is deterministic and complete.
8
+ 3. The filled deliverable template is complete (no blanks, draft
9
+ markers, sections) and the role carries the executable anatomy.
10
+ 4. The core engine builds the example deliverable standalone.
11
+ """
12
+ import os
13
+ import re
14
+ import sys
15
+ import unittest
16
+
17
+ ROLES_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
18
+ os.path.abspath(__file__))))) # repo root (4 up from tests/)
19
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
20
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
21
+ ROLE_DIR = os.path.join(ROLES_REPO, "roles", "state-estimation-engineer")
22
+ TEMPLATE = os.path.join(ROLE_DIR, "templates",
23
+ "nav-state-estimator-design-template.md")
24
+ CORE_DIR = os.path.join(ROLE_DIR, "core")
25
+
26
+ sys.path.insert(0, CORE_DIR)
27
+ import state_estimation_core # noqa: E402
28
+
29
+ EXPECTED_BOUND = [
30
+ "gnc-autonomy/estimation-filtering/alpha-beta-filter",
31
+ "gnc-autonomy/estimation-filtering/complementary-filter",
32
+ "gnc-autonomy/estimation-filtering/extended-kalman-filter",
33
+ "gnc-autonomy/estimation-filtering/interacting-multiple-model-filter",
34
+ "gnc-autonomy/estimation-filtering/particle-filter",
35
+ "gnc-autonomy/estimation-filtering/rts-smoother",
36
+ "gnc-autonomy/estimation-filtering/unscented-kalman-filter",
37
+ ]
38
+
39
+ EXPECTED_STAGES = [
40
+ "1. Architecture + noise model", "2. Attitude channel",
41
+ "3. Navigation channel", "4. Fixed-gain companion",
42
+ "5. Nonlinear updates", "6. Fallbacks",
43
+ "7. Offline post-processing", "8. Observability", "9. Report",
44
+ ]
45
+
46
+
47
+ def read_role_md():
48
+ p = os.path.join(ROLE_DIR, "ROLE.md")
49
+ if not os.path.exists(p):
50
+ return None
51
+ return open(p).read()
52
+
53
+
54
+ class TestStateEstimationEngineerRole(unittest.TestCase):
55
+
56
+ def test_bound_skills_resolve_in_aeroskills(self):
57
+ if not HAS_SKILLS:
58
+ self.skipTest("Aero Agent Skills checkout not present "
59
+ "(cross-repo dev check)")
60
+ for leaf in EXPECTED_BOUND:
61
+ sk = os.path.join(AEROSKILLS, "skills", leaf, "SKILL.md")
62
+ self.assertTrue(
63
+ os.path.exists(sk),
64
+ f"bound skill not found in aero-agent-skills: {leaf}")
65
+
66
+ def test_bound_skills_listed_in_frontmatter(self):
67
+ role = read_role_md()
68
+ self.assertIsNotNone(role)
69
+ for leaf in EXPECTED_BOUND:
70
+ self.assertIn(leaf, role,
71
+ f"missing in ROLE.md skills_bound: {leaf}")
72
+
73
+ def test_workflow_stages_deterministic(self):
74
+ role = read_role_md()
75
+ wf = role[role.find("## Workflow"):role.find("## Evidence gates")]
76
+ idx = [wf.find(stage) for stage in EXPECTED_STAGES]
77
+ self.assertTrue(all(i >= 0 for i in idx), "a stage is missing")
78
+ self.assertEqual(idx, sorted(idx), "stage order not deterministic")
79
+
80
+ def test_deliverable_template_complete(self):
81
+ self.assertTrue(os.path.exists(TEMPLATE),
82
+ "deliverable template missing")
83
+ text = open(TEMPLATE).read()
84
+ for n in range(1, 10):
85
+ self.assertTrue(
86
+ re.search(rf"^## {n}\. ", text, re.M),
87
+ f"report section {n} missing")
88
+ self.assertIn("Navigation State Estimator Design Report", text)
89
+ self.assertNotIn("___", text)
90
+ low = text.lower()
91
+ self.assertIn("draft", low)
92
+ self.assertIn("not an approval", low)
93
+ self.assertIn("estimator class", low)
94
+
95
+ def test_forbidden_lines_present(self):
96
+ role = read_role_md()
97
+ for phrase in ["release navigation/flight software",
98
+ "authorize flight", "claim certification approval",
99
+ "reproduce proprietary", "sign_off_required: true"]:
100
+ self.assertIn(phrase, role, f"missing boundary: {phrase}")
101
+
102
+ def test_sources_register_exists(self):
103
+ p = os.path.join(ROLE_DIR, "SOURCES.md")
104
+ self.assertTrue(os.path.exists(p), "SOURCES.md missing for role")
105
+
106
+ def test_core_engine_exists(self):
107
+ """100% standard: every role ships an executable core + cli."""
108
+ for f in ["core", "cli.py"]:
109
+ self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, f)),
110
+ f"{f} missing - role is not executable")
111
+
112
+ def test_author_attribution(self):
113
+ role = read_role_md()
114
+ self.assertIn("author: ashfordeOU", role)
115
+
116
+ def test_example_deliverable_builds_and_gates(self):
117
+ """The engine produces the deliverable standalone and gates pass."""
118
+ model = state_estimation_core.build_report(
119
+ state_estimation_core.example_item())
120
+ md = state_estimation_core.render_report_markdown(model)
121
+ self.assertGreater(len(md), 3000)
122
+ self.assertTrue(state_estimation_core.check_report(model)["all_pass"])
123
+ self.assertTrue(state_estimation_core.check_report_markdown(md)["all_pass"])
124
+
125
+
126
+ if __name__ == "__main__":
127
+ unittest.main()
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env python3
2
+ """Test state_estimation_core: the executable engine of the role.
3
+
4
+ Proves the role can DO its job standalone (no AeroSkills needed):
5
+ alpha-beta gain rules (Benedict-Bordner, Kalata), the discrete
6
+ constant-velocity Kalman recursion, EKF linearization, NEES/ESS
7
+ consistency metrics, RTS smoothing, observability, report generation,
8
+ and the evidence-gate checks.
9
+ """
10
+ import os
11
+ import sys
12
+ import unittest
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
15
+ "..", "core"))
16
+ from state_estimation_core import ( # noqa: E402
17
+ build_report, check_report, check_report_markdown,
18
+ effective_sample_size, example_item, example_report_markdown,
19
+ kalata_gains, nees, numeric_jacobian, observability_rank,
20
+ render_report_markdown, run_cv_kalman, rts_smooth, steady_state_beta,
21
+ steady_state_verdict, tracking_index_from_noise,
22
+ )
23
+
24
+
25
+ class TestAlphaBetaRules(unittest.TestCase):
26
+
27
+ def test_benedict_bordner_beta(self):
28
+ # beta = alpha^2 / (2 - alpha); at alpha = 0.5 -> 1/6.
29
+ self.assertAlmostEqual(steady_state_beta(0.5), 0.25 / 1.5)
30
+ self.assertAlmostEqual(steady_state_beta(0.3),
31
+ 0.09 / 1.7)
32
+
33
+ def test_benedict_bordner_invalid_alpha(self):
34
+ with self.assertRaises(ValueError):
35
+ steady_state_beta(2.0)
36
+ with self.assertRaises(ValueError):
37
+ steady_state_beta(0.0)
38
+
39
+ def test_kalata_gains_zero_index(self):
40
+ self.assertEqual(kalata_gains(0.0)["alpha"], 0.0)
41
+ self.assertEqual(kalata_gains(0.0)["beta"], 0.0)
42
+
43
+ def test_kalata_gains_match_leaf_example(self):
44
+ # alpha-beta leaf worked formula: lambda = 0.05 -> alpha~0.270867
45
+ g = kalata_gains(0.05)
46
+ self.assertAlmostEqual(g["alpha"], 0.270867119, places=6)
47
+ self.assertAlmostEqual(g["beta"], 0.042694639, places=6)
48
+
49
+ def test_tracking_index_definition(self):
50
+ # lambda = sigma_w*dt^2/sigma_v (leaf definition)
51
+ self.assertAlmostEqual(tracking_index_from_noise(0.1, 1.0, 2.0),
52
+ 0.05)
53
+ with self.assertRaises(ValueError):
54
+ tracking_index_from_noise(0.1, 1.0, 0.0)
55
+
56
+
57
+ class TestKalmanRecursion(unittest.TestCase):
58
+
59
+ def test_recursion_converges_to_kalata_gains(self):
60
+ # The converged CV-Kalman gain equals the Kalata alpha-beta pair
61
+ # for the same sigma_w*dt^2/sigma_v tracking index.
62
+ recs = run_cv_kalman([0.5 * k for k in range(1, 61)], 1.0,
63
+ 0.01, 4.0, [0.0, 0.0],
64
+ [[100.0, 0.0], [0.0, 25.0]])
65
+ k = kalata_gains(0.05)
66
+ self.assertAlmostEqual(recs[-1]["gain"][0], k["alpha"], places=6)
67
+ self.assertAlmostEqual(recs[-1]["gain"][1], k["beta"], places=6)
68
+
69
+ def test_covariance_decreases(self):
70
+ recs = run_cv_kalman([1.0] * 20, 1.0, 0.01, 4.0)
71
+ self.assertGreater(recs[0]["P_filt"][0][0],
72
+ recs[-1]["P_filt"][0][0])
73
+ self.assertLess(recs[-1]["P_filt"][0][0], 4.0) # below R
74
+
75
+ def test_first_gain_uses_initial_covariance(self):
76
+ # P_pred = F P0 F^T + Q -> [0][0] = P0_xx + 2*dt*P0_xv +
77
+ # dt^2*P0_vv + q*dt^4/4 = 125.0025 at the defaults.
78
+ recs = run_cv_kalman([1.0] * 5, 1.0, 0.01, 4.0,
79
+ [0.0, 0.0], [[100.0, 0.0], [0.0, 25.0]])
80
+ self.assertAlmostEqual(recs[0]["gain"][0], 125.0025 / 129.0025)
81
+
82
+ def test_invalid_inputs(self):
83
+ with self.assertRaises(ValueError):
84
+ run_cv_kalman([1.0], 0.0, 0.01, 4.0)
85
+ with self.assertRaises(ValueError):
86
+ run_cv_kalman([], 1.0, 0.01, 4.0)
87
+ with self.assertRaises(ValueError):
88
+ run_cv_kalman([1.0], 1.0, 0.01, 0.0)
89
+
90
+
91
+ class TestEKFAndMetrics(unittest.TestCase):
92
+
93
+ def test_numeric_jacobian_matches_analytic_range_derivative(self):
94
+ # dr/dpx = px/hypot(px, py) at the operating point.
95
+ op = [300.0, 40.0]
96
+ px, py = op
97
+ r = (px * px + py * py) ** 0.5
98
+ j = numeric_jacobian(lambda x: [(x[0] ** 2 + x[1] ** 2) ** 0.5], op)
99
+ self.assertAlmostEqual(j[0][0], px / r, places=6)
100
+
101
+ def test_ekf_correction_reduces_position_uncertainty(self):
102
+ model = build_report(example_item())
103
+ ek = model["ekf"]
104
+ self.assertTrue(ek["x_corrected"][0] > 0.0)
105
+ # innovation equals the deterministic +3 m range bias
106
+ self.assertAlmostEqual(ek["innovation"][0], 3.0, places=6)
107
+
108
+ def test_nees_zero_for_exact_estimate(self):
109
+ self.assertAlmostEqual(
110
+ nees([1.0, 2.0], [[1.0, 0.0], [0.0, 1.0]], [1.0, 2.0]), 0.0)
111
+
112
+ def test_nees_matches_ukf_leaf_criterion(self):
113
+ # 2-state example: NEES of (5, -2) error with P = 100*I is
114
+ # (25 + 4)/100 = 0.29, well below the expected n = 2.
115
+ v = nees([300.0, 40.0], [[100.0, 0.0], [0.0, 100.0]],
116
+ [295.0, 42.0])
117
+ self.assertAlmostEqual(v, 0.29, places=6)
118
+
119
+ def test_ess_uniform_ensemble(self):
120
+ # ESS = (sum w)^2/sum(w^2); uniform n=1000 -> 1000.
121
+ w = [1.0 / 1000.0] * 1000
122
+ self.assertAlmostEqual(effective_sample_size(w), 1000.0, places=4)
123
+
124
+ def test_ess_degenerate(self):
125
+ w = [1.0, 0.0, 0.0, 0.0]
126
+ self.assertAlmostEqual(effective_sample_size(w), 1.0, places=9)
127
+ with self.assertRaises(ValueError):
128
+ effective_sample_size([])
129
+
130
+ def test_steady_state_verdict(self):
131
+ # Converged: trailing-window (last min(10,n)) mean below tol.
132
+ norms = [1e-2] + [1e-4] * 10 # transient then 10 quiet steps
133
+ self.assertTrue(steady_state_verdict(norms, 1e-3))
134
+ self.assertFalse(steady_state_verdict([1e-2, 2e-3, 3e-3, 2e-3], 1e-3))
135
+ with self.assertRaises(ValueError):
136
+ steady_state_verdict([], 1e-3)
137
+ with self.assertRaises(ValueError):
138
+ steady_state_verdict([1e-4], 0.0)
139
+
140
+
141
+ class TestSmoothingAndObservability(unittest.TestCase):
142
+
143
+ def test_smoother_reduces_covariance(self):
144
+ model = build_report(example_item())
145
+ sm = model["smoothing"]
146
+ self.assertTrue(sm["all_reduced"])
147
+ self.assertTrue(sm["boundary_matches"])
148
+ self.assertGreater(sm["max_reduction"], 0.0)
149
+
150
+ def test_smoother_matches_filter_at_boundary(self):
151
+ recs = run_cv_kalman([0.5 * k for k in range(1, 11)], 1.0,
152
+ 0.01, 4.0)
153
+ out = rts_smooth([dict(r, dt=1.0) for r in recs])
154
+ self.assertAlmostEqual(out["smoothed_covs"][-1][0][0],
155
+ recs[-1]["P_filt"][0][0], places=12)
156
+ self.assertEqual(len(out["smoothed_states"]), len(recs))
157
+
158
+ def test_observability(self):
159
+ o = observability_rank(1.0)
160
+ self.assertEqual(o["rank"], 2)
161
+ self.assertTrue(o["observable"])
162
+ self.assertAlmostEqual(o["det"], 1.0)
163
+
164
+
165
+ class TestReportBuilderAndGates(unittest.TestCase):
166
+
167
+ def test_example_builds_complete_model(self):
168
+ model = build_report(example_item())
169
+ self.assertEqual(model["status"], "draft-for-review")
170
+ self.assertEqual(model["document_type"],
171
+ "Navigation State Estimator Design Report")
172
+ self.assertTrue(model["estimator_class"])
173
+ self.assertIn("kalman", model)
174
+ self.assertIn("ekf", model)
175
+ self.assertIn("attitude", model)
176
+ self.assertIn("alpha_beta", model)
177
+
178
+ def test_example_numbers_present(self):
179
+ model = build_report(example_item())
180
+ km = model["kalman"]["final"]
181
+ self.assertGreater(km["gain"][0], 0.0)
182
+ self.assertGreater(km["pos_var_after"], 0.0)
183
+ self.assertTrue(model["observability"]["observable"])
184
+ self.assertIsInstance(model["ekf"]["nees_2d"], float)
185
+ # attitude drift anchor: |b|*t with the example bias vector
186
+ self.assertAlmostEqual(model["attitude"]["drift_100s_rad"],
187
+ 0.141421, places=4)
188
+
189
+ def test_core_gates_all_pass(self):
190
+ model = build_report(example_item())
191
+ gates = check_report(model)
192
+ self.assertTrue(gates["all_pass"], gates)
193
+
194
+ def test_markdown_gates_all_pass(self):
195
+ md = example_report_markdown()
196
+ gates = check_report_markdown(md)
197
+ self.assertTrue(gates["all_pass"], gates)
198
+
199
+ def test_rendered_has_nine_sections_and_markers(self):
200
+ md = example_report_markdown()
201
+ for n in range(1, 10):
202
+ self.assertIn(f"## {n}. ", md)
203
+ low = md.lower()
204
+ self.assertIn("draft", low)
205
+ self.assertIn("not an approval", low)
206
+
207
+ def test_gates_fail_when_tampered(self):
208
+ model = build_report(example_item())
209
+ model["status"] = "approved"
210
+ self.assertFalse(check_report(model)["sign_off_honest"])
211
+ model2 = build_report(example_item())
212
+ model2["kalman"] = {}
213
+ self.assertFalse(check_report(model2)["kalman_numbers_present"])
214
+
215
+ def test_model_json_round_trip(self):
216
+ import json
217
+ model = build_report(example_item())
218
+ blob = json.dumps(model)
219
+ self.assertIn("kalman", json.loads(blob))
220
+
221
+ def test_standalone_no_skills_repo(self):
222
+ # Core must work with no AeroSkills present.
223
+ os.environ["AEROSKILLS_DEV"] = "/nonexistent"
224
+ model = build_report(example_item())
225
+ md = render_report_markdown(model)
226
+ self.assertGreater(len(md), 3000)
227
+ self.assertTrue(check_report(model)["all_pass"])
228
+
229
+
230
+ if __name__ == "__main__":
231
+ unittest.main()