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,109 @@
1
+ # Nondestructive Test Plan and Method Selection Report
2
+
3
+ **Item:** Main Gearbox Housing (investment casting) (PN 7450-103)
4
+ **Material:** A356 aluminum alloy casting (non-ferromagnetic, 30 pct IACS) **Route:** investment cast + T6
5
+ **Drawing:** DWG 7450-103 rev C
6
+ **Certification basis:** AS9100D special-process control; NAS 410 personnel qualification practice
7
+ **Status:** draft-for-review
8
+
9
+ Scope: Critical cast aluminum gearbox housing; the casting is fully machined after NDT and holds oil under pressure in service.
10
+
11
+ ## 1. Candidate defect population and method selection
12
+
13
+ Method selection follows the bound ndt-method-selection decision logic: defect class (surface / near-surface / internal) x material class (non-ferromagnetic) defines the applicable set, and sensitivity rank (UT 5, RT/ET 4, MT/PT 3) picks the method (cost is reporting only).
14
+
15
+ ### 1.1 Fatigue crack at oil-feed boss
16
+
17
+ - Zone / note: surface-breaking crack zone on the machined boss face (non-ferromagnetic aluminum)
18
+ - Defect class: surface · material class: non-ferromagnetic
19
+ - Applicable methods: eddy current testing (ET) (sensitivity 4), liquid penetrant testing (PT) (sensitivity 3)
20
+ - **Selected method: eddy current testing (ET)** — surface defect in non-ferromagnetic material: ET is the highest-sensitivity applicable method.
21
+ - Alternates: liquid penetrant testing (PT)
22
+
23
+ ### 1.2 Subsurface gas porosity under machined skin
24
+
25
+ - Zone / note: near-surface gas porosity band beneath the machined skin
26
+ - Defect class: near-surface · material class: non-ferromagnetic
27
+ - Applicable methods: eddy current testing (ET) (sensitivity 4), ultrasonic testing (UT) (sensitivity 5)
28
+ - **Selected method: ultrasonic testing (UT)** — near-surface defect in non-ferromagnetic material: UT is the highest-sensitivity applicable method.
29
+ - Alternates: eddy current testing (ET)
30
+
31
+ ### 1.3 Internal shrinkage porosity in lug section
32
+
33
+ - Zone / note: internal shrinkage/gas porosity population in the thick lug section
34
+ - Defect class: internal · material class: non-ferromagnetic
35
+ - Applicable methods: radiography (RT) (sensitivity 4), ultrasonic testing (UT) (sensitivity 5)
36
+ - **Selected method: ultrasonic testing (UT)** — internal defect in non-ferromagnetic material: UT is the highest-sensitivity applicable method.
37
+ - Alternates: radiography (RT)
38
+
39
+ ## 2. Inspection parameters per selected method
40
+
41
+ _Parameter rows below are computed by the role core mirroring the bound method leaf logic. The qualified procedure and the engineering specification govern the actual inspection settings._
42
+
43
+ ### 2.1 Eddy current (ET) — surface zone
44
+ - Flaw depth: 0.001 m; conductivity 30 pct IACS (1.74e+07 S/m)
45
+ - Test frequency (penetration factor 0.5): **58231 Hz**; standard depth of penetration **0.0005 m**
46
+ - Current density at the flaw depth: 0.135 of surface; phase lag 114.59 deg
47
+
48
+ ### 2.2 Eddy current (ET) — near-surface zone
49
+ - Subsurface flaw depth 0.001 m: frequency **3639 Hz**, standard depth of penetration 0.002 m keeps the flaw at half a delta (density ratio 0.607, phase lag 28.65 deg)
50
+
51
+ ### 2.3 Liquid penetrant (PT) — surface-breaking crack zone
52
+ - Penetrant: gamma 0.032 N/m, contact angle 5 deg, viscosity 0.008 Pa.s; crack opening 4e-06 m -> effective capillary radius 2e-06 m
53
+ - Capillary pressure across the meniscus: **3.19e+04 Pa**
54
+ - Washburn model: penetration depth 0.03458 m at the 300 s reference dwell; computed time to fill the 0.001 m-deep reference crack 0.251 s (dwell scales with depth squared and inversely with crack radius)
55
+ - Indication sizing: bleed-out width 1.6e-05 m for the 4e-06 m opening (ratio 4.0); developer 0.075 kg at 0.15 kg/m2 over 0.50 m2; fluorescent contrast vs background 0.938
56
+ - Dwell: procedure-qualified penetrant dwell governs; the model numbers above are sizing inputs, not settings.
57
+
58
+ ### 2.4 Radiography (RT) — internal volumetric zone
59
+ - Setup: focal spot 3.0 mm, SOD 500 mm, ODD 30 mm -> geometric unsharpness **0.18 mm** (limit 0.25 mm)
60
+ - Exposure (inverse-square): **1.39 min** at the working distance
61
+ - IQI sensitivity: 2.00 pct (0.6 mm visible on 30 mm section; limit 2.0 pct)
62
+ - Film density 2.50 in band 2.0-4.0 (acceptable); technique verdict: **acceptable**
63
+ - Expected discontinuity class from image geometry: porosity (acceptance criteria per class)
64
+
65
+ ### 2.5 Computed tomography (CT) — supplemental volumetric sizing
66
+ - Magnification 2.00, voxel size 0.0001 m; required flaw 0.0005 m
67
+ - Resolution: PASS: smallest detectable feature 3.000e-04 m (3 voxels) is at or below the required 5.000e-04 m flaw size
68
+ - Scan plan: 1609 projections at 350 kV (161 s), material aluminum thickness 50 mm
69
+ - Porosity ROI: 0.8 pct void fraction, equivalent spherical void diameter 0.00496 m; CT number +400 HU -> light-alloy
70
+
71
+ ### 2.6 Magnetic particle (MT) — ferromagnetic parts
72
+ - Not applicable to this non-ferromagnetic part; retained in the method library for ferromagnetic parts (forgings, steel castings).
73
+
74
+ ### 2.7 Leak test (LT) — sealed assembly
75
+ - Method recommendation: **pressure-decay** — only one side accessible and the part holds pressure, so a decay test on the sealed internal volume fits
76
+ - Pressure decay: 5 L at dP 0.02 bar over 600 s -> leak rate **0.1645 scc/s**; allowable 0.2 scc/s -> disposition **accept** (margin 0.85 dB)
77
+ - Gauge adequacy: 0.0005 bar resolution needs >= 12.3 s to catch the allowable leak; test time 600 s is adequate
78
+
79
+ ## 3. NDT personnel qualification
80
+
81
+ - Operator level: Level II (supervisor Level III)
82
+ - Certification status as of 2026-09-05: **current** (recert due 2027-09-05; near-vision due 2027-03-05)
83
+ - Supervision pairing: valid (Level I operators work under Level II/III)
84
+ - Upgrade evaluation (toward Level III): eligible when hours, months and examination are met
85
+ - Per-method certification check (interpretation of results requires the level shown):
86
+ - PT: Level II to interpret, Level I to perform — operator meets
87
+ - ET: Level II to interpret, Level I to perform — operator meets
88
+ - UT: Level II to interpret, Level I to perform — operator meets
89
+ - RT: Level II to interpret, Level I to perform — operator meets
90
+ - CT: Level II to interpret, Level II to perform — operator meets
91
+ - LT: Level II to interpret, Level II to perform — operator meets
92
+
93
+ ## 4. Acceptance and disposition framing
94
+
95
+ All dispositions in this plan are INPUTS for the responsible engineering authority; the customer acceptance document and the qualified procedure govern.
96
+
97
+ - **ET:** signal amplitude and phase vs the calibrated reference standard gate sizing; indications are evaluated against the acceptance document for the zone (crack-like vs volumetric responses)
98
+ - **PT:** rounded indications (pores) and linear indications (cracks, laps) are sized from the bleed-out image and evaluated against their class limits; excessive background is a process failure, not an indication
99
+ - **RT:** the discontinuity class (porosity / crack / inclusion / slag) gates the applicable acceptance criteria; the technique verdict (unsharpness, IQI sensitivity, film density) must be acceptable before any disposition is made
100
+ - **CT:** segmented void fraction and equivalent void diameter are disposition inputs against the casting acceptance standard; resolution must first pass the required-flaw check
101
+ - **LT:** measured leak rate is accepted when at or below the maximum allowable, rejected when above 1.25x the allowable, reviewed in between (margin band)
102
+
103
+ ## 5. Open items for the responsible authority
104
+
105
+ - Confirm the acceptance limits and the qualified procedure revisions named in the drawing and the program plan.
106
+ - Confirm operator certification records and intervals match the employer's written practice.
107
+
108
+ ---
109
+ *Generated by Aero Agent Roles ndt-engineer core (2026-09-05). DRAFT for human NDT engineering review. Not an approval document; carries no acceptance or certification authority.*
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """Test the evidence bundle + skill-dispatch protocol for ndt-engineer
3
+ (PROTOCOL.md v1).
4
+
5
+ Verifies:
6
+ - build --bundle emits evidence/{model,gates,provenance}.json
7
+ - bundle JSON parses; model contains the computed method-selection rows
8
+ and parameter cards; gates all_pass; exit code 0
9
+ - skill dispatch cross-checks core numbers against the bound ndt leaf
10
+ logic (agreeing within tolerance) when AeroSkills is present
11
+ - standalone (AEROSKILLS_DEV=/nonexistent) emits the bundle honestly
12
+ with cross_checked=false
13
+ - build --leak-limit-sccs re-runs the disposition
14
+ - check --file passes the gate vocabulary
15
+ - a malformed profile fails loudly with exit 1
16
+ """
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import unittest
24
+
25
+ ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
+ CLI = os.path.join(ROLE_DIR, "cli.py")
27
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(
28
+ os.path.dirname(os.path.abspath(__file__)))))
29
+ PROFILE = os.path.join(REPO_ROOT, "profiles", "example-airframer.json")
30
+ EXPECTED_CUSTOMER = "Example Airframers Ltd"
31
+
32
+ AEROSKILLS = os.environ.get("AEROSKILLS_DEV",
33
+ os.path.expanduser("~/AeroSkills"))
34
+ HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
35
+
36
+ # Functions the CLI should dispatch against the bound leaf logic.
37
+ EXPECTED_DISPATCH_FNS = [
38
+ "select_frequency_for_flaw", # eddy-current-inspection
39
+ "capillary_pressure", # liquid-penetrant-inspection
40
+ "exposure_time", # radiographic-inspection
41
+ "projection_count", # computed-tomography
42
+ "pressure_decay_rate", # leak-testing
43
+ ]
44
+
45
+
46
+ def run_build(outdir, extra_args=None, env_extra=None):
47
+ env = dict(os.environ)
48
+ if env_extra:
49
+ env.update(env_extra)
50
+ args = [sys.executable, CLI, "build", "--out",
51
+ os.path.join(outdir, "plan.md"), "--bundle"]
52
+ if extra_args:
53
+ args.extend(extra_args)
54
+ return subprocess.run(args, capture_output=True, text=True, env=env)
55
+
56
+
57
+ class TestNdtCliBundle(unittest.TestCase):
58
+
59
+ def setUp(self):
60
+ self.tmp = tempfile.mkdtemp()
61
+
62
+ def tearDown(self):
63
+ shutil.rmtree(self.tmp, ignore_errors=True)
64
+
65
+ def _model(self):
66
+ return json.load(open(os.path.join(self.tmp, "evidence",
67
+ "model.json")))
68
+
69
+ def _gates(self):
70
+ return json.load(open(os.path.join(self.tmp, "evidence",
71
+ "gates.json")))
72
+
73
+ def _prov(self):
74
+ return json.load(open(os.path.join(self.tmp, "evidence",
75
+ "provenance.json")))
76
+
77
+ def test_bundle_emits_three_files_exit_zero(self):
78
+ r = run_build(self.tmp)
79
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
80
+ for f in ("model.json", "gates.json", "provenance.json"):
81
+ self.assertTrue(os.path.exists(os.path.join(self.tmp, "evidence",
82
+ f)))
83
+
84
+ def test_model_content_and_gates(self):
85
+ r = run_build(self.tmp)
86
+ self.assertEqual(r.returncode, 0)
87
+ m = self._model()
88
+ self.assertEqual(m["role"], "ndt-engineer")
89
+ self.assertEqual(m["status"], "draft-for-review")
90
+ self.assertEqual(
91
+ m["document_type"],
92
+ "Nondestructive Test Plan and Method Selection Report")
93
+ self.assertEqual([x["top_method"] for x in m["defect_population"]],
94
+ ["ET", "UT", "UT"])
95
+ self.assertIn("eddy-current-surface", m["parameter_cards"])
96
+ self.assertIn("liquid-penetrant", m["parameter_cards"])
97
+ self.assertIn("radiography", m["parameter_cards"])
98
+ self.assertIn("computed-tomography", m["parameter_cards"])
99
+ self.assertIn("leak-test", m["parameter_cards"])
100
+ g = self._gates()
101
+ self.assertTrue(g["all_pass"])
102
+ self.assertEqual(g["exit_code"], 0)
103
+
104
+ def test_dispatch_crosscheck_when_skills_present(self):
105
+ r = run_build(self.tmp)
106
+ self.assertEqual(r.returncode, 0)
107
+ prov = self._prov()
108
+ fns = [row["function"] for row in prov["skills"]]
109
+ if HAS_SKILLS:
110
+ for fn in EXPECTED_DISPATCH_FNS:
111
+ self.assertIn(fn, fns, "expected dispatch fn missing: " + fn)
112
+ self.assertTrue(prov["cross_checked"])
113
+ self.assertTrue(all(row["agrees"] for row in prov["skills"]),
114
+ [row for row in prov["skills"]
115
+ if not row["agrees"]])
116
+ for row in prov["skills"]:
117
+ self.assertLessEqual(row["delta"], 1e-6)
118
+ else:
119
+ self.assertEqual(prov["skills"], [])
120
+ self.assertFalse(prov["cross_checked"])
121
+
122
+ def test_standalone_honest_bundle(self):
123
+ r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
124
+ self.assertEqual(r.returncode, 0)
125
+ prov = self._prov()
126
+ self.assertEqual(prov["skills"], [])
127
+ self.assertFalse(prov["cross_checked"])
128
+ # deliverable is still complete and gate-passing standalone
129
+ m = self._model()
130
+ self.assertTrue(m["defect_population"])
131
+ g = self._gates()
132
+ self.assertTrue(g["all_pass"])
133
+
134
+ def test_leak_limit_override_changes_disposition(self):
135
+ r = run_build(self.tmp, ["--leak-limit-sccs", "0.1"])
136
+ self.assertEqual(r.returncode, 0)
137
+ m = self._model()
138
+ self.assertEqual(m["parameter_cards"]["leak-test"]["verdict"],
139
+ "reject")
140
+ # plan gates remain green; the disposition data is honest
141
+ self.assertTrue(self._gates()["all_pass"])
142
+
143
+ def test_profile_header_and_model(self):
144
+ if not os.path.exists(PROFILE):
145
+ self.skipTest("profiles/example-airframer.json not present")
146
+ r = run_build(self.tmp, ["--profile", PROFILE])
147
+ self.assertEqual(r.returncode, 0, r.stderr[-500:])
148
+ md = open(os.path.join(self.tmp, "plan.md")).read()
149
+ self.assertTrue(md.startswith("## Program context"))
150
+ self.assertIn("Customer: " + EXPECTED_CUSTOMER, md)
151
+ m = self._model()
152
+ self.assertEqual(m["profile"]["customer"], EXPECTED_CUSTOMER)
153
+ self.assertEqual(m["status"], "draft-for-review")
154
+ # gate-check still passes on the profiled markdown
155
+ c = subprocess.run([sys.executable, CLI, "check", "--file",
156
+ os.path.join(self.tmp, "plan.md")],
157
+ capture_output=True, text=True)
158
+ self.assertEqual(c.returncode, 0, c.stdout)
159
+ self.assertIn("RESULT: PASS", c.stdout)
160
+
161
+ def test_malformed_profile_fails_loudly(self):
162
+ bad = os.path.join(self.tmp, "bad-profile.json")
163
+ with open(bad, "w") as f:
164
+ json.dump({"program": "no customer here"}, f)
165
+ r = run_build(self.tmp, ["--profile", bad])
166
+ self.assertEqual(r.returncode, 1)
167
+ self.assertIn("error: bad profile", r.stdout)
168
+
169
+ def test_check_command_gate_vocabulary(self):
170
+ r = run_build(self.tmp)
171
+ self.assertEqual(r.returncode, 0)
172
+ c = subprocess.run([sys.executable, CLI, "check", "--file",
173
+ os.path.join(self.tmp, "plan.md")],
174
+ capture_output=True, text=True)
175
+ self.assertEqual(c.returncode, 0)
176
+ self.assertIn("RESULT: PASS", c.stdout)
177
+ for name in ("has_title", "has_selection", "has_personnel",
178
+ "has_not_approval"):
179
+ self.assertIn(name, c.stdout)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ unittest.main()