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,120 @@
1
+ # Numerical Methods Verification Memo
2
+
3
+ **Code under verification:** Loads Post-Processor (LPP) (version v2.3.1)
4
+ **Verification basis:** classical numerical analysis (Richardson 1910/1927; Euler-Maclaurin error analysis; Newton/Simpson/Gauss); ASME V&V 20-2009 vocabulary for discretization error
5
+ **Status:** draft-for-review
6
+
7
+ ## 1. Scope and method selection
8
+
9
+ The LPP loads post-processor turns the spanwise load distribution into the half-span resultant used by the loads report, inverts the isentropic area-Mach relation for duct sizing, and solves the 2x2 aerodynamic coefficient system A c = b. This memo independently verifies the numerical methods of the item above: the spanwise-load integral, the area-Mach root inversion, and the loads coefficient solve are each recomputed with reference methods and compared against the code's reported values and claimed tolerances. A three-mesh convergence study and a conditioning report bound the discretization and rounding error of the results.
10
+
11
+ | Operation | Code method | Reference method | Why |
12
+ |---|---|---|---|
13
+ | spanwise load intensity w(y) = w0*(1 - (2y/span)^2), half-span integral | composite trapezoid, n=16 panels | composite Simpson | Composite Simpson error scales with h^4 and the rule is exact for polynomials of degree <= 3, beating trapezoid on smooth integrands at moderate panel counts. |
14
+ | area-Mach inversion (A/A* = 1.2, subsonic) | code-internal Newton solver | Newton-Raphson (quadratic, simple root) with bisection bracket cross-check | Bisection guarantees convergence when the bracket straddles zero (linear); Newton gives quadratic convergence from a nearby guess when the derivative is non-zero; agreement of the two independent iterations cross-validates the root. |
15
+ | loads coefficient solve A c = b | code-internal dense solver | Gaussian elimination with partial pivoting | Partial pivoting bounds element growth and avoids zero pivots; the reference residual A x - b is checked at machine precision, and the conditioning report bounds how much backward error the solve amplifies. |
16
+
17
+ ## 2. Reference computations
18
+
19
+ ### 2.1 Reference integral
20
+
21
+ Integrand: spanwise load intensity w(y) = w0*(1 - (2y/span)^2), half-span integral on [0.000000, 18.000000] m (peak load 4200 N/m, span 36 m).
22
+ - Reference value (composite Simpson): 50400.000000 N
23
+ - Closed-form anchor w0*span/3: 50400.000000 N (the integrand is a quadratic, so Simpson is exact here)
24
+ - Richardson error estimate at the code's panel count: the trapezoid error scales with h^2, giving error(2n) = |I_2n - I_n|/3 = 12.304688 N.
25
+
26
+ ### 2.2 Reference root
27
+
28
+ - isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch: M* = 0.590248761
29
+ - Reference method: reference Newton-Raphson from x0 = 0.3 + bisection bracket cross-check - converged in 6 iterations, residual |f| = 2.22e-16
30
+ - Derivative at the root f'(M*) = -1.238449 (non-zero: simple root, quadratic convergence expected); root sensitivity per unit residual 1/|f'| = 0.807462
31
+ - Bracket [0.200, 0.990] straddles zero: True
32
+ - Bisection cross-check: M = 0.590248761, agreement with Newton 8.17e-12
33
+
34
+ ### 2.3 Reference linear solve
35
+
36
+ - Reference solution c = A^-1 b: 0.285714286, 0.285714286
37
+ - Condition number kappa = s_max/s_min: 7.0000
38
+ - Conditioning assessment: kappa = 7: up to 0.85 decimal digits of input uncertainty can be amplified by the solve
39
+
40
+ ## 3. Cross-check against the engineering code
41
+
42
+ Each operation: code-reported value vs reference value; verdict PASS when the discrepancy is within the code's own claimed tolerance.
43
+
44
+ ### 3.1 Integration
45
+
46
+ - Operation: spanwise load integral
47
+ - Code method: composite trapezoid, n=16 panels
48
+ - Reference method: composite Simpson, n=64 (exact for degree <= 3) + Richardson error estimate
49
+ - Code-reported value: 50350.781250000 N
50
+ - Reference value (composite Simpson, n=64): 50400.000000000 N
51
+ - Discrepancy: 49.218750000 N
52
+ - Code-claimed tolerance: 1.000 N
53
+ - Richardson estimate of the code's n-panel error: 49.218750 N (from the code's own n and 2n outputs)
54
+ - **Verdict: FAIL**
55
+ - Method fingerprint: the reference engine's trapezoid at the code's panel count reproduces the code output (50350.781250000) - the code's method was identified correctly.
56
+
57
+ ### 3.2 Root finding
58
+
59
+ - Operation: isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch
60
+ - Code method: code-internal Newton solver
61
+ - Reference method: reference Newton-Raphson from x0 = 0.3 + bisection bracket cross-check
62
+ - Code-reported value: M = 0.590248761
63
+ - Reference value: M* = 0.590248760988 (residual |f| = 2.22e-16)
64
+ - Bisection cross-check: M = 0.590248761 (agreement with Newton 8.17e-12)
65
+ - Discrepancy: 1.15e-11
66
+ - Code-claimed tolerance: 1.00e-08
67
+ - **Verdict: PASS**
68
+
69
+ ### 3.3 Linear solve
70
+
71
+ - Operation: loads coefficient solve A c = b
72
+ - Code method: code-internal dense solver
73
+ - Reference method: Gaussian elimination with partial pivoting (reference engine)
74
+ - Code-reported solution: [0.285714000, 0.285714000]
75
+ - Reference solution: [0.285714286, 0.285714286]
76
+ - Discrepancy (max component): 2.86e-07
77
+ - Code-claimed tolerance: 1.00e-06
78
+ - **Verdict: PASS**
79
+ - Condition number kappa = s_max/s_min: 7.0000
80
+ - Conditioning: kappa = 7: up to 0.85 decimal digits of input uncertainty can be amplified by the solve
81
+
82
+ ## 4. Convergence and error analysis
83
+
84
+ Three-mesh study of the code's own outputs (refinement ratio r = 2, finest f1 to coarsest f3):
85
+
86
+ - 64: 50396.923828
87
+ - 32: 50387.695312
88
+ - 16: 50350.781250
89
+ - Observed order p = 2.000 (consistent with composite trapezoid (order 2))
90
+ - Richardson extrapolated value: 50400.000000
91
+ - Grid convergence index (GCI, Fs = 1.25): 7.63e-05 fraction (0.0076%)
92
+ - Verdict: monotone converged
93
+ - Code-reported value vs finest mesh: 46.142578 N
94
+
95
+ ## 5. Findings and recommendations
96
+
97
+ ### F-1 (Discrepancy)
98
+
99
+ spanwise load integral reports 50350.781250 N while the reference integration gives 50400.000000 N: discrepancy 49.218750 N exceeds the code's claimed tolerance of 1 N.
100
+
101
+ Evidence: Richardson error estimate of the code's n-panel output is 49.218750 N (measured discrepancy 49.218750 N); the three-mesh study shows observed order 2.00, so the code's panel count cannot support the claimed tolerance.
102
+
103
+ Recommendation: Refine to n >= 64 panels or switch to a composite Simpson rule, and gate the output with a Richardson error estimate before release.
104
+
105
+ ### O-1 (Observation)
106
+
107
+ root-finding (isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch) verified within the code's claimed tolerance (discrepancy 1.1549e-11 <= 1e-08).
108
+
109
+ ### O-2 (Observation)
110
+
111
+ linear solve (loads coefficient solve A c = b) verified within the code's claimed tolerance (discrepancy 2.85714e-07 <= 1e-06).
112
+
113
+ ## 6. Verification limits
114
+
115
+ - Scope: the three numerical operations named above, on the example item's input set. Other code paths (ODE integration, interpolation, least-squares fitting, finite-difference derivatives, uncertainty propagation) follow the same reference-engine workflow when exercised.
116
+ - The code's claimed tolerances are inputs to this memo, not endorsements; a PASS verdict means the claim is supported by the reference computation on this input set.
117
+ - Export classification: uncontrolled technical data (example).
118
+
119
+ ---
120
+ *Generated by Aero Agent Roles numerical-analysis-engineer core (2026-09-05). DRAFT for human numerical-analysis review. Not an approval document.*
@@ -0,0 +1,157 @@
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 the computed reference numbers,
7
+ gates all_pass
8
+ - the ten dispatch rows agree with the bound numerics leaf logic
9
+ modules (delta <= 1e-6) when AeroSkills is present
10
+ - standalone (no AeroSkills) emits the bundle honestly with
11
+ cross_checked=false
12
+ - build --profile prepends a program context header
13
+ - CLI overrides re-verify a different code report
14
+ - check gate-checks a built memo
15
+ """
16
+ import json
17
+ import os
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import unittest
23
+
24
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
25
+ CLI = os.path.join(ROLE_DIR, "cli.py")
26
+ PROFILE = os.path.normpath(os.path.join(ROLE_DIR, "..", "..", "profiles",
27
+ "example-airframer.json"))
28
+
29
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
30
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
31
+
32
+ EXPECTED_LEAVES = {
33
+ "numerical-integration", "root-finding", "convergence-verification",
34
+ "singular-value-decomposition", "matrix-operations",
35
+ }
36
+ EXPECTED_FUNCS = {"trapezoid", "simpson", "error_estimate_trapezoid",
37
+ "newton_raphson", "bisection", "condition_number", "solve"}
38
+
39
+
40
+ def run_build(outdir, env_extra=None, extra_args=None):
41
+ env = dict(os.environ)
42
+ if env_extra:
43
+ env.update(env_extra)
44
+ args = [sys.executable, CLI, "build", "--out",
45
+ os.path.join(outdir, "memo.md"), "--bundle"]
46
+ if extra_args:
47
+ args.extend(extra_args)
48
+ r = subprocess.run(args, capture_output=True, text=True, env=env)
49
+ return r
50
+
51
+
52
+ class TestBundleProtocol(unittest.TestCase):
53
+
54
+ def setUp(self):
55
+ self.tmp = tempfile.mkdtemp()
56
+
57
+ def tearDown(self):
58
+ shutil.rmtree(self.tmp, ignore_errors=True)
59
+
60
+ def test_bundle_emits_three_files(self):
61
+ r = run_build(self.tmp)
62
+ self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
63
+ ev = os.path.join(self.tmp, "evidence")
64
+ for f in ("model.json", "gates.json", "provenance.json"):
65
+ self.assertTrue(os.path.exists(os.path.join(ev, f)), f + " missing")
66
+
67
+ def test_model_has_numbers_and_status(self):
68
+ run_build(self.tmp)
69
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
70
+ self.assertEqual(m["role"], "numerical-analysis-engineer")
71
+ self.assertEqual(m["deliverable_type"],
72
+ "Numerical Methods Verification Memo")
73
+ self.assertEqual(m.get("status"), "draft-for-review")
74
+ # the model must carry the numbers that appear in the markdown
75
+ self.assertAlmostEqual(m["integration"]["code_value"], 50350.78125,
76
+ places=6)
77
+ self.assertAlmostEqual(m["integration"]["reference_value"], 50400.0,
78
+ places=6)
79
+ self.assertAlmostEqual(
80
+ m["root_finding"]["reference_root"], 0.59024876099, places=9)
81
+ self.assertAlmostEqual(m["convergence"]["observed_order"], 2.0,
82
+ places=6)
83
+ self.assertAlmostEqual(m["linear_solve"]["conditioning"]["kappa"],
84
+ 7.0, places=6)
85
+
86
+ def test_gates_all_pass_exit_zero(self):
87
+ r = run_build(self.tmp)
88
+ self.assertEqual(r.returncode, 0)
89
+ g = json.load(open(os.path.join(self.tmp, "evidence", "gates.json")))
90
+ self.assertTrue(g["all_pass"])
91
+ self.assertEqual(g["exit_code"], 0)
92
+ self.assertGreaterEqual(len(g["gates"]), 5)
93
+
94
+ def test_dispatch_crosschecks_when_skills_present(self):
95
+ if not HAS_SKILLS:
96
+ self.skipTest("AeroSkills not present")
97
+ r = run_build(self.tmp)
98
+ self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
99
+ p = json.load(open(os.path.join(self.tmp, "evidence",
100
+ "provenance.json")))
101
+ self.assertTrue(p["cross_checked"])
102
+ rows = [row for row in p["skills"] if row.get("dispatched")
103
+ and not row.get("error")]
104
+ self.assertGreaterEqual(len(rows), 10)
105
+ leafs = {row["leaf"].rsplit("/", 1)[-1] for row in rows}
106
+ self.assertEqual(leafs, EXPECTED_LEAVES)
107
+ funcs = {row["function"] for row in rows}
108
+ self.assertTrue(EXPECTED_FUNCS.issubset(funcs), funcs)
109
+ for row in rows:
110
+ self.assertTrue(row["agrees"],
111
+ "core/skill disagree for %s: %s"
112
+ % (row["function"], row))
113
+ self.assertLessEqual(row["delta"], 1e-6)
114
+ self.assertIn("cross-cutting/numerics/", row["leaf"])
115
+
116
+ def test_standalone_honest_no_skills(self):
117
+ r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
118
+ self.assertEqual(r.returncode, 0)
119
+ p = json.load(open(os.path.join(self.tmp, "evidence",
120
+ "provenance.json")))
121
+ self.assertFalse(p["cross_checked"])
122
+ self.assertEqual(len(p["skills"]), 0)
123
+
124
+ def test_profile_header_applied(self):
125
+ if not os.path.exists(PROFILE):
126
+ self.skipTest("profiles/example-airframer.json not present")
127
+ r = run_build(self.tmp, extra_args=["--profile", PROFILE])
128
+ self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
129
+ md = open(os.path.join(self.tmp, "memo.md")).read()
130
+ self.assertIn("Example Airframers Ltd", md)
131
+ self.assertIn("Program context (profile)", md)
132
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
133
+ self.assertEqual(m["profile"]["customer"], "Example Airframers Ltd")
134
+
135
+ def test_cli_overrides_flip_verdict(self):
136
+ # re-verify a code report that reports the correct integral:
137
+ # the integration check must now PASS
138
+ r = run_build(self.tmp, extra_args=["--code-integral", "50400.0"])
139
+ self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
140
+ m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
141
+ self.assertEqual(m["integration"]["verdict"], "PASS")
142
+ self.assertTrue(all(c["verdict"] == "PASS" for c in m["checks"]))
143
+ # findings become observations, not discrepancies
144
+ self.assertTrue(all(fd["severity"] == "observation"
145
+ for fd in m["findings"]))
146
+
147
+ def test_check_still_works(self):
148
+ run_build(self.tmp)
149
+ md = os.path.join(self.tmp, "memo.md")
150
+ c = subprocess.run([sys.executable, CLI, "check", "--file", md],
151
+ capture_output=True, text=True)
152
+ self.assertEqual(c.returncode, 0, c.stdout)
153
+ self.assertIn("RESULT: PASS", c.stdout)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ unittest.main()
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ """Test numerical_analysis_core: the executable engine of the
3
+ Numerical Analysis Engineer role.
4
+
5
+ Proves the role can DO its job standalone (no AeroSkills needed):
6
+ reference quadrature + Richardson error estimation, Newton/bisection
7
+ root finding with the area-Mach anchor, pivoted reference solves,
8
+ condition numbers, the three-mesh convergence study (observed order,
9
+ Richardson extrapolation, GCI), the memo builder, evidence-gate checks,
10
+ and the filled template.
11
+ """
12
+ import math
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 numerical_analysis_core import ( # noqa: E402
20
+ EPS_MACH, GCI_FS, RULE_SELECTION,
21
+ area_mach_derivative, area_mach_ratio, area_mach_residual, bisection,
22
+ bracket_straddles, build_report, check_report, check_report_markdown,
23
+ condition_number, condition_report, convergence_verdict,
24
+ example_item, example_report_markdown, forward_error_bound,
25
+ grid_convergence_index, lift_intensity, max_abs_component_diff,
26
+ newton_iterations, newton_raphson, observed_order,
27
+ render_report_markdown, richardson_error_n, richardson_error_trapezoid,
28
+ richardson_extrapolation, root_sensitivity, select_integration_rule,
29
+ simpson, solve, trapezoid,
30
+ )
31
+
32
+ # Deterministic across midnight: the committed template carries the
33
+ # generation date 2026-09-05; pin fresh renders to it (CI override wins).
34
+ os.environ.setdefault("ROLE_GEN_DATE", "2026-09-05")
35
+
36
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
37
+ TEMPLATE = os.path.join(ROLE_DIR, "templates",
38
+ "verification-memo-template.md")
39
+
40
+
41
+ class TestQuadratureRules(unittest.TestCase):
42
+
43
+ def test_trapezoid_linear_exact(self):
44
+ # trapezoid is exact for linears: integral of x on [0, 2] = 2
45
+ self.assertAlmostEqual(trapezoid(lambda x: x, 0.0, 2.0, 4), 2.0,
46
+ places=12)
47
+
48
+ def test_quadratic_anchor_x2(self):
49
+ # integral of x^2 on [0, 2] = 8/3; trapezoid converges O(h^2)
50
+ est = trapezoid(lambda x: x * x, 0.0, 2.0, 1000)
51
+ self.assertAlmostEqual(est, 8.0 / 3.0, places=4)
52
+ # Simpson n=2 is exact for the quadratic
53
+ self.assertAlmostEqual(simpson(lambda x: x * x, 0.0, 2.0, 2),
54
+ 8.0 / 3.0, places=12)
55
+
56
+ def test_sin_anchor(self):
57
+ # integral of sin(x) on [0, pi] = 2 (leaf textbook anchor)
58
+ self.assertAlmostEqual(simpson(math.sin, 0.0, math.pi, 1000), 2.0,
59
+ places=8)
60
+
61
+ def test_invalid_panels_raise(self):
62
+ with self.assertRaises(ValueError):
63
+ trapezoid(lambda x: x, 0.0, 1.0, 0)
64
+ with self.assertRaises(ValueError):
65
+ simpson(lambda x: x, 0.0, 1.0, 3) # odd n raises
66
+ with self.assertRaises(ValueError):
67
+ trapezoid(lambda x: x, 0.0, 1.0, 2.5)
68
+
69
+ def test_richardson_error_estimate(self):
70
+ # error(2n) = |I_2n - I_n| / 3 ; on the smooth quadratic
71
+ # half-span load at n=16 this is 12.3046875 N (measured)
72
+ f = lambda y: lift_intensity(y, 4200.0, 36.0)
73
+ self.assertAlmostEqual(
74
+ richardson_error_trapezoid(f, 0.0, 18.0, 16), 12.3046875,
75
+ places=9)
76
+
77
+ def test_richardson_error_n_recovers_trapezoid_error(self):
78
+ # for an order-2 rule E_n = |I_n - I_2n|/(1 - 2^-2); on the
79
+ # quadratic integrand the trapezoid error is exactly known:
80
+ # I16 - exact = -49.21875 N
81
+ f = lambda y: lift_intensity(y, 4200.0, 36.0)
82
+ i16 = trapezoid(f, 0.0, 18.0, 16)
83
+ i32 = trapezoid(f, 0.0, 18.0, 32)
84
+ self.assertAlmostEqual(richardson_error_n(i16, i32, 2),
85
+ abs(i16 - 4200.0 * 36.0 / 3.0), places=6)
86
+ self.assertAlmostEqual(richardson_error_n(i16, i32, 2), 49.21875,
87
+ places=6)
88
+
89
+
90
+ class TestRuleSelection(unittest.TestCase):
91
+
92
+ def test_smooth_selects_simpson(self):
93
+ sel = select_integration_rule("smooth")
94
+ self.assertEqual(sel["rule"], "composite Simpson")
95
+ self.assertEqual(sel["order"], 4)
96
+
97
+ def test_noisy_selects_trapezoid(self):
98
+ sel = select_integration_rule("arbitrary-or-noisy")
99
+ self.assertIn("trapezoid", sel["rule"])
100
+ self.assertEqual(sel["order"], 2)
101
+
102
+ def test_all_classes_have_rationale(self):
103
+ for cls, sel in RULE_SELECTION.items():
104
+ self.assertTrue(sel["rationale"])
105
+
106
+ def test_unknown_class_raises(self):
107
+ with self.assertRaises(ValueError):
108
+ select_integration_rule("not-a-class")
109
+
110
+
111
+ class TestRootFinding(unittest.TestCase):
112
+
113
+ def test_sqrt2_anchors(self):
114
+ # f = x^2 - 2, root sqrt(2) = 1.4142135623730951
115
+ root_b = bisection(lambda x: x * x - 2.0, 1.0, 2.0)
116
+ root_n = newton_raphson(lambda x: x * x - 2.0, lambda x: 2.0 * x,
117
+ 1.5)
118
+ self.assertAlmostEqual(root_b, math.sqrt(2.0), places=9)
119
+ self.assertAlmostEqual(root_n, math.sqrt(2.0), places=9)
120
+
121
+ def test_newton_quadratic_speed(self):
122
+ # three Newton steps reach ~machine precision from x0 = 1.5
123
+ r = newton_iterations(lambda x: x * x - 2.0, lambda x: 2.0 * x, 1.5)
124
+ self.assertLessEqual(r["iterations"], 3)
125
+ self.assertLess(r["residual"], 1e-10)
126
+
127
+ def test_area_mach_subsonic_anchor(self):
128
+ # isentropic area-Mach relation, A/A* = 1.2, gamma = 1.4:
129
+ # subsonic root M = 0.59024876099 (leaf-tested anchor)
130
+ resid = lambda M: area_mach_residual(M, 1.2, 1.4)
131
+ deriv = lambda M: area_mach_derivative(M, 1.4)
132
+ rn = newton_raphson(resid, deriv, 0.3)
133
+ rb = bisection(resid, 0.2, 0.99)
134
+ self.assertAlmostEqual(rn, 0.59024876099, places=9)
135
+ self.assertAlmostEqual(rb, 0.59024876099, places=9)
136
+ self.assertLess(abs(area_mach_residual(rn, 1.2, 1.4)), 1e-10)
137
+
138
+ def test_bracket_behavior(self):
139
+ resid = lambda M: area_mach_residual(M, 1.2, 1.4)
140
+ ok = bracket_straddles(resid, 0.2, 0.99)
141
+ self.assertTrue(ok["straddles"])
142
+ bad = bracket_straddles(resid, 0.05, 0.4) # both sides positive
143
+ self.assertFalse(bad["straddles"])
144
+ with self.assertRaises(ValueError):
145
+ bisection(resid, 0.05, 0.4) # no sign change
146
+ # Newton step undefined when the derivative vanishes before
147
+ # convergence: f = x^3 - 1 from x0 = 0 has df(0) = 0
148
+ with self.assertRaises(ValueError):
149
+ newton_raphson(lambda x: x ** 3 - 1.0, lambda x: 3.0 * x * x, 0.0)
150
+
151
+ def test_root_sensitivity(self):
152
+ # simple root: sensitivity 1/|f'|; multiple root: unbounded
153
+ self.assertAlmostEqual(root_sensitivity(-2.0), 0.5)
154
+ self.assertEqual(root_sensitivity(0.0), float("inf"))
155
+
156
+
157
+ class TestLinearSolveAndConditioning(unittest.TestCase):
158
+
159
+ def test_solve_leaf_anchor(self):
160
+ # matrix-operations leaf worked anchor:
161
+ # A x = b with A = [[2,1,-1],[-3,-1,2],[-2,1,2]], b = [8,-11,-3]
162
+ # gives x = [2, 3, -1]
163
+ A = [[2.0, 1.0, -1.0], [-3.0, -1.0, 2.0], [-2.0, 1.0, 2.0]]
164
+ b = [8.0, -11.0, -3.0]
165
+ x = solve(A, b)
166
+ self.assertAlmostEqual(x[0], 2.0, places=10)
167
+ self.assertAlmostEqual(x[1], 3.0, places=10)
168
+ self.assertAlmostEqual(x[2], -1.0, places=10)
169
+
170
+ def test_singular_matrix_raises(self):
171
+ with self.assertRaises(ValueError):
172
+ solve([[1.0, 2.0], [2.0, 4.0]], [1.0, 2.0])
173
+
174
+ def test_max_abs_component_diff(self):
175
+ self.assertAlmostEqual(max_abs_component_diff([1.0, 2.0],
176
+ [1.0, 2.5]), 0.5)
177
+
178
+ def test_condition_number(self):
179
+ # kappa = s_max/s_min: [1.75, 0.25] -> 7
180
+ self.assertAlmostEqual(condition_number([1.75, 0.25]), 7.0)
181
+ self.assertEqual(condition_number([1.0, 0.0]), float("inf"))
182
+ with self.assertRaises(ValueError):
183
+ condition_number([])
184
+ with self.assertRaises(ValueError):
185
+ condition_number([-1.0])
186
+
187
+ def test_condition_report_and_bound(self):
188
+ rep = condition_report([1.75, 0.25])
189
+ self.assertAlmostEqual(rep["kappa"], 7.0)
190
+ self.assertAlmostEqual(rep["digits_lost_max"],
191
+ math.log10(7.0), places=3)
192
+ self.assertFalse(rep["at_precision_limit"])
193
+ # classical forward-error bound: kappa * backward error
194
+ self.assertAlmostEqual(forward_error_bound(7.0, 1e-12), 7e-12)
195
+ # at the precision limit kappa >= 1/eps
196
+ lim = condition_report([EPS_MACH, 1.0])
197
+ self.assertTrue(lim["at_precision_limit"])
198
+
199
+
200
+ class TestConvergenceStudy(unittest.TestCase):
201
+
202
+ # three-mesh trapezoid outputs of the half-span quadratic load
203
+ F1, F2, F3 = 50396.923828125, 50387.6953125, 50350.78125
204
+
205
+ def test_observed_order_is_two(self):
206
+ # trapezoid on a quadratic: exactly second order, ratio 4
207
+ p = observed_order(self.F1, self.F2, self.F3, 2.0)
208
+ self.assertAlmostEqual(p, 2.0, places=9)
209
+
210
+ def test_richardson_extrapolation_recovers_exact(self):
211
+ p = observed_order(self.F1, self.F2, self.F3, 2.0)
212
+ exact = richardson_extrapolation(self.F1, self.F2, 2.0, p)
213
+ self.assertAlmostEqual(exact, 50400.0, places=6)
214
+
215
+ def test_gci_fraction(self):
216
+ p = observed_order(self.F1, self.F2, self.F3, 2.0)
217
+ gci = grid_convergence_index(self.F1, self.F2, 2.0, p)
218
+ self.assertAlmostEqual(gci, 7.62986022096e-05, places=12)
219
+ self.assertEqual(GCI_FS, 1.25)
220
+
221
+ def test_convergence_verdicts(self):
222
+ v = convergence_verdict(self.F1, self.F2, self.F3, 2.0)
223
+ self.assertEqual(v["verdict"], "monotone converged")
224
+ self.assertAlmostEqual(v["order"], 2.0, places=9)
225
+ # diverging: |ratio| > 1 on the negative branch
226
+ vd = convergence_verdict(1.0, 0.5, 2.0, 2.0)
227
+ self.assertEqual(vd["verdict"], "diverging")
228
+ # oscillatory: ratio < 0 with |ratio| <= 1
229
+ vo = convergence_verdict(1.0, 0.5, 0.75, 2.0)
230
+ self.assertEqual(vo["verdict"], "oscillatory")
231
+ self.assertIsNone(vo["order"])
232
+
233
+ def test_invalid_studies_raise(self):
234
+ with self.assertRaises(ValueError):
235
+ observed_order(1.0, 2.0, 3.0, 1.0) # r <= 1
236
+ with self.assertRaises(ValueError):
237
+ observed_order(1.0, 1.0, 1.5, 2.0) # f2 == f1
238
+ with self.assertRaises(ValueError):
239
+ observed_order(1.0, 0.5, 0.6, 2.0) # non-monotone
240
+
241
+
242
+ class TestMemoBuilder(unittest.TestCase):
243
+
244
+ def test_example_item_reports(self):
245
+ item = example_item()
246
+ # code reported the n=16 trapezoid value (50350.78125 N) and the
247
+ # subsonic area-Mach root to 9 decimals
248
+ self.assertAlmostEqual(item.code_integral, 50350.78125, places=6)
249
+ self.assertAlmostEqual(item.code_mach, 0.590248761, places=9)
250
+
251
+ def test_model_structure(self):
252
+ model = build_report(example_item())
253
+ self.assertEqual(model["document_type"],
254
+ "Numerical Methods Verification Memo")
255
+ self.assertEqual(model["status"], "draft-for-review")
256
+ self.assertEqual(len(model["checks"]), 3)
257
+ self.assertEqual(len(model["method_selection"]), 3)
258
+ self.assertTrue(model["findings"])
259
+
260
+ def test_example_verdicts(self):
261
+ # integration fails (49.2 N vs 1 N claim); root and solve pass
262
+ int_chk = build_report(example_item())["integration"]
263
+ self.assertEqual(int_chk["verdict"], "FAIL")
264
+ self.assertAlmostEqual(int_chk["discrepancy"], 49.21875, places=6)
265
+ root_chk = build_report(example_item())["root_finding"]
266
+ self.assertEqual(root_chk["verdict"], "PASS")
267
+ self.assertLess(root_chk["discrepancy"], 1e-9)
268
+ solve_chk = build_report(example_item())["linear_solve"]
269
+ self.assertEqual(solve_chk["verdict"], "PASS")
270
+ self.assertAlmostEqual(solve_chk["conditioning"]["kappa"], 7.0)
271
+
272
+ def test_convergence_in_model(self):
273
+ conv = build_report(example_item())["convergence"]
274
+ self.assertAlmostEqual(conv["observed_order"], 2.0, places=6)
275
+ self.assertAlmostEqual(conv["gci_fraction"], 7.62986022096e-05,
276
+ places=12)
277
+ self.assertEqual(conv["verdict"], "monotone converged")
278
+ self.assertAlmostEqual(conv["richardson_extrapolated"], 50400.0,
279
+ places=6)
280
+
281
+ def test_correct_report_flips_verdict(self):
282
+ # re-verify a code report that is actually correct: all PASS
283
+ item = example_item()
284
+ item.code_integral = 50400.0
285
+ model = build_report(item)
286
+ for c in model["checks"]:
287
+ self.assertEqual(c["verdict"], "PASS")
288
+
289
+ def test_core_gates_all_pass(self):
290
+ model = build_report(example_item())
291
+ gates = check_report(model)
292
+ self.assertTrue(gates["all_pass"], gates)
293
+
294
+ def test_markdown_gates_all_pass(self):
295
+ md = example_report_markdown()
296
+ gates = check_report_markdown(md)
297
+ self.assertTrue(gates["all_pass"], gates)
298
+
299
+ def test_sign_off_gate_fails_when_honest_marker_removed(self):
300
+ model = build_report(example_item())
301
+ model["status"] = "approved"
302
+ gates = check_report(model)
303
+ self.assertFalse(gates["sign_off_honest"])
304
+ self.assertFalse(gates["all_pass"])
305
+
306
+ def test_convergence_gate_fails_when_study_missing(self):
307
+ model = build_report(example_item())
308
+ model["convergence"] = {"meshes": []}
309
+ gates = check_report(model)
310
+ self.assertFalse(gates["convergence_study_present"])
311
+ self.assertFalse(gates["all_pass"])
312
+
313
+ def test_render_is_draft_and_not_approval(self):
314
+ md = example_report_markdown()
315
+ self.assertIn("draft", md.lower())
316
+ self.assertIn("not an approval", md.lower())
317
+ self.assertIn("Numerical Methods Verification Memo", md)
318
+
319
+ def test_template_is_the_filled_example(self):
320
+ # ROLE-STANDARD: the template IS the example deliverable
321
+ # generated by the core - byte-identical, zero blanks.
322
+ with open(TEMPLATE) as fh:
323
+ template = fh.read()
324
+ self.assertNotIn("___", template)
325
+ self.assertEqual(template, example_report_markdown())
326
+
327
+ def test_standalone_no_skills_repo(self):
328
+ # Core must work with no AeroSkills present
329
+ os.environ["AEROSKILLS_DEV"] = "/nonexistent"
330
+ model = build_report(example_item())
331
+ md = render_report_markdown(model)
332
+ self.assertGreater(len(md), 2000)
333
+ self.assertTrue(check_report(model)["all_pass"])
334
+
335
+
336
+ if __name__ == "__main__":
337
+ unittest.main()