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,96 @@
1
+ # Loads and Strength Report
2
+
3
+ **Item:** Example regional transport wing
4
+ **Aircraft:** Example 100,000 lb regional transport (FAR 25)
5
+ **Category / basis:** transport category, FAR 25 (Subpart C loads, Subpart D structure)
6
+ **Status:** draft-for-review
7
+
8
+ ## 1. Loads
9
+
10
+ - Wing loading W/S = 100.000 psf, mean geometric chord cbar = 11.180 ft (cbar = S/b), lift-curve slope a = 5.700/rad.
11
+ - Design speeds (EAS): VS = 230.000 ft/s, VA = 363.662 ft/s, VB = 414.000 ft/s, VC = 460.000 ft/s, VD = 621.000 ft/s.
12
+ - Gust alleviation (FAR 25.341(b)(2)): mass ratio mu_g = 41.020, alleviation factor K_g = 0.779.
13
+ - Limit maneuvering load factor at VA = 2.500 (transport category, FAR 25.337); negative limit -1.000 at speeds up to VC.
14
+ - Discrete-gust limit load factors (design gust velocities U_de of FAR 25.341(a) at sea level, fps EAS):
15
+
16
+ - VB (414.000 ft/s, U_de = 66.000 fps): n = 2.443 (positive), -0.443 (negative).
17
+ - VC (460.000 ft/s, U_de = 50.000 fps): n = 2.215 (positive), -0.215 (negative).
18
+ - VD (621.000 ft/s, U_de = 25.000 fps): n = 1.820 (positive), 0.180 (negative).
19
+
20
+ - Gust line vs maneuver envelope at VB/VC/VD: VB margin -0.215 (gust-critical); VC margin -0.416 (gust-critical); VD margin +1.000 (maneuver envelope governs).
21
+
22
+ - **Design limit condition: maneuver at VA, n_limit = +2.500** (negative envelope limit -1.000).
23
+ - **Ultimate loads: n_ult = 1.5 x 2.500 = 3.750 (factor of safety, FAR 25.303).**
24
+
25
+ - Root loads per wing at the design condition (elliptic span-load idealization: root shear = nW/2 and root bending M = nWb/(3 pi), the semi-ellipse lift resultant standing at 4/(3 pi) of the semi-span from the root):
26
+ - Limit root bending moment = 28.47 M in-lb; ultimate root bending M_ult = 42.70 M in-lb (3.56 M ft-lb).
27
+ - Ultimate root shear V_ult = 188 klb per wing.
28
+
29
+ - Ground loads - level landing at the limit vertical inertia factor 2.500 (bound landing-ground-loads leaf):
30
+ - Nose gear reaction 62 klb; main gear reaction 188 klb (total 250 klb).
31
+ - Braked-roll deceleration 0.60 g (braking friction 0.8 on the main gear).
32
+ - Spectra: fatigue and dynamic load spectra feed the fatigue screening (Section 6); random-vibration / shock-response-spectrum conditions for equipment-mounted structure are derived with the bound random-vibration-analysis / shock-response-spectrum leaves when that structure is in scope.
33
+
34
+ ## 2. Global model
35
+
36
+ - Idealization: each wing is a cantilever box beam from the root to the tip carrying the elliptic spanwise lift distribution l(y) = l0 sqrt(1 - (2y/b)^2); root shear and bending then follow in closed form (above), which is sufficient for the screening margins of this report. A refined beam-frame/truss/CalculiX model (bound FEM leaves) confirms station-by-station internal loads and deflections in the detailed phase.
37
+ - Boundary conditions: wing root fixed at the fuselage side of body; box depth h = 40.000 in between spar cap centroids at the root section (example geometry).
38
+ - Internal loads at the root: M_ult = 42.70 M in-lb, V_ult = 188 klb - used for every margin below.
39
+
40
+ ## 3. Strength/stability margins (ultimate load)
41
+
42
+ | Component | Load | Allowable | MS | Basis |
43
+ |---|---|---|---|---|
44
+ | Lower spar cap (tension, root) | P = M_ult/h = 1067611 lb | Ftu = 83.0 ksi | **+0.360** | boom couple at box depth 40 in; 7075-T6 Ftu per bound materials/lug leaf anchors (MMPDS B-basis to confirm) |
45
+ | Upper spar cap (compression, root) | P = M_ult/h = 1067611 lb | Fcy = 73.0 ksi | **+0.196** | compression material limit (yield); local stability of the stiffened panel carried by the skin/stringer rows |
46
+ | Spar web (shear, two webs) | V_ult = 187500 lb | Fsu = 48.0 ksi | **+0.557** | V_ult/(2 t h_web); 7075-T6 Fsu per bound lug leaf anchor (MMPDS to confirm) |
47
+ | Upper skin panel (compression buckling, between stringers) | sigma_app = 61.0 ksi | sigma_cr = 66.7 ksi (k=4, ssss long plate) | **+0.093** | plate buckling sigma_cr = k pi^2 E/(12(1-nu^2)) (t/b)^2 with b = stringer pitch 6.0 in, t = 0.25 in (bound plate-buckling leaf) |
48
+ | Upper stringer (column over rib pitch) | P_app = 73208 lb | Fcy*A = 87545 lb (yield-governed column) | **+0.196** | short column: lambda = 31.1 < lambda_1 = 37.5, yield governs (bound buckling leaf) |
49
+ | Wing root lug (vertical shear, one lug of pair) | P = V_ult/2 = 93750 lb | min over bearing Fbru, net-tension Ftu, tearout Fsu; governing mode tearout | **+2.259** | round-end lug: bearing, net section, tearout margins per bound lug-joint leaf (e/D = 1.50, D/t = 4.00) |
50
+
51
+ ## 4. Dynamics
52
+
53
+ - First wing bending frequency f1 = 2.135 Hz (uniform cantilever idealization over the semi-span with the root box stiffness EI, wing weight fraction applied; beta1*L = 1.875, bound beam-vibration leaf).
54
+ - Frequency placement vs rotor/propeller 1P and control-surface excitation bands is confirmed against the bound modal-analysis / beam-vibration leaves in the detailed phase.
55
+
56
+ ## 5. Materials
57
+
58
+ - Allowables used: 7075-T6 screening values from the bound AeroSkills anchors (material-selection leaf E/Fty/UTS, lug-joint worked example Fsu/Fbru, strain-life 7075-T6 class fatigue constants). MMPDS (licensed) is referenced, never reproduced; B-basis design allowables are confirmed by the materials group before release:
59
+ - E = 10400 ksi; Ftu = 83.0 ksi; Fty = 73.0 ksi; Fsu = 48.0 ksi; Fbru = 152.3 ksi.
60
+ - Ramberg-Osgood / creep / fracture data: not required for the 7075-T6 screening at room temperature; the bound ramberg-osgood / creep-rupture / fracture-toughness leaves apply where those regimes are in scope.
61
+
62
+ ## 6. Fatigue
63
+
64
+ - Critical location: lower spar cap at the root; 1g bending stress sigma_1g = 16.3 ksi.
65
+ - S-N basis: Basquin S = A N^b with A = 100.1 ksi, b = -0.10 (7075-T6 class fatigue strength constants, bound stress-life / strain-life leaves, reference-only). Mean stress is corrected with the modified Goodman rule Sa_eq = Sa / (1 - Sm/Ftu).
66
+ - Spectrum (per-flight cycle blocks at the lower spar cap, amplitude dn about the 1g mean):
67
+
68
+ - 1.00 cycle(s)/flight at dn = 0.80: Sa = 6.5 ksi, Sa_eq = 8.1 ksi, N = 8.34e+10 cycles, damage per flight = 1.20e-11
69
+ - 0.05 cycle(s)/flight at dn = 1.40: Sa = 11.4 ksi, Sa_eq = 14.2 ksi, N = 3.10e+08 cycles, damage per flight = 1.61e-10
70
+
71
+ - GAG cycle (0g to 1g, once per flight): Sa = 8.1 ksi, Sm = 8.1 ksi, Sa_eq = 9.0 ksi, N = 2.83e+10 cycles, damage per flight = 3.53e-11.
72
+ - **Palmgren-Miner cumulative damage sum per flight D = 2.09e-10** (limit 1.0): safe-life screening passes with equivalent life far beyond the design life; production fatigue refines with the counted load spectrum and detail notch factors from the bound load-spectrum-counting / notch-sensitivity / goodman-diagram leaves.
73
+
74
+ ## 7. Damage tolerance
75
+
76
+ - Safe-life screening (Section 6) is the scope of this example report; the damage tolerance evaluation per FAR 25.571 (crack growth, residual strength, inspection intervals) is executed with the bound crack-growth / residual-strength / widespread-fatigue-damage leaves once the production spectrum and crack-growth data are baselined.
77
+ - Bird strike (FAR 25.631) is a design load case for the leading edge and windshield supports; the bound bird-strike leaf applies when that structure is in scope.
78
+
79
+ ## 8. Composites / thermal
80
+
81
+ - The example item is metallic (7075-T6); laminate first-ply-failure and sandwich margins from the bound laminate-stiffness / laminate-first-ply-failure / failure-criteria / sandwich-panels leaves apply when composite structure enters the design.
82
+ - Thermal stress and thermal buckling margins are computed with the bound thermal-stress-analysis / thermal-buckling leaves for thermal-critical structure; no thermal-critical metallic margin is driven in this room-temperature screening.
83
+
84
+ ## 9. Margin summary
85
+
86
+ | Component | MS | Status |
87
+ |---|---|---|
88
+ | Lower spar cap (tension, root) | +0.360 | PASS |
89
+ | Upper spar cap (compression, root) | +0.196 | PASS |
90
+ | Spar web (shear, two webs) | +0.557 | PASS |
91
+ | Upper skin panel (compression buckling, between stringers) | +0.093 | PASS |
92
+ | Upper stringer (column over rib pitch) | +0.196 | PASS |
93
+ | Wing root lug (vertical shear, one lug of pair) | +2.259 | PASS |
94
+
95
+ ---
96
+ *Generated by Aero Agent Roles structures-loads-engineer core (2026-09-05). DRAFT for human stress lead review. Not an approval document - no certification approval is claimed, no compliance finding is declared. FEM results are as good as the model idealizations stated above.*
@@ -0,0 +1,101 @@
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 gust load factor against the core
8
+ (agrees within tolerance) when AeroSkills is present
9
+ - standalone (no AeroSkills) emits the bundle honestly with
10
+ cross_checked=false
11
+ """
12
+ import json
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ import tempfile
18
+ import unittest
19
+
20
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
+ CLI = os.path.join(ROLE_DIR, "cli.py")
22
+
23
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
24
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
25
+
26
+
27
+ def run_build(outdir, env_extra=None, extra_args=None):
28
+ env = dict(os.environ)
29
+ if env_extra:
30
+ env.update(env_extra)
31
+ args = [sys.executable, CLI, "build", "--out",
32
+ os.path.join(outdir, "report.md"), "--bundle"]
33
+ if extra_args:
34
+ args.extend(extra_args)
35
+ r = subprocess.run(args, capture_output=True, text=True, env=env)
36
+ return r
37
+
38
+
39
+ class TestBundleProtocol(unittest.TestCase):
40
+
41
+ def setUp(self):
42
+ self.tmp = tempfile.mkdtemp()
43
+
44
+ def tearDown(self):
45
+ shutil.rmtree(self.tmp, ignore_errors=True)
46
+
47
+ def test_bundle_emits_three_files(self):
48
+ r = run_build(self.tmp)
49
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
50
+ ev = os.path.join(self.tmp, "evidence")
51
+ for f in ("model.json", "gates.json", "provenance.json"):
52
+ self.assertTrue(os.path.exists(os.path.join(ev, f)), f + " missing")
53
+
54
+ def test_model_has_numbers_and_status(self):
55
+ run_build(self.tmp)
56
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
57
+ self.assertIn("n_ult", m)
58
+ self.assertIn("vn", m)
59
+ self.assertEqual(m.get("status"), "draft-for-review")
60
+ self.assertIn("role", m)
61
+
62
+ def test_gates_all_pass_exit_zero(self):
63
+ r = run_build(self.tmp)
64
+ g = json.load(open(os.path.join(self.tmp, "evidence", "gates.json")))
65
+ self.assertTrue(g["all_pass"])
66
+ self.assertEqual(g["exit_code"], 0)
67
+ self.assertGreaterEqual(len(g["gates"]), 3)
68
+
69
+ def test_dispatch_crosschecks_when_skills_present(self):
70
+ if not HAS_SKILLS:
71
+ self.skipTest("AeroSkills not present")
72
+ r = run_build(self.tmp)
73
+ p = json.load(open(os.path.join(self.tmp, "evidence",
74
+ "provenance.json")))
75
+ self.assertTrue(p["cross_checked"])
76
+ self.assertEqual(len(p["skills"]), 1)
77
+ row = p["skills"][0]
78
+ self.assertTrue(row["dispatched"])
79
+ self.assertTrue(row["agrees"], "core/skill gust disagree: %s" % row)
80
+ self.assertLessEqual(row["delta"], 1e-6)
81
+ self.assertIn("gust_load_factor", row["function"])
82
+
83
+ def test_standalone_honest_no_skills(self):
84
+ r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
85
+ self.assertEqual(r.returncode, 0)
86
+ p = json.load(open(os.path.join(self.tmp, "evidence",
87
+ "provenance.json")))
88
+ self.assertFalse(p["cross_checked"])
89
+ self.assertEqual(len(p["skills"]), 0)
90
+
91
+ def test_check_still_works(self):
92
+ run_build(self.tmp)
93
+ md = os.path.join(self.tmp, "report.md")
94
+ c = subprocess.run([sys.executable, CLI, "check", "--file", md],
95
+ capture_output=True, text=True)
96
+ self.assertEqual(c.returncode, 0, c.stdout)
97
+ self.assertIn("RESULT: PASS", c.stdout)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ unittest.main()
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ """Role test: structures-loads-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
+ # Bound-skill resolution requires the Aero Agent Skills checkout. It is a
20
+ # cross-repo dev check: when the skills repo is absent (fresh public clone),
21
+ # skip resolution rather than fail - the role's bound list is verified in CI.
22
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
23
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
24
+ ROLE_DIR = os.path.join(ROLES_REPO, "roles", "structures-loads-engineer")
25
+ TEMPLATE = os.path.join(ROLE_DIR, "templates",
26
+ "loads-strength-report-template.md")
27
+
28
+ EXPECTED_BOUND = [
29
+ "structures/loads/gust-maneuver-loads",
30
+ "structures/loads/landing-ground-loads",
31
+ "structures/loads/random-vibration-analysis",
32
+ "structures/loads/shock-response-spectrum",
33
+ "structures/fem/beam-frame-analysis",
34
+ "structures/fem/truss-analysis",
35
+ "structures/fem/calculix-linear",
36
+ "structures/fem/calculix-nonlinear",
37
+ "structures/fem/buckling-analysis",
38
+ "structures/fem/plate-buckling",
39
+ "structures/fem/cylindrical-shell-buckling",
40
+ "structures/fem/modal-analysis",
41
+ "structures/fem/beam-vibration",
42
+ "structures/fem/lug-joint-analysis",
43
+ "structures/fem/contact-analysis",
44
+ "structures/fem/pressure-bulkhead",
45
+ "structures/materials/material-selection",
46
+ "structures/materials/mmpsd-allowables",
47
+ "structures/materials/ramberg-osgood",
48
+ "structures/materials/creep-rupture",
49
+ "structures/materials/fracture-toughness",
50
+ "structures/fatigue/goodman-diagram",
51
+ "structures/fatigue/stress-life-curve",
52
+ "structures/fatigue/strain-life-fatigue",
53
+ "structures/fatigue/miner-damage",
54
+ "structures/fatigue/notch-sensitivity",
55
+ "structures/fatigue/load-spectrum-counting",
56
+ "structures/damage-tolerance/crack-growth",
57
+ "structures/damage-tolerance/residual-strength",
58
+ "structures/damage-tolerance/bird-strike",
59
+ "structures/damage-tolerance/widespread-fatigue-damage",
60
+ "structures/composites/laminate-stiffness",
61
+ "structures/composites/laminate-first-ply-failure",
62
+ "structures/composites/failure-criteria",
63
+ "structures/composites/sandwich-panels",
64
+ "structures/composites/composite-repair",
65
+ "structures/thermal-structures/thermal-stress-analysis",
66
+ "structures/thermal-structures/thermal-buckling"
67
+ ]
68
+
69
+ STAGES = [
70
+ "1. Design loads",
71
+ "2. Global FEM",
72
+ "3. Nonlinear/buckling",
73
+ "4. Detail joints",
74
+ "5. Dynamics",
75
+ "6. Materials",
76
+ "7. Fatigue",
77
+ "8. Damage tolerance",
78
+ "9. Composites",
79
+ "10. Thermal",
80
+ "11. Report",
81
+ ]
82
+
83
+
84
+ def role_text():
85
+ p = os.path.join(ROLE_DIR, "ROLE.md")
86
+ return open(p).read() if os.path.exists(p) else None
87
+
88
+
89
+ class TestStructuresLoadsEngineerRole(unittest.TestCase):
90
+
91
+ def test_bound_skills_resolve(self):
92
+ if not HAS_SKILLS:
93
+ self.skipTest("skills checkout absent (cross-repo dev check)")
94
+ for leaf in EXPECTED_BOUND:
95
+ self.assertTrue(
96
+ os.path.exists(os.path.join(AEROSKILLS, "skills", leaf,
97
+ "SKILL.md")),
98
+ f"bound skill missing: {leaf}")
99
+
100
+ def test_stages_ordered(self):
101
+ role = role_text()
102
+ self.assertIsNotNone(role)
103
+ wf = role[role.find("## Workflow"):role.find("## Evidence gates")]
104
+ idx = [wf.find(s) for s in STAGES]
105
+ self.assertTrue(all(i >= 0 for i in idx), "stage missing in workflow")
106
+ self.assertEqual(idx, sorted(idx), "stage order not deterministic")
107
+
108
+ def test_template_present(self):
109
+ self.assertTrue(os.path.exists(TEMPLATE), "deliverable template missing")
110
+
111
+ def test_filled_template_zero_blanks(self):
112
+ # ROLE-STANDARD 100% bar: templates hold ONE filled deliverable
113
+ # (the generated worked example). Zero ___ placeholders.
114
+ if not os.path.exists(TEMPLATE):
115
+ self.fail("template missing")
116
+ text = open(TEMPLATE).read()
117
+ self.assertNotIn("___", text, "template still has blanks")
118
+ low = text.lower()
119
+ # gate-check vocabulary must exist in the filled deliverable
120
+ for phrase in ["loads and strength report", "margins", "draft",
121
+ "not an approval"]:
122
+ self.assertIn(phrase, low, f"gate vocabulary missing: {phrase}")
123
+ # a real margin-of-safety number is present (MS like +0.xxx)
124
+ self.assertRegex(text, r"\*\*[+-]\d\.\d{3}\*\*")
125
+
126
+ def test_executable_core_and_cli(self):
127
+ # 100% worker anatomy: executable domain engine + runnable CLI.
128
+ for rel in ("core/structures_loads_core.py", "cli.py",
129
+ "tests/test_structures_loads_core.py"):
130
+ self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, rel)),
131
+ f"missing executable part: {rel}")
132
+
133
+ def test_boundaries(self):
134
+ role = role_text()
135
+ self.assertIsNotNone(role)
136
+ role = role or ""
137
+ for phrase in ["sign_off_required: true", "forbidden",
138
+ "never invent allowables", "not an approval"]:
139
+ self.assertIn(phrase, role, f"missing boundary: {phrase}")
140
+
141
+ def test_sources(self):
142
+ self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, "SOURCES.md")),
143
+ "SOURCES.md missing")
144
+
145
+
146
+ if __name__ == "__main__":
147
+ unittest.main()
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ """Test structures_loads_core: the executable engine of the Structures
3
+ and Loads Engineer role.
4
+
5
+ Proves the role can DO its job standalone (no AeroSkills needed): FAR 25
6
+ gust + maneuver envelope, limit/ultimate loads, root internal loads,
7
+ margins of safety per component, fatigue screening (S-N basis + Miner),
8
+ dynamics, evidence gates, and the worked-example report. Anchor values
9
+ are taken from the bound AeroSkills leaves' own behavior contracts
10
+ (gust load factor ~2.3-2.4 band at VB, lug margins +1.800/+1.135/+0.926
11
+ governing tearout, Basquin life 1.69e5 cycles at S=300/A=1000/b=-0.1).
12
+ """
13
+ import os
14
+ import sys
15
+ import unittest
16
+
17
+ sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
18
+ "..", "core"))
19
+ from structures_loads_core import (
20
+ CANTILEVER_BETA1_L, MATERIALS, NEGATIVE_MANEUVER_LIMIT,
21
+ basquin_life, build_report, cantilever_frequency_hz, check_report,
22
+ check_report_markdown, column_check, cumulative_damage,
23
+ design_limit_load_factor, effective_length_factor, envelope_margins,
24
+ euler_buckling_load, example_item, example_report_markdown,
25
+ far25_gust_velocity, goodman_effective_amplitude, gust_alleviation_factor,
26
+ gust_load_factor, gust_mass_ratio, level_landing_reactions,
27
+ lug_analysis, maneuver_limit_load_factor, margin_of_safety,
28
+ plate_buckling_stress, render_report_markdown, shear_buckling_stress,
29
+ vn_diagram,
30
+ )
31
+
32
+ KSI_MPA = 6.894757
33
+
34
+
35
+ class TestGustManeuverDomain(unittest.TestCase):
36
+ """Domain rules mirroring the bound gust-maneuver-loads leaf."""
37
+
38
+ def test_far25_gust_velocities(self):
39
+ self.assertAlmostEqual(far25_gust_velocity("vb-vc", 0.0), 66.0)
40
+ self.assertAlmostEqual(far25_gust_velocity("vc", 0.0), 50.0)
41
+ self.assertAlmostEqual(far25_gust_velocity("vd", 0.0), 25.0)
42
+ self.assertAlmostEqual(far25_gust_velocity("vc", 15000.0), 25.0)
43
+ self.assertAlmostEqual(far25_gust_velocity("vd", 50000.0), 12.5)
44
+
45
+ def test_alleviation_anchor(self):
46
+ # Transport anchor: W/S=100 psf, cbar=11.18 ft, a=5.7 -> mu_g ~41,
47
+ # K_g ~0.78 (same band as the leaf's cbar=12.5 anchor of 36.7/0.769).
48
+ mu = gust_mass_ratio(100.0, 11.18, 5.7)
49
+ kg = gust_alleviation_factor(100.0, 11.18, 5.7)
50
+ self.assertAlmostEqual(mu, 41.02, delta=0.5)
51
+ self.assertAlmostEqual(kg, 0.779, delta=0.02)
52
+ self.assertLess(kg, 0.88)
53
+
54
+ def test_gust_load_factor_band(self):
55
+ # 66 fps design gust at VB on the example wing: n in the 2.3-2.5 band.
56
+ n = gust_load_factor(414.0, 100.0, 5.7, 66.0, cbar=11.18)
57
+ self.assertGreater(n, 2.3)
58
+ self.assertLess(n, 2.6)
59
+ vn = vn_diagram(100.0, 230.0, 621.0, 5.7, 11.18,
60
+ weight_lb=100_000.0)
61
+ n_vb = [p["n_pos"] for p in vn["gust_points"] if p["speed"] == "VB"][0]
62
+ self.assertAlmostEqual(n_vb, n, delta=0.01)
63
+
64
+ def test_maneuver_factor_far337(self):
65
+ # FAR 25.337(b): n = max(2.5, min(3.8, 2.1+24000/(W+10000))).
66
+ self.assertAlmostEqual(maneuver_limit_load_factor(100_000.0,
67
+ "transport"), 2.5)
68
+ self.assertAlmostEqual(maneuver_limit_load_factor(3_000.0,
69
+ "transport"), 3.8)
70
+ self.assertEqual(maneuver_limit_load_factor(50_000.0, "normal"), 2.5)
71
+ self.assertEqual(maneuver_limit_load_factor(50_000.0, "commuter"), 3.8)
72
+ self.assertEqual(maneuver_limit_load_factor(50_000.0, "transport",
73
+ negative=True),
74
+ NEGATIVE_MANEUVER_LIMIT)
75
+
76
+ def test_vn_speed_ordering_and_corner(self):
77
+ vn = vn_diagram(100.0, 230.0, 621.0, 5.7, 11.18,
78
+ weight_lb=100_000.0)
79
+ self.assertTrue(vn["vs"] < vn["va"] < vn["vb"] < vn["vc"] < vn["vd"])
80
+ import math
81
+ self.assertAlmostEqual(vn["va"], 230.0 * math.sqrt(2.5), places=3)
82
+ self.assertEqual(len(vn["gust_points"]), 3)
83
+
84
+ def test_design_limit_and_gust_critical(self):
85
+ vn = vn_diagram(100.0, 230.0, 621.0, 5.7, 11.18,
86
+ weight_lb=100_000.0)
87
+ design = design_limit_load_factor(vn)
88
+ self.assertAlmostEqual(design["n_limit_pos"], 2.5)
89
+ em = envelope_margins(vn)
90
+ # gust-criticality is checked at least somewhere on the envelope
91
+ self.assertTrue(any(v["gust_critical_pos"] or v["gust_critical_neg"]
92
+ for v in em.values())
93
+ or all(v["margin_pos"] >= 0 for v in em.values()))
94
+
95
+
96
+ class TestStrengthDomain(unittest.TestCase):
97
+ """Strength rules mirroring the bound buckling / plate-buckling /
98
+ lug-joint leaves."""
99
+
100
+ def test_margin_of_safety(self):
101
+ self.assertEqual(margin_of_safety(100.0, 80.0), 0.25)
102
+ self.assertEqual(margin_of_safety(100.0, 100.0), 0.0)
103
+ self.assertEqual(margin_of_safety(100.0, 200.0), -0.5)
104
+ with self.assertRaises(ValueError):
105
+ margin_of_safety(0.0, 1.0)
106
+
107
+ def test_euler_anchor(self):
108
+ # Euler scaling anchor: doubling K quarters Pcr (leaf behavior).
109
+ pcr2 = euler_buckling_load(29_000.0, 0.6, 22.0)
110
+ pcr_half = euler_buckling_load(29_000.0, 0.6, 22.0, "fixed-free")
111
+ self.assertAlmostEqual(pcr_half / pcr2, 0.25, places=4)
112
+
113
+ def test_end_conditions(self):
114
+ self.assertEqual(effective_length_factor("pinned-pinned"), 1.0)
115
+ self.assertEqual(effective_length_factor("fixed-fixed"), 0.5)
116
+ self.assertEqual(effective_length_factor("fixed-pinned"), 0.7)
117
+ self.assertEqual(effective_length_factor("cantilever"), 2.0)
118
+
119
+ def test_column_check_transition(self):
120
+ # Stocky column: yield-governed; slender column: Euler-governed.
121
+ stocky = column_check(10_400.0, 0.60, 1.2, 8.0, "pinned-pinned",
122
+ 40_000.0, 73.0)
123
+ self.assertFalse(stocky["euler_governs"])
124
+ slender = column_check(10_400.0, 0.60, 1.2, 60.0, "pinned-pinned",
125
+ 40_000.0, 73.0)
126
+ self.assertTrue(slender["euler_governs"])
127
+
128
+ def test_plate_buckling(self):
129
+ # k=4 long plate anchor: E=10,400 ksi, nu=0.33, t=0.25, b=6
130
+ # -> sigma_cr ~ 66.7 ksi (matches the worked example wing panel).
131
+ sig = plate_buckling_stress(10_400.0, 0.33, 0.25, 6.0, k=4.0)
132
+ self.assertAlmostEqual(sig, 66.7, delta=1.0)
133
+ # clamped long plate k=6.97 is stiffer than ssss k=4
134
+ sig_c = plate_buckling_stress(10_400.0, 0.33, 0.25, 6.0, k=6.97)
135
+ self.assertGreater(sig_c, sig)
136
+
137
+ def test_lug_worked_example_anchor(self):
138
+ # Bound lug leaf worked example, converted to US customary:
139
+ # 7075-T6 D=20mm,t=12mm,e=24mm,w=48mm,P=90kN with Ftu=572,
140
+ # Fsu=331, Fbru=1050 MPa -> margins +1.800/+1.135/+0.926, tearout
141
+ # governs, passes.
142
+ d_in = 20.0 / 25.4
143
+ t_in = 12.0 / 25.4
144
+ e_in = 24.0 / 25.4
145
+ w_in = 48.0 / 25.4
146
+ p_lb = 90_000.0 / 4.448222
147
+ lug = lug_analysis(p_lb, d_in, t_in, w_in, e_in,
148
+ 572.0 / KSI_MPA, 331.0 / KSI_MPA,
149
+ 1050.0 / KSI_MPA)
150
+ self.assertAlmostEqual(lug["bearing_margin"], 1.800, delta=0.02)
151
+ self.assertAlmostEqual(lug["net_tension_margin"], 1.135, delta=0.02)
152
+ self.assertAlmostEqual(lug["tearout_margin"], 0.926, delta=0.02)
153
+ self.assertEqual(lug["governing_mode"], "tearout")
154
+ self.assertTrue(lug["passes"])
155
+
156
+
157
+ class TestFatigueDomain(unittest.TestCase):
158
+ """Fatigue rules mirroring the bound stress-life / goodman / miner
159
+ leaves."""
160
+
161
+ def test_basquin_anchor(self):
162
+ # stress-life leaf anchor: A=1000, b=-0.1, S=300 -> N ~1.69e5.
163
+ n = basquin_life(300.0, 1000.0, -0.1)
164
+ self.assertAlmostEqual(n, 1.6935e5, delta=0.05e5)
165
+
166
+ def test_goodman_effective_amplitude(self):
167
+ self.assertAlmostEqual(goodman_effective_amplitude(10.0, 0.0, 83.0),
168
+ 10.0)
169
+ # A tensile mean stress raises the equivalent fully-reversed
170
+ # amplitude (same damage at zero mean needs a bigger amplitude).
171
+ self.assertLess(goodman_effective_amplitude(10.0, 0.0, 83.0),
172
+ goodman_effective_amplitude(10.0, 30.0, 83.0))
173
+ self.assertAlmostEqual(goodman_effective_amplitude(10.0, 30.0, 83.0),
174
+ 10.0 / (1.0 - 30.0 / 83.0), places=6)
175
+ with self.assertRaises(ValueError):
176
+ goodman_effective_amplitude(10.0, 90.0, 83.0)
177
+
178
+ def test_miner_sum(self):
179
+ self.assertEqual(cumulative_damage([(100, 200)]), 0.5)
180
+ d = cumulative_damage([(500, 1000), (300, 600)])
181
+ self.assertEqual(d, 1.0)
182
+ with self.assertRaises(ValueError):
183
+ cumulative_damage([])
184
+ with self.assertRaises(ValueError):
185
+ cumulative_damage([(10, 0)])
186
+
187
+ def test_fatigue_report_fields(self):
188
+ model = build_report(example_item())
189
+ f = model["fatigue"]
190
+ self.assertIn("damage_per_flight", f)
191
+ self.assertGreater(f["damage_per_flight"], 0.0)
192
+ self.assertLess(f["damage_per_flight"], 1.0)
193
+ self.assertTrue(f["gust_blocks"])
194
+
195
+
196
+ class TestBuilderAndGates(unittest.TestCase):
197
+
198
+ def test_example_report_model(self):
199
+ model = build_report(example_item())
200
+ self.assertEqual(model["document_type"], "Loads and Strength Report")
201
+ self.assertEqual(model["status"], "draft-for-review")
202
+ self.assertGreaterEqual(len(model["margins"]), 5)
203
+ # the example wing must pass: every margin of safety >= 0
204
+ for m in model["margins"]:
205
+ self.assertGreaterEqual(m["MS"], 0.0,
206
+ "example margin negative: %s" % m)
207
+ for key in ("component", "load", "allowable", "basis"):
208
+ self.assertTrue(m[key], "missing %s in margin row" % key)
209
+
210
+ def test_ground_loads(self):
211
+ g = level_landing_reactions(100_000.0, 18.0, 6.0, 2.5)
212
+ self.assertAlmostEqual(g["total_lb"], 250_000.0, places=1)
213
+
214
+ def test_dynamics_frequency_sane(self):
215
+ model = build_report(example_item())
216
+ # regional transport wing first bending: ~1-4 Hz
217
+ self.assertGreater(model["dynamics_f1_hz"], 1.0)
218
+ self.assertLess(model["dynamics_f1_hz"], 4.0)
219
+ self.assertAlmostEqual(CANTILEVER_BETA1_L, 1.87510407, places=5)
220
+
221
+ def test_core_gates_all_pass(self):
222
+ model = build_report(example_item())
223
+ gates = check_report(model)
224
+ self.assertTrue(gates["all_pass"], gates)
225
+
226
+ def test_markdown_gates_all_pass(self):
227
+ md = example_report_markdown()
228
+ gates = check_report_markdown(md)
229
+ self.assertTrue(gates["all_pass"], gates)
230
+
231
+ def test_rendered_report_structure(self):
232
+ md = example_report_markdown()
233
+ for sec in ["## 1. Loads", "## 3. Strength/stability margins",
234
+ "## 6. Fatigue", "## 9. Margin summary"]:
235
+ self.assertIn(sec, md)
236
+ self.assertIn("not an approval", md.lower())
237
+ # no template blanks left in the worked example
238
+ self.assertNotIn("___", md)
239
+
240
+ def test_standalone_no_skills_repo(self):
241
+ # Core must work with no AeroSkills present.
242
+ os.environ["AEROSKILLS_DEV"] = "/nonexistent"
243
+ model = build_report(example_item())
244
+ self.assertGreaterEqual(len(model["margins"]), 5)
245
+ md = render_report_markdown(model)
246
+ self.assertGreater(len(md), 2000)
247
+ self.assertTrue(check_report(model)["all_pass"])
248
+ self.assertTrue(check_report_markdown(md)["all_pass"])
249
+
250
+ def test_material_allowables_grounded(self):
251
+ # The allowables used in the example report must be the leaf
252
+ # anchors (572/503/331/1050 MPa -> 83.0/73.0/48.0/152.3 ksi).
253
+ mat = MATERIALS["al-7075-t6"]
254
+ self.assertAlmostEqual(mat["Ftu_ksi"], 572.0 / KSI_MPA, places=2)
255
+ self.assertAlmostEqual(mat["Fty_ksi"], 503.0 / KSI_MPA, places=2)
256
+ self.assertAlmostEqual(mat["Fsu_ksi"], 331.0 / KSI_MPA, places=2)
257
+ self.assertAlmostEqual(mat["Fbru_ksi"], 1050.0 / KSI_MPA, places=2)
258
+
259
+
260
+ if __name__ == "__main__":
261
+ unittest.main()