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,75 @@
1
+ # High-Speed Aerodynamic Analysis Memo
2
+
3
+ **Vehicle:** HSX-1 supersonic cruiser (reference flight condition)
4
+ **Status:** draft-for-review (generated 2026-09-05)
5
+
6
+ ## 1. Flight condition summary
7
+
8
+ - Vehicle: HSX-1 supersonic cruiser (reference flight condition) - Mach 2.0 supersonic transport configuration, subsonic-leading-edge swept wing, mixed-compression intake, high-temperature aluminium airframe
9
+ - Flight condition: cruise M 2.00 at 18288 m; transonic climb check M 0.85 at 12192 m
10
+ - ISA state at cruise: T 216.65 K, p 7172 Pa, rho 0.1153 kg/m3, a 295.07 m/s, mu 1.42e-05 kg/(m s)
11
+ - Flow state: V = M a = 590.1 m/s, q = 20081 Pa
12
+ - Configuration: swept wing (subsonic leading edge, sweep 66 deg), mixed-compression intake (ramp deflection 10.0 deg), equivalent body length 62 m
13
+
14
+ ## 2. Freestream total conditions and Mach-area
15
+
16
+ - Isentropic total-to-static ratios at M 2.00: T0/T = 1.8000, p0/p = 7.8244, rho0/rho = 4.3469
17
+ - Total conditions (isentropic, no shock): p0 = 56114 Pa, T0 = 389.97 K
18
+ - Mach-area: A/A* at M 2.00 = 1.6875 (throat needed for isentropic capture); subsonic-branch root for the same ratio M = 0.3722
19
+ - Use: intake contraction and nozzle expansion sizing (isentropic-flow-relations stage).
20
+
21
+ ## 3. Compressibility corrections and transonic limit
22
+
23
+ - Karman-Tsien corrected peak suction at M 0.70: Cp = -0.55 / (sqrt(1-M^2) + (M^2/(1+sqrt(1-M^2))) x Cp/2) = -0.8654 (vs Prandtl-Glauert -0.7702)
24
+ - Critical pressure coefficient at climb M 0.85: Cp* = -0.3020
25
+ - Section critical Mach from Cp0 = -0.55: M_cr = 0.7017 (Cp* crossing solve)
26
+ - Sweep effect: M_eff = M cos(Lambda): climb M 0.85 x cos(66 deg) = 0.3457; cruise M_eff = 0.8135 (subsonic leading edge)
27
+ - Korn drag-divergence Mach (t/c 0.030, cruise CL 0.12): M_DD = 0.908; transonic wave-drag penalty at climb M 0.85 = 0.0000 (0 below M_DD), parabolic rise 0.0000
28
+ - Terminating-shock strength at local M 1.30: p2/p1 = 1.8050
29
+ - Transonic similarity parameter K = (1 - M^2)/tau^(2/3) = 2.874
30
+
31
+ ## 4. Shock system at the cruise Mach
32
+
33
+ - Normal shock at nose/inlet face, M1 = 2.00: M2 = 0.5774, p2/p1 = 4.5000, T2/T1 = 1.6875, rho2/rho1 = 2.6667, p02/p01 = 0.7209
34
+ - Intake ramp oblique shock, theta = 10.0 deg at M 2.00 (weak): beta = 39.31 deg, M2 = 1.641, p2/p1 = 1.7066, p02/p01 = 0.9846; theta_max = 22.97 deg
35
+ - Terminal normal shock at M2 = 1.641: p02/p01 = 0.8797
36
+ - Intake recovery: 2-shock p02/p01 = 0.8662 vs single normal shock 0.7209 (gain +0.1453)
37
+ - Regular shock reflection at (M 1.641, theta 10.0 deg): verdict mach (required reflected deflection reaches the reflected-shock detachment limit at M2 (or M2 <= 1): Mach reflection)
38
+ - Aft expansion (Prandtl-Meyer), turn 10.0 deg from M 1.641: M = 1.988, p2/p1 = 0.5875
39
+ - Detached bow shock at the nose (sphere, R = 0.050 m): standoff Delta = 0.0161 m (Billig correlation, Delta/R = 0.3215)
40
+
41
+ ## 5. Supersonic section analysis and wave drag
42
+
43
+ - Shock-expansion diamond airfoil at M 2.00, alpha 2.0 deg, half-angle 2.0 deg: cl = 0.0808, cd_wave = 0.00565, cm_le = 0.0387
44
+ - Surface Cp (uf/ur/lf/lr): 0.0000 / -0.0738 / 0.0881 / 0.0000
45
+ - Zero-lift wave drag area (Sears-Haack equivalent body, L = 62 m, r_max = 1.6 m): D/q = (9 pi/2)(A_max/L)^2 = 0.2379 m2
46
+
47
+ ## 6. Boundary layer and skin friction
48
+
49
+ - Station x = 6.0 m at cruise: Re = rho V x / mu = 2.872e+07 (turbulent flat-plate regime)
50
+ - Flat-plate thickness: delta = 0.07157 m, delta* = 0.00895 m, theta = 0.00696 m, H = 1.286
51
+ - Skin friction: Cf_local = 0.00191 (1/7 power), Cf_avg = 0.00239, log-law Cf = 0.00255
52
+ - Transition: Michel/Thwaites flat-plate natural transition at x_tr = 0.349 m (6% of the analysis station)
53
+ - Attachment-line (Hiemenz 2-D) layer at the nose: a = 2U/R = 23606 1/s, delta = 0.000173 m, Cf = 0.00713
54
+ - Surface roughness: k_s = 6.0e-06 m -> k+ = 0.887 (smooth; smooth retained below k+ = 5)
55
+
56
+ ## 7. Aerodynamic heating screen
57
+
58
+ - Stagnation point (Sutton-Graves): q_s = C_SG sqrt(rho/Rn) V^3 = 57119 W/m2; radiation-equilibrium T = 1043 K (eps 0.85)
59
+ - Flat-plate skin: recovery factor r = Pr^(1/3) = 0.8921 (turbulent); adiabatic wall temperature T_aw = T (1 + r (g-1)/2 M^2) = 371.3 K
60
+ - Cold-wall skin flux at x = 6.0 m (Eckert reference temp, T_wall 300 K): q = 3838 W/m2, Cf = 0.00213 at Re* = 1.668e+07
61
+ - Screening margins: skin 51.7 K below the 423 K limit; nose 210 K below the 600 K limit
62
+
63
+ ## 8. Margin summary
64
+
65
+ | Margin | Value | Requirement | Source (stage) |
66
+ |---|---|---|---|
67
+ | Intake total-pressure recovery (2-shock system) | 0.8662 | > single normal-shock recovery 0.7209 at M 2.00 | stage 4: ramp oblique p02/p01 0.98464 x terminal normal p02/p01 0.87972 |
68
+ | Oblique-shock attachment margin (theta_max - theta) | 12.9735 | > 0 deg (attached weak shock; detached above) | stage 4: theta_max 22.97 deg at M 2.00 (oblique-shock leaf) |
69
+ | Section drag-divergence margin at cruise (M_DD - M_eff,cruise) | 0.0945 | > 0 (supercritical section subcritical at the cruise effective Mach M_eff 0.813) | stage 7: Korn rule M_DD = 0.908 (t/c 0.030, CL 0.12, supercritical) vs swept section M_eff = M cos(Lambda) |
70
+ | Section critical-Mach margin at climb (M_cr,section - M_eff,climb) | 0.3560 | > 0 (wing section subcritical through the transonic climb leg, M_eff 0.346) | stage 3: Cp* crossing M_cr = 0.702 for Cp0 = -0.55; climb M_eff = M cos(Lambda) = 0.346 |
71
+ | Skin kinetic-heating margin (T_limit - T_aw) | 51.7291 | > 0 (skin screening limit 423 K vs recovery temp) | stage 10: T_aw = T (1 + r (g-1)/2 M^2) = 371.3 K at M 2.00, recovery r = 0.8921 |
72
+ | Nose stagnation-temperature margin (T_limit - T0) | 210.0300 | > 0 (nose screening limit 600 K vs stagnation-line recovery temperature T0) | stage 10: T0 = T (1 + (g-1)/2 M^2) = 390.0 K at M 2.00 (hot-wall ceiling; Sutton-Graves cold-wall flux 57119 W/m2 screened separately) |
73
+
74
+ ---
75
+ *DRAFT - for human high-speed aerodynamics lead review. Not an approval document.*
@@ -0,0 +1,103 @@
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 core against bound AeroSkills leaf
8
+ logic (isentropic ratios, normal shock, oblique shock, boundary
9
+ layer) when AeroSkills is present
10
+ - standalone (no AeroSkills) emits the bundle honestly with
11
+ cross_checked=false
12
+ """
13
+ import json
14
+ import os
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import unittest
20
+
21
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+ CLI = os.path.join(ROLE_DIR, "cli.py")
23
+
24
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
25
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
26
+
27
+
28
+ def run_build(outdir, env_extra=None, extra_args=None):
29
+ env = dict(os.environ)
30
+ if env_extra:
31
+ env.update(env_extra)
32
+ args = [sys.executable, CLI, "build", "--out",
33
+ os.path.join(outdir, "memo.md"), "--bundle"]
34
+ if extra_args:
35
+ args.extend(extra_args)
36
+ r = subprocess.run(args, capture_output=True, text=True, env=env)
37
+ return r
38
+
39
+
40
+ class TestBundleProtocol(unittest.TestCase):
41
+
42
+ def setUp(self):
43
+ self.tmp = tempfile.mkdtemp()
44
+
45
+ def tearDown(self):
46
+ shutil.rmtree(self.tmp, ignore_errors=True)
47
+
48
+ def test_bundle_emits_three_files(self):
49
+ r = run_build(self.tmp)
50
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
51
+ ev = os.path.join(self.tmp, "evidence")
52
+ for f in ("model.json", "gates.json", "provenance.json"):
53
+ self.assertTrue(os.path.exists(os.path.join(ev, f)), f + " missing")
54
+
55
+ def test_model_has_numbers_and_status(self):
56
+ run_build(self.tmp)
57
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
58
+ self.assertIn("area_ratio", m)
59
+ self.assertIn("shocks", m)
60
+ self.assertIn("recovery_total", m["shocks"])
61
+ self.assertEqual(m.get("status"), "draft-for-review")
62
+ self.assertIn("role", m)
63
+
64
+ def test_gates_all_pass_exit_zero(self):
65
+ r = run_build(self.tmp)
66
+ g = json.load(open(os.path.join(self.tmp, "evidence", "gates.json")))
67
+ self.assertTrue(g["all_pass"])
68
+ self.assertEqual(g["exit_code"], 0)
69
+ self.assertGreaterEqual(len(g["gates"]), 4)
70
+
71
+ def test_dispatch_crosschecks_when_skills_present(self):
72
+ if not HAS_SKILLS:
73
+ self.skipTest("AeroSkills not present")
74
+ r = run_build(self.tmp)
75
+ p = json.load(open(os.path.join(self.tmp, "evidence",
76
+ "provenance.json")))
77
+ self.assertTrue(p["cross_checked"])
78
+ self.assertGreaterEqual(len(p["skills"]), 3)
79
+ for row in p["skills"]:
80
+ self.assertTrue(row["dispatched"])
81
+ self.assertTrue(row["agrees"],
82
+ "core/skill disagree: %s" % row)
83
+ self.assertLessEqual(row["delta"], 1e-4)
84
+
85
+ def test_standalone_honest_no_skills(self):
86
+ r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
87
+ self.assertEqual(r.returncode, 0)
88
+ p = json.load(open(os.path.join(self.tmp, "evidence",
89
+ "provenance.json")))
90
+ self.assertFalse(p["cross_checked"])
91
+ self.assertEqual(len(p["skills"]), 0)
92
+
93
+ def test_check_still_works(self):
94
+ run_build(self.tmp)
95
+ md = os.path.join(self.tmp, "memo.md")
96
+ c = subprocess.run([sys.executable, CLI, "check", "--file", md],
97
+ capture_output=True, text=True)
98
+ self.assertEqual(c.returncode, 0, c.stdout)
99
+ self.assertIn("RESULT: PASS", c.stdout)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env python3
2
+ """Test high_speed_aero_core: the executable engine of the High-Speed
3
+ Aerodynamics role.
4
+
5
+ Proves the role can DO its job standalone (no AeroSkills checkout
6
+ needed): formula correctness against the same known anchors the bound
7
+ Aero Agent Skills leaves verify (isentropic ratios, Mach-area, normal
8
+ and oblique shocks, Prandtl-Meyer, Karman-Tsien, Korn rule, boundary
9
+ layer, heating, standoff), the memo builder, evidence gates, markdown
10
+ completeness, and STANDALONE mode.
11
+ """
12
+ import math
13
+ import os
14
+ import re
15
+ import sys
16
+ import unittest
17
+
18
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
19
+ "..", "core"))
20
+ import high_speed_aero_core as hs
21
+
22
+
23
+ class TestIsentropicAndMachArea(unittest.TestCase):
24
+ """Anchors from the isentropic-flow-relations leaf contract tests."""
25
+
26
+ def test_total_static_ratios_anchor_m2(self):
27
+ r = hs.total_static_ratios(2.0)
28
+ self.assertAlmostEqual(r["t0_over_t"], 1.8, delta=1e-9)
29
+ self.assertAlmostEqual(r["p0_over_p"], 7.8244490669, delta=1e-9)
30
+ self.assertAlmostEqual(r["rho0_over_rho"], 4.3469161483, delta=1e-9)
31
+ sl = hs.total_static_ratios(0.0)
32
+ self.assertEqual(sl["t0_over_t"], 1.0)
33
+
34
+ def test_area_ratio_anchors(self):
35
+ # A/A* at M2 = 1.6875 exactly (leaf test anchor)
36
+ self.assertAlmostEqual(hs.area_ratio(2.0), 1.6875, delta=1e-12)
37
+ self.assertAlmostEqual(hs.area_ratio(0.5), 1.33984375, delta=1e-12)
38
+ self.assertEqual(hs.area_ratio(1.0), 1.0)
39
+ # inverse on both branches round-trips (leaf anchors)
40
+ self.assertAlmostEqual(hs.mach_from_area_ratio(1.6875, subsonic=False),
41
+ 2.0, delta=1e-9)
42
+ self.assertAlmostEqual(hs.mach_from_area_ratio(1.6875, subsonic=True),
43
+ 0.372244486203, delta=1e-9)
44
+ with self.assertRaises(ValueError):
45
+ hs.area_ratio(0.0)
46
+
47
+ def test_static_to_total_and_choked_flow(self):
48
+ # leaf anchor: static 30000 Pa, 200 K, M 2 -> p0 ~ 234733
49
+ tot = hs.static_to_total(30000.0, 200.0, 2.0)
50
+ self.assertAlmostEqual(tot["p0"], 234733.472, delta=1e-2)
51
+ # choked mass flow at the leaf anchor p0 1e5, T0 300, A* 0.01
52
+ self.assertAlmostEqual(hs.choked_mass_flow(100000.0, 300.0, 0.01),
53
+ 2.3335585606, delta=1e-8)
54
+
55
+
56
+ class TestCompressibilityCorrections(unittest.TestCase):
57
+ """Anchors from the transonic-similarity leaf logic."""
58
+
59
+ def test_pg_factor_and_kt_correction(self):
60
+ self.assertAlmostEqual(hs.prandtl_glauert_factor(0.5),
61
+ 1.0 / math.sqrt(0.75))
62
+ # leaf anchor: Karman-Tsien at cp0 -0.6, M 0.5 -> -0.7265391
63
+ self.assertAlmostEqual(hs.karman_tsien_correction(-0.6, 0.5),
64
+ -0.7265391210, delta=1e-9)
65
+ with self.assertRaises(ValueError):
66
+ hs.karman_tsien_correction(-0.6, 0.9) # documented to M ~ 0.85
67
+
68
+ def test_critical_pressure_and_mach(self):
69
+ # Cp* at M 0.8134 is negative; critical Mach from Cp0 -0.55 ~ 0.702
70
+ self.assertAlmostEqual(hs.critical_pressure_coefficient(0.8134),
71
+ -0.3969978925, delta=1e-9)
72
+ self.assertAlmostEqual(hs.critical_mach_number(-0.55),
73
+ 0.7017202167, delta=1e-6)
74
+ with self.assertRaises(ValueError):
75
+ hs.critical_mach_number(0.1)
76
+
77
+ def test_transonic_similarity_and_sweep(self):
78
+ k = hs.transonic_similarity_parameter(0.85, 0.03)
79
+ self.assertAlmostEqual(k, (1 - 0.85 ** 2) / 0.03 ** (2.0 / 3.0))
80
+ self.assertAlmostEqual(hs.effective_mach(0.85, 66.0),
81
+ 0.85 * math.cos(math.radians(66.0)))
82
+ with self.assertRaises(ValueError):
83
+ hs.effective_mach(1.1, 45.0) # subsonic only
84
+
85
+
86
+ class TestShockRelations(unittest.TestCase):
87
+ """Anchors from the normal-shock and oblique-shock leaf tests."""
88
+
89
+ def test_normal_shock_anchor_m2(self):
90
+ # Anderson table A.2 anchor at M1 = 2: M2 0.5774, p2/p1 4.5 ...
91
+ p = hs.normal_shock(2.0)
92
+ self.assertAlmostEqual(p["m2"], 0.5773503, places=6)
93
+ self.assertAlmostEqual(p["p2_p1"], 4.5, places=6)
94
+ self.assertAlmostEqual(p["rho2_rho1"], 2.6666667, places=6)
95
+ self.assertAlmostEqual(p["t2_t1"], 1.6875, places=6)
96
+ self.assertAlmostEqual(p["p02_p01"], 0.7208739, places=6)
97
+ with self.assertRaises(ValueError):
98
+ hs.normal_shock(1.0)
99
+
100
+ def test_oblique_shock_anchor(self):
101
+ # Anderson example 4.2: M1 2, theta 10 deg -> weak beta 39.31,
102
+ # M2 1.64, p2/p1 1.707 (leaf contract anchor)
103
+ p = hs.oblique_shock(2.0, 10.0)
104
+ self.assertAlmostEqual(p["beta_deg"], 39.31393, delta=1e-3)
105
+ self.assertAlmostEqual(p["m2"], 1.64052, delta=1e-3)
106
+ self.assertAlmostEqual(p["p2_p1"], 1.70658, delta=1e-3)
107
+ self.assertAlmostEqual(p["p02_p01"], 0.984644, delta=1e-4)
108
+ self.assertAlmostEqual(hs.deflection_limit(2.0), 22.9735, delta=1e-2)
109
+ self.assertGreater(p["theta_max_deg"], 10.0) # attached
110
+ # detached above theta_max
111
+ with self.assertRaises(ValueError):
112
+ hs.oblique_shock(2.0, 40.0)
113
+
114
+ def test_mach_angle_and_regular_reflection(self):
115
+ self.assertAlmostEqual(hs.mach_angle(2.0), 30.0, delta=1e-9)
116
+ v = hs.shock_reflection_verdict(2.0, 10.0)
117
+ self.assertEqual(v["verdict"], "regular") # leaf anchor
118
+ v2 = hs.shock_reflection_verdict(hs.oblique_shock(2.0, 10.0)["m2"], 10.0)
119
+ self.assertEqual(v2["verdict"], "mach") # leaf anchor @ M2 1.64
120
+
121
+ def test_prandtl_meyer(self):
122
+ # nu(2.0) = 0.460414 rad = 26.3799 deg (leaf anchor, Anderson A.5)
123
+ self.assertAlmostEqual(hs.prandtl_meyer_function(2.0),
124
+ 0.4604136821, delta=1e-9)
125
+ self.assertAlmostEqual(hs.prandtl_meyer_function(1.0), 0.0)
126
+ # expansion from M 1.64 through 10 deg -> ~ M 1.988 (leaf anchor)
127
+ exp = hs.expansion_properties(1.6405222, 10.0)
128
+ self.assertAlmostEqual(exp["m2"], 1.987794, delta=1e-3)
129
+ self.assertLess(exp["pressure_ratio_p2_p1"], 1.0) # expansion drops p
130
+
131
+ def test_shock_expansion_airfoil_anchor(self):
132
+ # diamond airfoil M2, alpha 2, eps 2 (shock-expansion-airfoil leaf)
133
+ r = hs.shock_expansion_airfoil(2.0, 2.0, 2.0)
134
+ self.assertAlmostEqual(r["cl"], 0.0808190, delta=1e-4)
135
+ self.assertAlmostEqual(r["cd_wave"], 0.0056500, delta=1e-4)
136
+ self.assertAlmostEqual(r["cm_le"], 0.0386972, delta=1e-4)
137
+ self.assertGreater(r["cd_wave"], 0.0)
138
+
139
+
140
+ class TestTransonicAndWaveDrag(unittest.TestCase):
141
+ """Anchors from supercritical-airfoil and wave-drag-area-rule leaves."""
142
+
143
+ def test_korn_and_terminating_shock(self):
144
+ self.assertAlmostEqual(hs.drag_divergence_mach(0.03, 0.12, True),
145
+ 0.95 - 0.03 - 0.012)
146
+ self.assertAlmostEqual(hs.drag_divergence_mach(0.035, 0.12, False),
147
+ 0.90 - 0.035 - 0.012)
148
+ # terminating shock strength: p2/p1 at local M 1.3 = 1.805,
149
+ # at 1.15 = 1.37625 (leaf anchors)
150
+ self.assertAlmostEqual(hs.terminating_shock_strength(1.3), 1.805)
151
+ self.assertAlmostEqual(hs.terminating_shock_strength(1.15),
152
+ 1.37625, delta=1e-9)
153
+
154
+ def test_wave_drag_penalty_and_sears_haack(self):
155
+ # penalty index zero below M_DD, (M-M_DD)^3 above (supercritical leaf)
156
+ self.assertEqual(hs.wave_drag_penalty(0.85, 0.908), 0.0)
157
+ self.assertAlmostEqual(hs.wave_drag_penalty(0.95, 0.853),
158
+ 0.097 ** 3, delta=1e-9)
159
+ # Sears-Haack zero-lift wave drag area (area-rule leaf anchor):
160
+ # D/q for L=62, r_max=1.6 -> 0.2378804 m2
161
+ self.assertAlmostEqual(hs.sears_haack_wave_drag_area(62.0, 1.6),
162
+ 0.2378804128, delta=1e-6)
163
+ # parabolic rise above M_DD: k (M-M_DD)^2
164
+ self.assertAlmostEqual(hs.wave_drag_rise_coef(0.95, 0.853, k=20.0),
165
+ 0.18818, delta=1e-6)
166
+
167
+
168
+ class TestBoundaryLayerAndHeating(unittest.TestCase):
169
+ """Anchors from the boundary-layer and heating leaf logic."""
170
+
171
+ def test_flat_plate_thicknesses_laminar(self):
172
+ # Blasius at Re 1e5, x=1: delta 5/sqrt(1e5) = 0.0158 m
173
+ bl = hs.flat_plate_thicknesses(1.0, 1e5)
174
+ self.assertEqual(bl["regime"], "laminar")
175
+ self.assertAlmostEqual(bl["delta"], 5.0 / math.sqrt(1e5))
176
+ self.assertAlmostEqual(bl["delta_star"],
177
+ 1.7208 / math.sqrt(1e5))
178
+ self.assertAlmostEqual(bl["cf_local"], 0.664 / math.sqrt(1e5))
179
+ self.assertAlmostEqual(bl["shape_factor"], 1.7208 / 0.664)
180
+
181
+ def test_flat_plate_thicknesses_turbulent_and_loglaw(self):
182
+ # 1/7-power turbulent at Re 2.866e7, x=6 m (cruise-state anchor)
183
+ bl = hs.flat_plate_thicknesses(6.0, 2.866e7)
184
+ self.assertEqual(bl["regime"], "turbulent")
185
+ self.assertAlmostEqual(bl["delta"], 0.37 * 6.0 / (2.866e7) ** 0.2,
186
+ delta=1e-4)
187
+ self.assertAlmostEqual(bl["shape_factor"], 9.0 / 7.0)
188
+ self.assertAlmostEqual(hs.cf_turbulent_log_law(1e7),
189
+ 0.455 / (7.0 ** 2.58), delta=1e-9)
190
+
191
+ def test_transition_and_roughness(self):
192
+ # flat-plate natural transition at the cruise state ~0.349 m
193
+ x_tr = hs.flat_plate_transition(1.236e-4, 590.0, 6.0)
194
+ self.assertIsNotNone(x_tr)
195
+ self.assertAlmostEqual(x_tr, 0.3489, delta=1e-2)
196
+ # roughness: k+ 0.887 is smooth
197
+ self.assertEqual(hs.roughness_regime(0.887), "smooth")
198
+ self.assertEqual(hs.roughness_regime(20.0), "transitional")
199
+ self.assertEqual(hs.roughness_regime(100.0), "fully-rough")
200
+
201
+ def test_stagnation_flow_layer(self):
202
+ a = hs.stagnation_velocity_gradient("cylinder", 590.0, 0.05)
203
+ self.assertAlmostEqual(a, 2.0 * 590.0 / 0.05)
204
+ self.assertAlmostEqual(hs.stagnation_bl_thickness(1.236e-4, a),
205
+ 2.4 * math.sqrt(1.236e-4 / a), delta=1e-6)
206
+
207
+ def test_heating_and_standoff(self):
208
+ # Sutton-Graves: q at the cruise nose state ~5.7e4 W/m2 (leaf run)
209
+ q = hs.stagnation_heat_flux(0.115, 590.0, 0.05)
210
+ self.assertAlmostEqual(q, 56999.5, delta=1.0)
211
+ t = hs.radiation_equilibrium_temp(q)
212
+ self.assertAlmostEqual(t, 1042.82, delta=0.5)
213
+ # recovery factor turbulent Pr^(1/3), laminar sqrt(Pr)
214
+ self.assertAlmostEqual(hs.recovery_factor("turbulent"),
215
+ 0.71 ** (1.0 / 3.0))
216
+ self.assertAlmostEqual(hs.recovery_factor("laminar"),
217
+ math.sqrt(0.71))
218
+ # adiabatic wall temp at M2, 216.65 K -> 371.27 K (leaf anchor)
219
+ self.assertAlmostEqual(hs.adiabatic_wall_temperature(2.0, 216.65,
220
+ "turbulent"),
221
+ 371.2709, delta=1e-3)
222
+ # Billig sphere standoff at M2: Delta/R = 0.32145
223
+ self.assertAlmostEqual(hs.bow_shock_standoff_ratio(2.0, "sphere"),
224
+ 0.143 * math.exp(3.24 / 4.0), delta=1e-9)
225
+
226
+
227
+ class TestMemoBuilderAndGates(unittest.TestCase):
228
+
229
+ def test_example_memo_numbers(self):
230
+ model = hs.build_memo(hs.example_item())
231
+ # isentropic anchor at M2.0: A/A* = 1.6875 exactly
232
+ self.assertAlmostEqual(model["area_ratio"], 1.6875, delta=1e-9)
233
+ # normal-shock anchor at the cruise Mach: recovery ~0.721
234
+ self.assertAlmostEqual(model["shocks"]["normal"]["p02_p01"],
235
+ 0.720874, delta=1e-4)
236
+ # two-shock intake recovery beats a single normal shock
237
+ self.assertGreater(model["shocks"]["recovery_total"],
238
+ model["shocks"]["recovery_single"])
239
+ # supersonic cruise with a subsonic leading edge (sweep 66 deg)
240
+ self.assertLess(model["corrections"]["m_eff_cruise"], 1.0)
241
+ # drag-divergence margin positive at the cruise effective Mach
242
+ self.assertGreater(model["corrections"]["m_dd"],
243
+ model["corrections"]["m_eff_cruise"])
244
+ # boundary layer real at the cruise station
245
+ self.assertGreater(model["bl"]["re"], 1e6)
246
+ self.assertGreater(model["bl"]["flat_plate"]["delta"], 0.0)
247
+
248
+ def test_core_gates_all_pass(self):
249
+ model = hs.build_memo(hs.example_item())
250
+ gates = hs.check_memo(model)
251
+ self.assertTrue(gates["all_pass"], gates)
252
+ self.assertGreaterEqual(len(model["margins"]), 4)
253
+ for m in model["margins"]:
254
+ self.assertIn("stage ", m["source"])
255
+ self.assertGreater(m["value"], 0.0)
256
+
257
+ def test_memo_markdown_complete_and_no_blanks(self):
258
+ md = hs.example_memo_markdown()
259
+ for n in range(1, 9):
260
+ self.assertTrue(re.search(rf"^## {n}\. ", md, re.M),
261
+ "section %d missing" % n)
262
+ for banned in ["___", "TBD", "OPEN item"]:
263
+ self.assertNotIn(banned, md)
264
+ self.assertIn("DRAFT", md)
265
+ self.assertIn("not an approval", md.lower())
266
+ self.assertIn("HSX-1", md)
267
+ self.assertGreater(len(md), 3000)
268
+ gates = hs.check_memo_markdown(md)
269
+ self.assertTrue(gates["all_pass"], gates)
270
+
271
+ def test_standalone_no_skills_repo(self):
272
+ os.environ["AEROSKILLS_DEV"] = "/nonexistent"
273
+ model = hs.build_memo(hs.example_item())
274
+ md = hs.render_memo_markdown(model)
275
+ self.assertTrue(hs.check_memo(model)["all_pass"])
276
+ self.assertGreater(len(md), 3000)
277
+
278
+
279
+ if __name__ == "__main__":
280
+ unittest.main()
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """Role test: high-speed-aerodynamics-engineer.
3
+
4
+ Verifies (offline, no network):
5
+ 1. Every skill bound in ROLE.md frontmatter RESOLVES to a real leaf in
6
+ the AeroSkills repo (the pinned skills_release band).
7
+ 2. The workflow stage order is deterministic and complete.
8
+ 3. The role is a 100% executable worker: core engine + cli.py exist,
9
+ and the filled deliverable template (generated worked example) has
10
+ zero blanks and carries the gate-check vocabulary.
11
+ 4. Boundaries are present in ROLE.md.
12
+ """
13
+ import os
14
+ import re
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", "high-speed-aerodynamics-engineer")
22
+ TEMPLATE = os.path.join(ROLE_DIR, "templates", "high-speed-memo-template.md")
23
+
24
+ EXPECTED_BOUND = [
25
+ "aerodynamics/high-speed/isentropic-flow-relations",
26
+ "aerodynamics/high-speed/normal-shock",
27
+ "aerodynamics/high-speed/oblique-shock",
28
+ "aerodynamics/high-speed/prandtl-meyer",
29
+ "aerodynamics/high-speed/regular-shock-reflection",
30
+ "aerodynamics/high-speed/shock-expansion-airfoil",
31
+ "aerodynamics/high-speed/transonic-similarity",
32
+ "aerodynamics/high-speed/swept-wing-aerodynamics",
33
+ "aerodynamics/high-speed/supercritical-airfoil",
34
+ "aerodynamics/high-speed/wave-drag-area-rule",
35
+ "aerodynamics/high-speed/bow-shock-standoff",
36
+ "aerodynamics/high-speed/aerodynamic-heating",
37
+ "aerodynamics/high-speed/flat-plate-skin-friction-heating",
38
+ "aerodynamics/high-speed/hypersonic-flow",
39
+ "aerodynamics/boundary-layer/boundary-layer-theory",
40
+ "aerodynamics/boundary-layer/boundary-layer-transition",
41
+ "aerodynamics/boundary-layer/boundary-layer-separation",
42
+ "aerodynamics/boundary-layer/rough-wall-skin-friction",
43
+ "aerodynamics/boundary-layer/stagnation-flow-boundary-layer",
44
+ ]
45
+
46
+ STAGES = [
47
+ "1. Flow-state setup",
48
+ "2. Isentropic relations",
49
+ "3. Compressibility corrections",
50
+ "4. Shock system",
51
+ "5. Expansion",
52
+ "6. Supersonic section",
53
+ "7. Transonic limit",
54
+ "8. Boundary layer",
55
+ "9. BL separation screen",
56
+ "10. Surface state",
57
+ "11. Heating screen",
58
+ "12. Memo",
59
+ ]
60
+
61
+
62
+ def role_text():
63
+ p = os.path.join(ROLE_DIR, "ROLE.md")
64
+ return open(p).read() if os.path.exists(p) else None
65
+
66
+
67
+ class TestHighSpeedAerodynamicsRole(unittest.TestCase):
68
+
69
+ def test_bound_skills_resolve(self):
70
+ if not HAS_SKILLS:
71
+ self.skipTest("skills checkout absent (cross-repo dev check)")
72
+ for leaf in EXPECTED_BOUND:
73
+ self.assertTrue(
74
+ os.path.exists(os.path.join(AEROSKILLS, "skills", leaf,
75
+ "SKILL.md")),
76
+ f"bound skill missing: {leaf}")
77
+
78
+ def test_stages_ordered(self):
79
+ role = role_text()
80
+ self.assertIsNotNone(role)
81
+ wf = role[role.find("## Workflow"):role.find("## Evidence gates")]
82
+ idx = [wf.find(s) for s in STAGES]
83
+ self.assertTrue(all(i >= 0 for i in idx), "stage missing in workflow")
84
+ self.assertEqual(idx, sorted(idx), "stage order not deterministic")
85
+
86
+ def test_template_present(self):
87
+ self.assertTrue(os.path.exists(TEMPLATE), "deliverable template missing")
88
+
89
+ def test_filled_template_zero_blanks(self):
90
+ # ROLE-STANDARD 100% bar: templates hold ONE filled deliverable
91
+ # (the generated worked example). Zero ___ placeholders.
92
+ if not os.path.exists(TEMPLATE):
93
+ self.fail("template missing")
94
+ text = open(TEMPLATE).read()
95
+ self.assertNotIn("___", text, "template still has blanks")
96
+ low = text.lower()
97
+ for phrase in ["high-speed aerodynamic analysis memo", "draft",
98
+ "not an approval", "margin summary"]:
99
+ self.assertIn(phrase, low, f"gate vocabulary missing: {phrase}")
100
+ # real compressible-flow numbers present (M 2.00, p0/p ~ 7.82)
101
+ self.assertRegex(text, r"cruise M 2\.00")
102
+ self.assertRegex(text, r"p0/p = 7\.8244")
103
+
104
+ def test_executable_core_and_cli(self):
105
+ # 100% worker anatomy: executable domain engine + runnable CLI.
106
+ for rel in ("core/high_speed_aero_core.py", "cli.py",
107
+ "tests/test_high_speed_aero_core.py"):
108
+ self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, rel)),
109
+ f"missing executable part: {rel}")
110
+
111
+ def test_boundaries(self):
112
+ role = role_text()
113
+ self.assertIsNotNone(role)
114
+ role = role or ""
115
+ for phrase in ["sign_off_required: true", "forbidden",
116
+ "never", "claim certification approval", "screening"]:
117
+ self.assertIn(phrase, role, f"missing boundary: {phrase}")
118
+
119
+ def test_sources(self):
120
+ self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, "SOURCES.md")),
121
+ "SOURCES.md missing")
122
+
123
+
124
+ if __name__ == "__main__":
125
+ unittest.main()