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.
- package/NOTICE +18 -0
- package/README.md +49 -0
- package/bin/aero-agent-roles.js +127 -0
- package/lib/catalog.js +97 -0
- package/lib/install.js +48 -0
- package/lib/mcp.js +199 -0
- package/manifest.json +516 -0
- package/package.json +44 -0
- package/roles/aerodynamics-engineer/ROLE.md +153 -0
- package/roles/aerodynamics-engineer/SOURCES.md +12 -0
- package/roles/aerodynamics-engineer/cli.py +143 -0
- package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
- package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
- package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
- package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
- package/roles/aircraft-design-engineer/ROLE.md +138 -0
- package/roles/aircraft-design-engineer/SOURCES.md +12 -0
- package/roles/aircraft-design-engineer/cli.py +152 -0
- package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
- package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
- package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
- package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
- package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
- package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
- package/roles/airworthiness-compliance-engineer/cli.py +146 -0
- package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
- package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
- package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
- package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
- package/roles/as9100-quality-auditor/ROLE.md +128 -0
- package/roles/as9100-quality-auditor/SOURCES.md +16 -0
- package/roles/as9100-quality-auditor/cli.py +136 -0
- package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
- package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
- package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
- package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
- package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
- package/roles/data-bus-avionics-engineer/cli.py +311 -0
- package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
- package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
- package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
- package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
- package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
- package/roles/do160-environmental-engineer/ROLE.md +136 -0
- package/roles/do160-environmental-engineer/SOURCES.md +30 -0
- package/roles/do160-environmental-engineer/cli.py +147 -0
- package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
- package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
- package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
- package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
- package/roles/do178c-cert-engineer/ROLE.md +120 -0
- package/roles/do178c-cert-engineer/SOURCES.md +16 -0
- package/roles/do178c-cert-engineer/cli.py +129 -0
- package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
- package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
- package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
- package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
- package/roles/do254-hardware-engineer/ROLE.md +127 -0
- package/roles/do254-hardware-engineer/SOURCES.md +19 -0
- package/roles/do254-hardware-engineer/cli.py +308 -0
- package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
- package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
- package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
- package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
- package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
- package/roles/engineering-analysis-engineer/ROLE.md +154 -0
- package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
- package/roles/engineering-analysis-engineer/cli.py +151 -0
- package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
- package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
- package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
- package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
- package/roles/flight-management-engineer/ROLE.md +160 -0
- package/roles/flight-management-engineer/SOURCES.md +30 -0
- package/roles/flight-management-engineer/cli.py +258 -0
- package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
- package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
- package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
- package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
- package/roles/flight-mechanics-engineer/ROLE.md +162 -0
- package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
- package/roles/flight-mechanics-engineer/cli.py +149 -0
- package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
- package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
- package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
- package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
- package/roles/flight-test-engineer/ROLE.md +137 -0
- package/roles/flight-test-engineer/SOURCES.md +24 -0
- package/roles/flight-test-engineer/cli.py +135 -0
- package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
- package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
- package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
- package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
- package/roles/flight-test-performance-engineer/ROLE.md +165 -0
- package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
- package/roles/flight-test-performance-engineer/cli.py +319 -0
- package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
- package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
- package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
- package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
- package/roles/gnc-engineer/ROLE.md +141 -0
- package/roles/gnc-engineer/SOURCES.md +35 -0
- package/roles/gnc-engineer/cli.py +141 -0
- package/roles/gnc-engineer/core/gnc_core.py +1074 -0
- package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
- package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
- package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
- package/roles/guidance-engineer/ROLE.md +156 -0
- package/roles/guidance-engineer/SOURCES.md +43 -0
- package/roles/guidance-engineer/cli.py +280 -0
- package/roles/guidance-engineer/core/guidance_core.py +820 -0
- package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
- package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
- package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
- package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
- package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
- package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
- package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
- package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
- package/roles/ndt-engineer/ROLE.md +157 -0
- package/roles/ndt-engineer/SOURCES.md +18 -0
- package/roles/ndt-engineer/cli.py +316 -0
- package/roles/ndt-engineer/core/ndt_core.py +1590 -0
- package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
- package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
- package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
- package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
- package/roles/numerical-analysis-engineer/ROLE.md +158 -0
- package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
- package/roles/numerical-analysis-engineer/cli.py +414 -0
- package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
- package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
- package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
- package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
- package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
- package/roles/propulsion-engineer/ROLE.md +163 -0
- package/roles/propulsion-engineer/SOURCES.md +15 -0
- package/roles/propulsion-engineer/cli.py +168 -0
- package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
- package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
- package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
- package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
- package/roles/space-systems-engineer/ROLE.md +161 -0
- package/roles/space-systems-engineer/SOURCES.md +49 -0
- package/roles/space-systems-engineer/cli.py +173 -0
- package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
- package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
- package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
- package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
- package/roles/state-estimation-engineer/ROLE.md +154 -0
- package/roles/state-estimation-engineer/SOURCES.md +27 -0
- package/roles/state-estimation-engineer/cli.py +326 -0
- package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
- package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
- package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
- package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
- package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
- package/roles/structures-loads-engineer/ROLE.md +171 -0
- package/roles/structures-loads-engineer/SOURCES.md +44 -0
- package/roles/structures-loads-engineer/cli.py +219 -0
- package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
- package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
- package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
- package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
- package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""airworthiness_core.py - Airworthiness Compliance Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given a certification item's project facts it
|
|
5
|
+
determines the applicable FAR-25 / CS-25 regulations, selects the means of
|
|
6
|
+
compliance per regulation, assigns the compliance document and an honest
|
|
7
|
+
proposed status, and BUILDS the airworthiness compliance matrix (the
|
|
8
|
+
certification matrix / compliance checklist deliverable). It also
|
|
9
|
+
gate-checks deliverables. Standalone: no external repo needed.
|
|
10
|
+
|
|
11
|
+
Domain rules encoded here are public process knowledge:
|
|
12
|
+
- Regulation numbers, section titles and subpart placement of 14 CFR
|
|
13
|
+
Part 25 (FAR-25) are public-domain (US government work, quotable with
|
|
14
|
+
citation; verified against eCFR). CS-25 mirrors FAR-25 with EU
|
|
15
|
+
amendments; referenced at rule level only.
|
|
16
|
+
- The means-of-compliance selection rules are a deterministic summary of
|
|
17
|
+
public certification practice (the six-class MOC scheme and the
|
|
18
|
+
analysis / test / inspection / similarity vocabulary). No verbatim AMC
|
|
19
|
+
or regulatory body text is reproduced.
|
|
20
|
+
|
|
21
|
+
Author: ashfordeOU
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import date
|
|
29
|
+
|
|
30
|
+
def _today() -> str:
|
|
31
|
+
import os
|
|
32
|
+
from datetime import date
|
|
33
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Domain tables (public-domain FAR-25 / CS-25 structure and practice)
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
# Severity of a failure condition -> development assurance level context
|
|
40
|
+
# (standard 25.1309 practice, as used in certification programs).
|
|
41
|
+
SEVERITY_TO_DAL = {
|
|
42
|
+
"catastrophic": "A",
|
|
43
|
+
"hazardous": "B",
|
|
44
|
+
"major": "C",
|
|
45
|
+
"minor": "D",
|
|
46
|
+
"no-safety-effect": "E",
|
|
47
|
+
"n/a": "n/a",
|
|
48
|
+
}
|
|
49
|
+
DALS = ("A", "B", "C", "D", "E", "n/a")
|
|
50
|
+
|
|
51
|
+
# Failure-condition severities that trigger the 25.1309 safety assessment
|
|
52
|
+
# (systems whose failure conditions are catastrophic, hazardous, or major).
|
|
53
|
+
SAFETY_SIGNIFICANT = ("catastrophic", "hazardous", "major")
|
|
54
|
+
|
|
55
|
+
# Means-of-compliance vocabulary, six-class scheme used in certification
|
|
56
|
+
# practice (summary of public EASA/FAA style MOC categories).
|
|
57
|
+
MOC_NAMES = {
|
|
58
|
+
1: "engineering/analysis",
|
|
59
|
+
2: "ground test",
|
|
60
|
+
3: "flight test",
|
|
61
|
+
4: "simulation/analysis tool",
|
|
62
|
+
5: "certification by similarity",
|
|
63
|
+
6: "safety assessment",
|
|
64
|
+
}
|
|
65
|
+
MOC_IDS = tuple(sorted(MOC_NAMES))
|
|
66
|
+
|
|
67
|
+
# Coarse method words accepted in a compliance matrix (FAA-style vocabulary
|
|
68
|
+
# per public airworthiness practice: analysis, test, inspection, similarity).
|
|
69
|
+
MOC_WORDS = (
|
|
70
|
+
"analysis",
|
|
71
|
+
"test",
|
|
72
|
+
"ground test",
|
|
73
|
+
"flight test",
|
|
74
|
+
"inspection",
|
|
75
|
+
"similarity",
|
|
76
|
+
"safety assessment",
|
|
77
|
+
"engineering/analysis",
|
|
78
|
+
"simulation/analysis tool",
|
|
79
|
+
"certification program demonstration",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Item kinds a certification item can be (per-item MOC suitability rules).
|
|
83
|
+
ITEM_KINDS = (
|
|
84
|
+
"structure",
|
|
85
|
+
"systems",
|
|
86
|
+
"powerplant",
|
|
87
|
+
"equipment",
|
|
88
|
+
"software",
|
|
89
|
+
"hardware",
|
|
90
|
+
"performance",
|
|
91
|
+
"handling",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# FAR-25 subpart placement for the regulations this role routes on.
|
|
95
|
+
# Letters/names are the public FAR-25 structure (A General, C Structure,
|
|
96
|
+
# D Design and Construction, F Equipment, ...).
|
|
97
|
+
SUBPARTS = {
|
|
98
|
+
"A": "General",
|
|
99
|
+
"B": "Flight",
|
|
100
|
+
"C": "Structure",
|
|
101
|
+
"D": "Design and Construction",
|
|
102
|
+
"E": "Powerplant",
|
|
103
|
+
"F": "Equipment",
|
|
104
|
+
"G": "Operating Limitations and Information",
|
|
105
|
+
"H": "Electrical Wiring Interconnection Systems (EWIS)",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Representative area mapping (which product area a paragraph belongs to),
|
|
109
|
+
# transport-airplane oriented per public FAR-25 structure.
|
|
110
|
+
AREA_PARAGRAPHS = {
|
|
111
|
+
"systems": ["25.1309", "25.1310", "25.1329"],
|
|
112
|
+
"flight-controls": ["25.671", "25.672", "25.675", "25.677", "25.679", "25.683"],
|
|
113
|
+
"structure": ["25.301", "25.303", "25.305", "25.307", "25.571", "25.629"],
|
|
114
|
+
"design-and-construction": ["25.601", "25.603", "25.605", "25.607", "25.609"],
|
|
115
|
+
"powerplant": ["25.901", "25.903", "25.933", "25.943", "25.1101"],
|
|
116
|
+
"equipment": ["25.1301", "25.1303", "25.1305", "25.1309", "25.1316"],
|
|
117
|
+
"operating-limitations": ["25.1501", "25.1503", "25.1511", "25.1521"],
|
|
118
|
+
"ewis": ["25.1701", "25.1703", "25.1707", "25.1709", "25.1711", "25.1713", "25.1717"],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Regulation register: real FAR-25 paragraphs used by this role. Fields:
|
|
122
|
+
# subpart - public FAR-25 subpart letter
|
|
123
|
+
# area - product area key from AREA_PARAGRAPHS
|
|
124
|
+
# item_kind - item kind for MOC suitability
|
|
125
|
+
# severity - context severity of the paragraph in a typical systems
|
|
126
|
+
# installation (structure rows carry "n/a")
|
|
127
|
+
# novel_by_default - True when a retrofit of this kind is typically a new
|
|
128
|
+
# feature screened against the existing basis
|
|
129
|
+
# Section titles are the public eCFR titles (public domain, quotable).
|
|
130
|
+
REGS = {
|
|
131
|
+
"25.1301": {
|
|
132
|
+
"title": "Function and installation",
|
|
133
|
+
"subpart": "F",
|
|
134
|
+
"area": "equipment",
|
|
135
|
+
"item_kind": "equipment",
|
|
136
|
+
"severity": "n/a",
|
|
137
|
+
"compliance_document": "Equipment installation inspection records and "
|
|
138
|
+
"qualification test report (installation conformity)",
|
|
139
|
+
},
|
|
140
|
+
"25.1303": {
|
|
141
|
+
"title": "Flight and navigation instruments",
|
|
142
|
+
"subpart": "F",
|
|
143
|
+
"area": "equipment",
|
|
144
|
+
"item_kind": "equipment",
|
|
145
|
+
"severity": "n/a",
|
|
146
|
+
"compliance_document": "Instrument installation conformity records",
|
|
147
|
+
},
|
|
148
|
+
"25.1309": {
|
|
149
|
+
"title": "Equipment, systems, and installations",
|
|
150
|
+
"subpart": "F",
|
|
151
|
+
"area": "systems",
|
|
152
|
+
"item_kind": "systems",
|
|
153
|
+
"severity": "hazardous",
|
|
154
|
+
"compliance_document": "System safety assessment (SSA) per 25.1309 "
|
|
155
|
+
"with failure condition classification",
|
|
156
|
+
},
|
|
157
|
+
"25.1310": {
|
|
158
|
+
"title": "Power source capacity and distribution",
|
|
159
|
+
"subpart": "F",
|
|
160
|
+
"area": "systems",
|
|
161
|
+
"item_kind": "systems",
|
|
162
|
+
"severity": "major",
|
|
163
|
+
"compliance_document": "Electrical load analysis and power source "
|
|
164
|
+
"capacity report",
|
|
165
|
+
},
|
|
166
|
+
"25.1329": {
|
|
167
|
+
"title": "Flight guidance system (autopilot)",
|
|
168
|
+
"subpart": "F",
|
|
169
|
+
"area": "systems",
|
|
170
|
+
"item_kind": "systems",
|
|
171
|
+
"severity": "hazardous",
|
|
172
|
+
"compliance_document": "Autopilot ground/flight test report and SSA "
|
|
173
|
+
"input for the flight guidance system",
|
|
174
|
+
},
|
|
175
|
+
"25.671": {
|
|
176
|
+
"title": "General (flight control systems)",
|
|
177
|
+
"subpart": "D",
|
|
178
|
+
"area": "flight-controls",
|
|
179
|
+
"item_kind": "systems",
|
|
180
|
+
"severity": "hazardous",
|
|
181
|
+
"compliance_document": "Flight control integration analysis and test "
|
|
182
|
+
"report (control system failure assessment)",
|
|
183
|
+
},
|
|
184
|
+
"25.683": {
|
|
185
|
+
"title": "Operation tests",
|
|
186
|
+
"subpart": "D",
|
|
187
|
+
"area": "flight-controls",
|
|
188
|
+
"item_kind": "systems",
|
|
189
|
+
"severity": "n/a",
|
|
190
|
+
"compliance_document": "Control system operation test report",
|
|
191
|
+
},
|
|
192
|
+
"25.301": {
|
|
193
|
+
"title": "Loads",
|
|
194
|
+
"subpart": "C",
|
|
195
|
+
"area": "structure",
|
|
196
|
+
"item_kind": "structure",
|
|
197
|
+
"severity": "n/a",
|
|
198
|
+
"compliance_document": "Loads analysis report (change-effect "
|
|
199
|
+
"assessment on the affected structure)",
|
|
200
|
+
},
|
|
201
|
+
"25.571": {
|
|
202
|
+
"title": "Damage tolerance and fatigue evaluation of structure",
|
|
203
|
+
"subpart": "C",
|
|
204
|
+
"area": "structure",
|
|
205
|
+
"item_kind": "structure",
|
|
206
|
+
"severity": "n/a",
|
|
207
|
+
"compliance_document": "Damage tolerance / fatigue evaluation and "
|
|
208
|
+
"inspection program for affected structure",
|
|
209
|
+
},
|
|
210
|
+
"25.607": {
|
|
211
|
+
"title": "Fasteners",
|
|
212
|
+
"subpart": "D",
|
|
213
|
+
"area": "design-and-construction",
|
|
214
|
+
"item_kind": "structure",
|
|
215
|
+
"severity": "n/a",
|
|
216
|
+
"compliance_document": "Fastener standards compliance record",
|
|
217
|
+
},
|
|
218
|
+
"25.629": {
|
|
219
|
+
"title": "Aeroelastic stability requirements",
|
|
220
|
+
"subpart": "C",
|
|
221
|
+
"area": "structure",
|
|
222
|
+
"item_kind": "structure",
|
|
223
|
+
"severity": "n/a",
|
|
224
|
+
"compliance_document": "Flutter / aeroelastic stability analysis and "
|
|
225
|
+
"flight test clearance report",
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Per-regulation extra methods of compliance that real demonstration
|
|
230
|
+
# practice requires on top of the item-kind suitability set (original
|
|
231
|
+
# routing summary, e.g. flight test for autopilot operation tests).
|
|
232
|
+
REG_EXTRA_MOC = {
|
|
233
|
+
"25.1329": ["flight test"],
|
|
234
|
+
"25.683": ["ground test"],
|
|
235
|
+
"25.629": ["flight test"],
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Certification basis by aircraft type (public regulation mapping).
|
|
239
|
+
# Product airplane: transport -> Part 25 (FAR-25/CS-25); normal/utility/
|
|
240
|
+
# acrobatic/commuter -> Part 23; rotorcraft -> Part 27/29; engines Part 33;
|
|
241
|
+
# propellers Part 35. This role routes on transport-category airplanes
|
|
242
|
+
# (FAR-25 / CS-25) but keeps the full public mapping for screening.
|
|
243
|
+
AIRCRAFT_REGULATION = {
|
|
244
|
+
("airplane", "transport"): "25",
|
|
245
|
+
("airplane", "normal"): "23",
|
|
246
|
+
("airplane", "utility"): "23",
|
|
247
|
+
("airplane", "acrobatic"): "23",
|
|
248
|
+
("airplane", "commuter"): "23",
|
|
249
|
+
("rotorcraft", "normal"): "27",
|
|
250
|
+
("rotorcraft", "transport"): "29",
|
|
251
|
+
("engine", "n/a"): "33",
|
|
252
|
+
("propeller", "n/a"): "35",
|
|
253
|
+
}
|
|
254
|
+
AIRCRAFT_ALIASES = {
|
|
255
|
+
"transport": ("airplane", "transport"),
|
|
256
|
+
"transport-category": ("airplane", "transport"),
|
|
257
|
+
"transport airplane": ("airplane", "transport"),
|
|
258
|
+
"large airplane": ("airplane", "transport"),
|
|
259
|
+
"normal": ("airplane", "normal"),
|
|
260
|
+
"normal-category": ("airplane", "normal"),
|
|
261
|
+
"rotorcraft": ("rotorcraft", "normal"),
|
|
262
|
+
"rotorcraft transport": ("rotorcraft", "transport"),
|
|
263
|
+
"engine": ("engine", "n/a"),
|
|
264
|
+
"propeller": ("propeller", "n/a"),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Owners by product area (original role assignment table).
|
|
268
|
+
AREA_OWNER = {
|
|
269
|
+
"systems": "systems safety engineer",
|
|
270
|
+
"flight-controls": "flight controls engineer",
|
|
271
|
+
"structure": "structures engineer",
|
|
272
|
+
"design-and-construction": "design engineer",
|
|
273
|
+
"powerplant": "powerplant engineer",
|
|
274
|
+
"equipment": "equipment engineer",
|
|
275
|
+
"operating-limitations": "flight test engineer",
|
|
276
|
+
"ewis": "EWIS engineer",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Statuses this role may write. A finding ("compliant") is NEVER written:
|
|
280
|
+
# that is the certification authority's call.
|
|
281
|
+
STATUS_PROPOSED = "proposed"
|
|
282
|
+
STATUS_VOCABULARY = ("proposed", "open", "in-work", "evidence-available")
|
|
283
|
+
FORBIDDEN_STATUS = ("compliant", "found", "approved", "issued")
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# Core computations
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _canonical_aircraft(aircraft_type: str) -> tuple[str, str]:
|
|
291
|
+
"""Map an aircraft_type string to (product_type, category)."""
|
|
292
|
+
key = (aircraft_type or "").strip().lower()
|
|
293
|
+
if key in AIRCRAFT_ALIASES:
|
|
294
|
+
return AIRCRAFT_ALIASES[key]
|
|
295
|
+
raise ValueError(
|
|
296
|
+
"unknown aircraft_type %r; expected one of %s"
|
|
297
|
+
% (aircraft_type, ", ".join(sorted(AIRCRAFT_ALIASES)))
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def reg_number(reg) -> str:
|
|
302
|
+
"""Normalize a regulation reference to a bare number (e.g. 25.1309)."""
|
|
303
|
+
if isinstance(reg, dict):
|
|
304
|
+
reg = reg.get("number") or reg.get("reg") or ""
|
|
305
|
+
text = str(reg or "").strip()
|
|
306
|
+
text = text.replace("FAR ", "").replace("CS-", "").replace("CS ", "")
|
|
307
|
+
match = re.fullmatch(r"(\d{2}\.\d{3,4})", text)
|
|
308
|
+
if match is None:
|
|
309
|
+
raise ValueError("invalid regulation reference %r" % (reg,))
|
|
310
|
+
return match.group(1)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def determine_applicability(reg, aircraft_type: str,
|
|
314
|
+
change_scope=None) -> dict:
|
|
315
|
+
"""Determine whether a regulation applies to a change on an aircraft.
|
|
316
|
+
|
|
317
|
+
Returns a verdict dict {reg, aircraft_type, product, category,
|
|
318
|
+
applicable, part, reason}. Two gates:
|
|
319
|
+
1. Aircraft-type gate: a FAR-25/CS-25 paragraph applies to
|
|
320
|
+
transport-category airplanes only; other categories screen to
|
|
321
|
+
their governing part (public regulation mapping).
|
|
322
|
+
2. Change-scope gate (when ``change_scope`` is given): the reg's
|
|
323
|
+
product area must be inside the change scope (e.g. an autopilot
|
|
324
|
+
STC that touches systems/flight-controls/equipment screens out
|
|
325
|
+
pure-structure paragraphs such as 25.571).
|
|
326
|
+
"""
|
|
327
|
+
number = reg_number(reg)
|
|
328
|
+
if number not in REGS:
|
|
329
|
+
raise ValueError(
|
|
330
|
+
"unknown regulation %r - not in the role's FAR-25 register"
|
|
331
|
+
% (number,)
|
|
332
|
+
)
|
|
333
|
+
product, category = _canonical_aircraft(aircraft_type)
|
|
334
|
+
governing = AIRCRAFT_REGULATION.get((product, category))
|
|
335
|
+
if governing is None:
|
|
336
|
+
raise ValueError(
|
|
337
|
+
"no governing airworthiness part for %s %s"
|
|
338
|
+
% (product, category)
|
|
339
|
+
)
|
|
340
|
+
if not number.startswith(governing + "."):
|
|
341
|
+
return {
|
|
342
|
+
"reg": number,
|
|
343
|
+
"aircraft_type": aircraft_type,
|
|
344
|
+
"product": product,
|
|
345
|
+
"category": category,
|
|
346
|
+
"applicable": False,
|
|
347
|
+
"part": governing,
|
|
348
|
+
"reason": (
|
|
349
|
+
"not applicable: %s is a Part 25 paragraph but a %s category "
|
|
350
|
+
"%s certifies under Part %s/CS-%s (the equivalent paragraph, "
|
|
351
|
+
"where one exists, lives in that part)"
|
|
352
|
+
% (number, category, product, governing, governing)
|
|
353
|
+
),
|
|
354
|
+
}
|
|
355
|
+
# Part matches the aircraft type: now screen against the change scope.
|
|
356
|
+
if change_scope is not None:
|
|
357
|
+
area = REGS[number]["area"]
|
|
358
|
+
if area not in change_scope:
|
|
359
|
+
return {
|
|
360
|
+
"reg": number,
|
|
361
|
+
"aircraft_type": aircraft_type,
|
|
362
|
+
"product": product,
|
|
363
|
+
"category": category,
|
|
364
|
+
"applicable": False,
|
|
365
|
+
"part": governing,
|
|
366
|
+
"reason": (
|
|
367
|
+
"not applicable to this change: %s covers the %s area "
|
|
368
|
+
"but the change scope is limited to %s; screened out "
|
|
369
|
+
"unless the change also touches the %s area - confirm "
|
|
370
|
+
"with the certification authority"
|
|
371
|
+
% (number, area, ", ".join(sorted(change_scope)), area)
|
|
372
|
+
),
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
"reg": number,
|
|
376
|
+
"aircraft_type": aircraft_type,
|
|
377
|
+
"product": product,
|
|
378
|
+
"category": category,
|
|
379
|
+
"applicable": True,
|
|
380
|
+
"part": governing,
|
|
381
|
+
"reason": (
|
|
382
|
+
"applies: %s is a %s airworthiness paragraph and the product "
|
|
383
|
+
"is a %s category %s"
|
|
384
|
+
% (number, "FAR-25/CS-25" if governing == "25"
|
|
385
|
+
else "FAR-%s/CS-%s" % (governing, governing),
|
|
386
|
+
category, product)
|
|
387
|
+
),
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _validate_kind_severity_dal(item_kind, severity, dal) -> None:
|
|
392
|
+
if item_kind not in ITEM_KINDS:
|
|
393
|
+
raise ValueError(
|
|
394
|
+
"unknown item_kind %r; expected one of %s"
|
|
395
|
+
% (item_kind, ", ".join(ITEM_KINDS))
|
|
396
|
+
)
|
|
397
|
+
if severity not in SEVERITY_TO_DAL:
|
|
398
|
+
raise ValueError(
|
|
399
|
+
"unknown severity %r; expected one of %s"
|
|
400
|
+
% (severity, ", ".join(sorted(SEVERITY_TO_DAL)))
|
|
401
|
+
)
|
|
402
|
+
if dal not in DALS:
|
|
403
|
+
raise ValueError(
|
|
404
|
+
"unknown development_assurance_level %r; expected one of %s"
|
|
405
|
+
% (dal, ", ".join(DALS))
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _dedupe(seq) -> list:
|
|
410
|
+
seen = []
|
|
411
|
+
for entry in seq:
|
|
412
|
+
if entry not in seen:
|
|
413
|
+
seen.append(entry)
|
|
414
|
+
return seen
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def moc_suitability(item_kind: str, severity: str = "n/a",
|
|
418
|
+
dal: str = "n/a", novel: bool = False) -> list:
|
|
419
|
+
"""Recommended MOC ids (ordered, primary first) for an item kind.
|
|
420
|
+
|
|
421
|
+
Deterministic routing summary of public certification practice:
|
|
422
|
+
structure - analysis primary with static/ground test evidence (novel
|
|
423
|
+
items add flight test); systems - analysis plus the MOC 6 safety
|
|
424
|
+
assessment for catastrophic/hazardous failures (simulation acceptable
|
|
425
|
+
for DAL C-E, a test MOC is added for novel items); powerplant - bench/
|
|
426
|
+
ground and flight test with supporting analysis; equipment -
|
|
427
|
+
environmental qualification test (DO-160 style) with analysis support;
|
|
428
|
+
software - DO-178C lifecycle data; hardware - DO-254 style analysis
|
|
429
|
+
plus verification test.
|
|
430
|
+
"""
|
|
431
|
+
_validate_kind_severity_dal(item_kind, severity, dal)
|
|
432
|
+
if item_kind == "structure":
|
|
433
|
+
return [1, 2, 3] if novel else [1, 2]
|
|
434
|
+
if item_kind == "systems":
|
|
435
|
+
rec = [1]
|
|
436
|
+
if severity in ("catastrophic", "hazardous"):
|
|
437
|
+
rec.append(6)
|
|
438
|
+
elif dal in ("C", "D", "E"):
|
|
439
|
+
rec.append(4)
|
|
440
|
+
if novel:
|
|
441
|
+
rec.append(2)
|
|
442
|
+
return _dedupe(rec)
|
|
443
|
+
if item_kind == "powerplant":
|
|
444
|
+
return [2, 3, 1]
|
|
445
|
+
if item_kind == "equipment":
|
|
446
|
+
return [2, 1]
|
|
447
|
+
if item_kind == "software":
|
|
448
|
+
return [1, 4]
|
|
449
|
+
if item_kind == "hardware":
|
|
450
|
+
return [1, 2]
|
|
451
|
+
return [3, 1, 4] # performance, handling
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def safety_assessment_required(severity: str) -> bool:
|
|
455
|
+
"""25.1309 practice: catastrophic/hazardous/major failure conditions
|
|
456
|
+
require the safety assessment."""
|
|
457
|
+
if severity not in SEVERITY_TO_DAL:
|
|
458
|
+
raise ValueError("unknown failure-condition severity %r" % (severity,))
|
|
459
|
+
return severity in SAFETY_SIGNIFICANT
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def severity_to_dal(severity: str) -> str:
|
|
463
|
+
if severity not in SEVERITY_TO_DAL:
|
|
464
|
+
raise ValueError("unknown severity %r" % (severity,))
|
|
465
|
+
return SEVERITY_TO_DAL[severity]
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def select_moc(reg, cert_basis: str = "FAR-25",
|
|
469
|
+
severity: str | None = None,
|
|
470
|
+
dal: str | None = None,
|
|
471
|
+
novel: bool = False) -> dict:
|
|
472
|
+
"""Select the means of compliance for a regulation.
|
|
473
|
+
|
|
474
|
+
Returns a dict {reg, item_kind, severity, dal, moc_ids, methods,
|
|
475
|
+
moc_display, safety_assessment}. ``methods`` are coarse method words
|
|
476
|
+
(analysis/test/inspection vocabulary); ``moc_display`` renders the
|
|
477
|
+
six-class MOC scheme when the basis is CS-25 (per AMC practice) and
|
|
478
|
+
the coarse vocabulary for FAR-25.
|
|
479
|
+
"""
|
|
480
|
+
number = reg_number(reg)
|
|
481
|
+
if number not in REGS:
|
|
482
|
+
raise ValueError(
|
|
483
|
+
"unknown regulation %r - not in the role's FAR-25 register"
|
|
484
|
+
% (number,)
|
|
485
|
+
)
|
|
486
|
+
record = REGS[number]
|
|
487
|
+
item_kind = record["item_kind"]
|
|
488
|
+
resolved_severity = severity or record["severity"]
|
|
489
|
+
resolved_dal = dal or severity_to_dal(resolved_severity)
|
|
490
|
+
ids = moc_suitability(item_kind, resolved_severity, resolved_dal, novel)
|
|
491
|
+
ids = _dedupe(ids + [moc_id(m) for m in REG_EXTRA_MOC.get(number, [])
|
|
492
|
+
if moc_id(m) not in ids])
|
|
493
|
+
methods = [moc_name(i) for i in ids]
|
|
494
|
+
basis = str(cert_basis or "FAR-25").upper()
|
|
495
|
+
if "CS" in basis:
|
|
496
|
+
display = " + ".join(
|
|
497
|
+
"MOC %d %s" % (i, MOC_NAMES[i]) for i in ids
|
|
498
|
+
) + " (per AMC-25 style acceptable means of compliance)"
|
|
499
|
+
else:
|
|
500
|
+
display = " + ".join(methods)
|
|
501
|
+
return {
|
|
502
|
+
"reg": number,
|
|
503
|
+
"item_kind": item_kind,
|
|
504
|
+
"severity": resolved_severity,
|
|
505
|
+
"dal": resolved_dal,
|
|
506
|
+
"moc_ids": ids,
|
|
507
|
+
"methods": methods,
|
|
508
|
+
"moc_display": display,
|
|
509
|
+
"safety_assessment": safety_assessment_required(resolved_severity),
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def moc_id(word: str) -> int:
|
|
514
|
+
"""Map a coarse method word to its MOC class id."""
|
|
515
|
+
mapping = {
|
|
516
|
+
"analysis": 1,
|
|
517
|
+
"engineering/analysis": 1,
|
|
518
|
+
"ground test": 2,
|
|
519
|
+
"test": 2,
|
|
520
|
+
"flight test": 3,
|
|
521
|
+
"simulation": 4,
|
|
522
|
+
"simulation/analysis tool": 4,
|
|
523
|
+
"similarity": 5,
|
|
524
|
+
"safety assessment": 6,
|
|
525
|
+
}
|
|
526
|
+
key = word.strip().lower()
|
|
527
|
+
if key not in mapping:
|
|
528
|
+
raise ValueError("unknown means of compliance %r" % (word,))
|
|
529
|
+
return mapping[key]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def moc_name(moc_id_val: int) -> str:
|
|
533
|
+
if moc_id_val not in MOC_NAMES:
|
|
534
|
+
raise ValueError("unknown MOC id %r" % (moc_id_val,))
|
|
535
|
+
return MOC_NAMES[moc_id_val]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def moc_is_valid(method: str) -> bool:
|
|
539
|
+
return method.strip().lower() in MOC_WORDS
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def methods_are_valid(methods) -> bool:
|
|
543
|
+
return all(moc_is_valid(m) for m in methods)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def basis_name(aircraft_type: str, jurisdiction: str) -> str:
|
|
547
|
+
"""Certification basis regulation id for a transport airplane."""
|
|
548
|
+
product, category = _canonical_aircraft(aircraft_type)
|
|
549
|
+
governing = AIRCRAFT_REGULATION.get((product, category))
|
|
550
|
+
if governing != "25":
|
|
551
|
+
raise ValueError(
|
|
552
|
+
"role routes FAR-25/CS-25 transport work; %r certifies under "
|
|
553
|
+
"Part %s" % (aircraft_type, governing)
|
|
554
|
+
)
|
|
555
|
+
if jurisdiction not in ("FAA", "EASA"):
|
|
556
|
+
raise ValueError("jurisdiction must be FAA or EASA, got %r" % (jurisdiction,))
|
|
557
|
+
return "FAR-25" if jurisdiction == "FAA" else "CS-25"
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
# Compliance matrix builder: produces the actual deliverable content
|
|
562
|
+
# ---------------------------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
@dataclass
|
|
566
|
+
class AirworthinessItem:
|
|
567
|
+
"""Project facts the role needs to build the compliance matrix."""
|
|
568
|
+
item_name: str
|
|
569
|
+
description: str = ""
|
|
570
|
+
aircraft_type: str = "transport"
|
|
571
|
+
jurisdiction: str = "FAA"
|
|
572
|
+
certification_path: str = "STC" # type certificate / STC / amended TC / TSO
|
|
573
|
+
regulation_ids: list = field(default_factory=list)
|
|
574
|
+
change_scope: list = field(default_factory=list) # areas the change touches
|
|
575
|
+
severity_overrides: dict = field(default_factory=dict)
|
|
576
|
+
novel_regs: list = field(default_factory=list)
|
|
577
|
+
special_conditions: list = field(default_factory=list) # list[str]
|
|
578
|
+
elos_items: list = field(default_factory=list) # list[str]
|
|
579
|
+
|
|
580
|
+
def basis(self) -> str:
|
|
581
|
+
return basis_name(self.aircraft_type, self.jurisdiction)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def compliance_row(reg, item: AirworthinessItem) -> dict:
|
|
585
|
+
"""Build one matrix row: reg -> applicability -> MoC -> document ->
|
|
586
|
+
proposed status."""
|
|
587
|
+
number = reg_number(reg)
|
|
588
|
+
if number not in REGS:
|
|
589
|
+
raise ValueError(
|
|
590
|
+
"unknown regulation %r - not in the role's FAR-25 register"
|
|
591
|
+
% (number,)
|
|
592
|
+
)
|
|
593
|
+
record = REGS[number]
|
|
594
|
+
verdict = determine_applicability(
|
|
595
|
+
number, item.aircraft_type, change_scope=item.change_scope or None
|
|
596
|
+
)
|
|
597
|
+
severity = item.severity_overrides.get(number, record["severity"])
|
|
598
|
+
novel = number in (item.novel_regs or [])
|
|
599
|
+
moc = select_moc(number, item.basis(), severity=severity, novel=novel)
|
|
600
|
+
dal = moc["dal"]
|
|
601
|
+
if not verdict["applicable"]:
|
|
602
|
+
return {
|
|
603
|
+
"reg": number,
|
|
604
|
+
"title": record["title"],
|
|
605
|
+
"subpart": record["subpart"],
|
|
606
|
+
"area": record["area"],
|
|
607
|
+
"applicable": False,
|
|
608
|
+
"reason": verdict["reason"],
|
|
609
|
+
"item_kind": record["item_kind"],
|
|
610
|
+
"severity": severity,
|
|
611
|
+
"dal": dal,
|
|
612
|
+
"moc_ids": [],
|
|
613
|
+
"methods": [],
|
|
614
|
+
"moc_display": "not applicable - screened out",
|
|
615
|
+
"safety_assessment": False,
|
|
616
|
+
"compliance_document": "not required (not applicable)",
|
|
617
|
+
"status": STATUS_PROPOSED,
|
|
618
|
+
"owner": "-",
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
"reg": number,
|
|
622
|
+
"title": record["title"],
|
|
623
|
+
"subpart": record["subpart"],
|
|
624
|
+
"area": record["area"],
|
|
625
|
+
"applicable": True,
|
|
626
|
+
"reason": verdict["reason"],
|
|
627
|
+
"item_kind": record["item_kind"],
|
|
628
|
+
"severity": severity,
|
|
629
|
+
"dal": dal,
|
|
630
|
+
"moc_ids": moc["moc_ids"],
|
|
631
|
+
"methods": moc["methods"],
|
|
632
|
+
"moc_display": moc["moc_display"],
|
|
633
|
+
"safety_assessment": moc["safety_assessment"],
|
|
634
|
+
"compliance_document": record["compliance_document"],
|
|
635
|
+
"status": STATUS_PROPOSED,
|
|
636
|
+
"owner": AREA_OWNER.get(record["area"], "compliance engineer"),
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _row_matches_item(row: dict, item: AirworthinessItem) -> bool:
|
|
641
|
+
"""Gate helper: is the row's applicability determination consistent
|
|
642
|
+
with the item scope? (all rows built from the same item are)."""
|
|
643
|
+
if row["reg"] not in REGS:
|
|
644
|
+
return False
|
|
645
|
+
return True
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def build_matrix(item: AirworthinessItem) -> dict:
|
|
649
|
+
"""Build the full compliance matrix content model."""
|
|
650
|
+
rows = [compliance_row(r, item) for r in item.regulation_ids]
|
|
651
|
+
applicable = [r for r in rows if r["applicable"]]
|
|
652
|
+
covered = [r for r in applicable if r["methods"] and r["compliance_document"]]
|
|
653
|
+
coverage = (len(covered) / len(applicable)) if applicable else 0.0
|
|
654
|
+
issues = [
|
|
655
|
+
"%s: no accepted means of compliance yet" % r["reg"]
|
|
656
|
+
for r in applicable
|
|
657
|
+
if not r["methods"]
|
|
658
|
+
]
|
|
659
|
+
special = item.special_conditions or []
|
|
660
|
+
elos = item.elos_items or []
|
|
661
|
+
return {
|
|
662
|
+
"document_type": "Airworthiness compliance checklist / certification matrix",
|
|
663
|
+
"status": "draft-for-review",
|
|
664
|
+
"item": item.item_name,
|
|
665
|
+
"item_description": item.description,
|
|
666
|
+
"aircraft_type": item.aircraft_type,
|
|
667
|
+
"jurisdiction": item.jurisdiction,
|
|
668
|
+
"certification_basis": item.basis(),
|
|
669
|
+
"certification_path": item.certification_path,
|
|
670
|
+
"special_conditions": special,
|
|
671
|
+
"elos_items": elos,
|
|
672
|
+
"rows": rows,
|
|
673
|
+
"applicable_count": len(applicable),
|
|
674
|
+
"row_count": len(rows),
|
|
675
|
+
"coverage": coverage,
|
|
676
|
+
"issues": issues,
|
|
677
|
+
"generated": _today(),
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def render_matrix_markdown(model: dict) -> str:
|
|
682
|
+
"""Render the compliance matrix content model as markdown."""
|
|
683
|
+
basis = model["certification_basis"]
|
|
684
|
+
regs_applicable = ", ".join(
|
|
685
|
+
r["reg"] for r in model["rows"] if r["applicable"]
|
|
686
|
+
) or "none"
|
|
687
|
+
sc = ", ".join(model["special_conditions"]) or "none identified"
|
|
688
|
+
elos = ", ".join(model["elos_items"]) or "none proposed"
|
|
689
|
+
lines = [
|
|
690
|
+
"# Airworthiness Compliance Matrix",
|
|
691
|
+
"",
|
|
692
|
+
f"**Item:** {model['item']}",
|
|
693
|
+
f"**Aircraft type:** {model['aircraft_type']} (jurisdiction "
|
|
694
|
+
f"{model['jurisdiction']})",
|
|
695
|
+
f"**Certification basis:** {basis}",
|
|
696
|
+
f"**Certification path:** {model['certification_path']}",
|
|
697
|
+
f"**Applicable regulations (proposed):** {regs_applicable}",
|
|
698
|
+
f"**Special conditions:** {sc}",
|
|
699
|
+
f"**Equivalent level of safety (ELOS) findings:** {elos}",
|
|
700
|
+
f"**Status:** {model['status']}",
|
|
701
|
+
f"**Coverage:** {model['coverage']:.0%} of applicable regulations "
|
|
702
|
+
"have a proposed means of compliance and compliance document",
|
|
703
|
+
"",
|
|
704
|
+
"## Certification basis",
|
|
705
|
+
"",
|
|
706
|
+
f"- Aircraft/system: {model['item']}",
|
|
707
|
+
f"- Certification basis reference (TC/STC/amendment level): "
|
|
708
|
+
f"{basis}, {model['certification_path']}",
|
|
709
|
+
f"- Applicable regulations (list with amendment): "
|
|
710
|
+
f"{regs_applicable} (amendment level per the basis)",
|
|
711
|
+
f"- Special conditions: {sc}",
|
|
712
|
+
f"- Equivalent level of safety findings: {elos}",
|
|
713
|
+
"",
|
|
714
|
+
"## Compliance matrix",
|
|
715
|
+
"",
|
|
716
|
+
"| Reg | Applicable? | Means of compliance | Compliance document | "
|
|
717
|
+
"Status (proposed/open) | Owner |",
|
|
718
|
+
"|---|---|---|---|---|---|",
|
|
719
|
+
]
|
|
720
|
+
for row in model["rows"]:
|
|
721
|
+
reg_cell = "%s (Subpart %s - %s)" % (
|
|
722
|
+
row["reg"], row["subpart"], SUBPARTS[row["subpart"]]
|
|
723
|
+
)
|
|
724
|
+
if row["applicable"]:
|
|
725
|
+
app_cell = "YES"
|
|
726
|
+
moc_cell = row["moc_display"]
|
|
727
|
+
if row["safety_assessment"]:
|
|
728
|
+
moc_cell += " (safety assessment required)"
|
|
729
|
+
doc_cell = row["compliance_document"]
|
|
730
|
+
dal_cell = "proposed (severity %s / DAL %s)" % (
|
|
731
|
+
row["severity"], row["dal"]
|
|
732
|
+
)
|
|
733
|
+
owner_cell = row["owner"]
|
|
734
|
+
else:
|
|
735
|
+
app_cell = "NO"
|
|
736
|
+
moc_cell = "n/a"
|
|
737
|
+
doc_cell = "not required (screened out: %s)" % row["reason"]
|
|
738
|
+
dal_cell = "proposed (n/a)"
|
|
739
|
+
owner_cell = row["owner"]
|
|
740
|
+
lines.append(
|
|
741
|
+
"| %s | %s | %s | %s | %s | %s |"
|
|
742
|
+
% (reg_cell, app_cell, moc_cell, doc_cell, dal_cell, owner_cell)
|
|
743
|
+
)
|
|
744
|
+
lines += [
|
|
745
|
+
"",
|
|
746
|
+
"## Notes",
|
|
747
|
+
"",
|
|
748
|
+
"- Status vocabulary: proposed | open | in-work | evidence-available.",
|
|
749
|
+
"- Every row above is a PROPOSED determination by the compliance "
|
|
750
|
+
"engineer (this role). The certification authority makes the binding "
|
|
751
|
+
"applicability and compliance findings.",
|
|
752
|
+
"- NEVER mark a row \"compliant/found\" without evidence - that is "
|
|
753
|
+
"the authority's finding, not ours.",
|
|
754
|
+
"- Severity and DAL context are proposed inputs to the 25.1309 "
|
|
755
|
+
"safety assessment, subject to the system safety process.",
|
|
756
|
+
"",
|
|
757
|
+
"## Open items (gap report)",
|
|
758
|
+
"",
|
|
759
|
+
]
|
|
760
|
+
if model["issues"]:
|
|
761
|
+
lines += ["- " + issue for issue in model["issues"]]
|
|
762
|
+
else:
|
|
763
|
+
lines.append("- No open gaps in this draft; confirm every row with "
|
|
764
|
+
"the certification authority before submission.")
|
|
765
|
+
lines += [
|
|
766
|
+
"",
|
|
767
|
+
"---",
|
|
768
|
+
f"*Generated by Aero Agent Roles airworthiness-compliance-engineer "
|
|
769
|
+
f"core ({model['generated']}). DRAFT - for human compliance engineer "
|
|
770
|
+
"review. Not a compliance finding or an approval.*",
|
|
771
|
+
]
|
|
772
|
+
return "\n".join(lines)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
# Evidence gate checkers: verify a deliverable meets the role's gates
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
GATE_CHECKS = {
|
|
780
|
+
"basis_identified": "certification basis (FAR-25/CS-25) is identified",
|
|
781
|
+
"rows_present": "at least one matrix row exists",
|
|
782
|
+
"regs_real": "every row references a real FAR-25 paragraph",
|
|
783
|
+
"all_applicable_have_moc": "every applicable row has a means of compliance",
|
|
784
|
+
"all_applicable_have_doc": "every applicable row has a compliance document",
|
|
785
|
+
"moc_vocabulary_valid": "means of compliance use the real vocabulary",
|
|
786
|
+
"statuses_honest": "no row claims a compliant/found finding",
|
|
787
|
+
"sign_off_honest": "document is marked draft-for-review, not approval",
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def check_matrix(model: dict) -> dict:
|
|
792
|
+
"""Run the evidence gates against a compliance matrix model."""
|
|
793
|
+
rows = model.get("rows", [])
|
|
794
|
+
applicable = [r for r in rows if r.get("applicable")]
|
|
795
|
+
results = {
|
|
796
|
+
"basis_identified": bool(model.get("certification_basis")),
|
|
797
|
+
"rows_present": bool(rows),
|
|
798
|
+
"regs_real": all(
|
|
799
|
+
r.get("reg") in REGS and r.get("reg") in _REAL_POOL
|
|
800
|
+
for r in rows
|
|
801
|
+
),
|
|
802
|
+
"all_applicable_have_moc": all(
|
|
803
|
+
bool(r.get("methods")) for r in applicable
|
|
804
|
+
),
|
|
805
|
+
"all_applicable_have_doc": all(
|
|
806
|
+
bool(r.get("compliance_document")) for r in applicable
|
|
807
|
+
),
|
|
808
|
+
"moc_vocabulary_valid": all(
|
|
809
|
+
methods_are_valid(r["methods"]) for r in applicable
|
|
810
|
+
),
|
|
811
|
+
"statuses_honest": all(
|
|
812
|
+
(r.get("status") or "").lower() not in FORBIDDEN_STATUS
|
|
813
|
+
for r in rows
|
|
814
|
+
),
|
|
815
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
816
|
+
}
|
|
817
|
+
results["all_pass"] = all(results.values())
|
|
818
|
+
return results
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
# All real paragraphs any reg row may reference (routing check pool).
|
|
822
|
+
_REAL_POOL = [p for area in AREA_PARAGRAPHS.values() for p in area]
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def check_matrix_markdown(md_text: str) -> dict:
|
|
826
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
827
|
+
low = md_text.lower()
|
|
828
|
+
has_reg = bool(re.search(r"\b25\.\d{3,4}\b", md_text))
|
|
829
|
+
checks = {
|
|
830
|
+
"has_title": "airworthiness compliance matrix" in low,
|
|
831
|
+
"has_basis": "certification basis" in low,
|
|
832
|
+
"has_reg_numbers": has_reg,
|
|
833
|
+
"has_table_header": ("| reg |" in low and "means of compliance" in low),
|
|
834
|
+
"has_status_word": "proposed" in low,
|
|
835
|
+
"no_blank_fields": "___" not in md_text,
|
|
836
|
+
"has_draft_marker": "draft" in low,
|
|
837
|
+
"has_not_approval": "not a compliance finding or an approval" in low,
|
|
838
|
+
}
|
|
839
|
+
checks["all_pass"] = all(checks.values())
|
|
840
|
+
return checks
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
844
|
+
"""Public entry point used by gate tooling: check a matrix document."""
|
|
845
|
+
return check_matrix_markdown(md_text)
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
# ---------------------------------------------------------------------------
|
|
849
|
+
# Example project (for tests and worked-example generation)
|
|
850
|
+
# ---------------------------------------------------------------------------
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def example_item() -> AirworthinessItem:
|
|
854
|
+
return AirworthinessItem(
|
|
855
|
+
item_name="Autopilot flight guidance system retrofit (STC)",
|
|
856
|
+
description="Supplemental type certificate for a digital autopilot / "
|
|
857
|
+
"flight guidance system on a transport-category airplane: "
|
|
858
|
+
"autopilot computer, mode selector, servo actuators on "
|
|
859
|
+
"the flight controls, and annunciators. The change is a "
|
|
860
|
+
"major change by someone other than the type certificate "
|
|
861
|
+
"holder, so it takes the supplemental type certificate "
|
|
862
|
+
"(STC) path against the existing FAR-25 basis. The STC "
|
|
863
|
+
"delta touches the systems, flight-controls and equipment "
|
|
864
|
+
"areas; no primary structure is modified.",
|
|
865
|
+
aircraft_type="transport",
|
|
866
|
+
jurisdiction="FAA",
|
|
867
|
+
certification_path="STC",
|
|
868
|
+
change_scope=["systems", "flight-controls", "equipment"],
|
|
869
|
+
regulation_ids=[
|
|
870
|
+
# applicable to the STC delta:
|
|
871
|
+
"25.1301", "25.1309", "25.1329", "25.671", "25.683",
|
|
872
|
+
# screened-out candidates that exercise the applicability logic:
|
|
873
|
+
"25.301", "25.571", "25.607",
|
|
874
|
+
],
|
|
875
|
+
severity_overrides={}, # use the register context severities
|
|
876
|
+
novel_regs=["25.1329"], # new flight guidance feature on the type
|
|
877
|
+
special_conditions=[],
|
|
878
|
+
elos_items=[],
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def example_matrix_markdown() -> str:
|
|
883
|
+
return render_matrix_markdown(build_matrix(example_item()))
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
if __name__ == "__main__":
|
|
887
|
+
item = example_item()
|
|
888
|
+
model = build_matrix(item)
|
|
889
|
+
md = render_matrix_markdown(model)
|
|
890
|
+
print("CERTIFICATION BASIS: %s" % model["certification_basis"])
|
|
891
|
+
print("APPLICABLE REGS: %d/%d" % (model["applicable_count"], model["row_count"]))
|
|
892
|
+
print("COVERAGE: %.2f" % model["coverage"])
|
|
893
|
+
print("GATES: %s" % check_matrix(model))
|
|
894
|
+
print("RENDERED: %d chars" % len(md))
|