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,857 @@
1
+ #!/usr/bin/env python3
2
+ """as9100_core.py - AS9100 Quality / Internal Auditor executable core.
3
+
4
+ This is the role's ENGINE: given a supplier audit's facts (scope,
5
+ schedule facts, record population, and the observed issues), it
6
+ classifies each nonconformity on the audit finding ladder, sizes the
7
+ record sample, computes the audit due date from the risk category,
8
+ scores each corrective-action response against the closure chain, and
9
+ BUILDS the findings report deliverable. It also gate-checks
10
+ deliverables. Standalone: no external repo needed.
11
+
12
+ Domain rules encoded here are paraphrased from the bound AeroSkills
13
+ leaves (manufacturing-quality/as9100/*), which encode AS9100D audit
14
+ practice: the finding ladder (internal-quality-audit), the corrective
15
+ action chain (corrective-action), nonconformance disposition
16
+ discipline (nonconformance-control), calibration practice (7.1.5),
17
+ and the audit focus -> clause mapping (quality). AS9100D text is never
18
+ reproduced - clause ids are identifiers and the report structure is an
19
+ original synthesis of public QMS-audit practice (ISO 19011-style
20
+ evidence discipline).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import calendar
25
+ import math
26
+ import re
27
+ from dataclasses import dataclass, field
28
+ from datetime import date, datetime
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 (paraphrased leaf rules, summary-not-copy)
37
+ # ---------------------------------------------------------------------------
38
+
39
+ # AS9100D inherits the ISO 9001:2015 clause structure: sections 4-10 with
40
+ # aerospace additions inside them. Section names are the public ISO 9001
41
+ # structure; clause ids below are the leaf-verified AS9100D ids the audit
42
+ # criteria cite.
43
+ AS9100D_SECTIONS = [
44
+ ("4", "Context of the organization"),
45
+ ("5", "Leadership"),
46
+ ("6", "Planning"),
47
+ ("7", "Support"),
48
+ ("8", "Operation"),
49
+ ("9", "Performance evaluation"),
50
+ ("10", "Improvement"),
51
+ ]
52
+
53
+ # Clause ids the audit criteria may cite, with the focus each leaf maps to
54
+ # the clause. Only leaf-verified AS9100D clause ids are listed.
55
+ CLAUSE_BY_ID = {
56
+ "6.1": "risks and opportunities",
57
+ "7.1.5": "monitoring and measuring resources (calibration)",
58
+ "8.1.1": "operational risk management",
59
+ "8.1.2": "configuration management",
60
+ "8.1.3": "product safety",
61
+ "8.1.4": "counterfeit parts prevention",
62
+ "8.2": "requirements for products and services (order review)",
63
+ "8.4": "control of externally provided processes, products and services",
64
+ "8.5.1": "control of production and service provision",
65
+ "8.5.1.3": "control of special processes",
66
+ "8.6": "release of products and services",
67
+ "8.7": "control of nonconforming outputs",
68
+ "10.2": "nonconformity and corrective action",
69
+ }
70
+
71
+ # Finding classifications used in audit reporting. The audit leaf's ladder
72
+ # returns major/minor/ofi (ofi = opportunity for improvement); audit report
73
+ # practice labels the ofi grade "observation".
74
+ NC_CLASSIFICATIONS = ("major", "minor", "observation")
75
+
76
+ # Corrective action closure chain, ordered; each stage gates the next
77
+ # (paraphrase of the corrective-action leaf STAGES tuple).
78
+ CA_STAGES = ("containment", "root-cause", "corrective-action", "effectiveness")
79
+
80
+ # Placeholder answers that do not count as a recorded action (leaf rule).
81
+ NON_ANSWERS = frozenset(("", "none", "n/a", "na", "no", "unknown",
82
+ "not-applicable"))
83
+
84
+ # Root cause analysis needs at least this many distinct "why" levels.
85
+ MIN_WHY_DEPTH = 3
86
+
87
+ # Audit program mechanics (leaf rules): a 12-month base interval scaled by
88
+ # process risk, and a square-root record sample scaled by confidence.
89
+ BASE_INTERVAL_MONTHS = 12.0
90
+ RISK_MULTIPLIERS = {"low": 1.5, "medium": 1.0, "high": 0.5}
91
+ CONFIDENCE_ANCHORS = ((0.90, 0.8), (0.95, 1.0), (0.99, 1.2))
92
+
93
+
94
+ def clause_focus(clause_id: str) -> str:
95
+ """Focus name for a known AS9100D clause id."""
96
+ if clause_id not in CLAUSE_BY_ID:
97
+ raise ValueError("unknown AS9100D clause id: %r" % (clause_id,))
98
+ return CLAUSE_BY_ID[clause_id]
99
+
100
+
101
+ # ---------------------------------------------------------------------------
102
+ # NC classification ladder (internal-quality-audit leaf rule)
103
+ # ---------------------------------------------------------------------------
104
+
105
+ def classify_nc(severity_impact: int, systemic: bool, detection: bool) -> str:
106
+ """Classify a nonconformity on the 1-5 impact severity scale.
107
+
108
+ Ladder (leaf rule): "major" when severity impact >= 4 or when the
109
+ condition escaped normal detection so containment of suspect output
110
+ is required; "minor" when severity impact is 2-3, with systemic
111
+ spread escalating minor to major; "observation" when severity impact
112
+ is 1 (the leaf's opportunity for improvement, reported as an
113
+ observation). Raises ValueError when severity_impact is outside 1-5.
114
+
115
+ Args:
116
+ severity_impact: impact of the nonconformity, integer 1-5.
117
+ systemic: whether the same condition is found across other
118
+ records, areas, or product lines.
119
+ detection: whether the nonconformity escaped normal detection
120
+ so containment of suspect output is required.
121
+ """
122
+ if not 1 <= severity_impact <= 5:
123
+ raise ValueError("severity_impact must be an integer in 1-5")
124
+ if severity_impact >= 4 or detection:
125
+ return "major"
126
+ if severity_impact >= 2:
127
+ return "major" if systemic else "minor"
128
+ return "observation"
129
+
130
+
131
+ def nc_classification_reason(severity_impact: int, systemic: bool,
132
+ detection: bool) -> str:
133
+ """One-line rationale for a classification (leaf ladder rules)."""
134
+ cls = classify_nc(severity_impact, systemic, detection)
135
+ if severity_impact >= 4:
136
+ return ("severity impact %d is 4-5, which is a major "
137
+ "nonconformity" % severity_impact)
138
+ if detection:
139
+ return ("the condition escaped normal detection and containment "
140
+ "of suspect output is required, which is a major "
141
+ "nonconformity")
142
+ if severity_impact >= 2 and systemic:
143
+ return ("severity impact %d with systemic spread, which escalates "
144
+ "a minor to a major nonconformity" % severity_impact)
145
+ if severity_impact >= 2:
146
+ return ("severity impact %d is 2-3 with no systemic spread, "
147
+ "which is a minor nonconformity" % severity_impact)
148
+ return ("severity impact 1 is an opportunity for improvement, "
149
+ "reported as an observation")
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Corrective action closure chain (corrective-action leaf rules)
154
+ # ---------------------------------------------------------------------------
155
+
156
+ def _clean(text) -> str:
157
+ return (text or "").strip()
158
+
159
+
160
+ def _is_non_answer(text: str) -> bool:
161
+ return not text or text.lower() in NON_ANSWERS
162
+
163
+
164
+ def containment_ok(containment) -> bool:
165
+ """Containment is recorded and not a placeholder (leaf rule)."""
166
+ return not _is_non_answer(_clean(containment))
167
+
168
+
169
+ def root_cause_chain_ok(whys, min_depth: int = MIN_WHY_DEPTH) -> bool:
170
+ """Root cause chain is long enough and every level is distinct."""
171
+ if not isinstance(whys, (list, tuple)):
172
+ return False
173
+ chain = [_clean(w) for w in whys]
174
+ if len(chain) < min_depth:
175
+ return False
176
+ prev = None
177
+ for w in chain:
178
+ if _is_non_answer(w):
179
+ return False
180
+ if prev is not None and w.lower() == prev.lower():
181
+ return False
182
+ prev = w
183
+ return True
184
+
185
+
186
+ def corrective_action_ok(action) -> bool:
187
+ """Corrective action is recorded and not a placeholder (leaf rule)."""
188
+ return not _is_non_answer(_clean(action))
189
+
190
+
191
+ def effectiveness_evidence_ok(evidence, root_cause_statement=None) -> bool:
192
+ """Effectiveness evidence exists and is not circular (does not simply
193
+ restate the root cause)."""
194
+ e = _clean(evidence)
195
+ if _is_non_answer(e):
196
+ return False
197
+ rc = _clean(root_cause_statement)
198
+ return not (rc and e.lower() == rc.lower())
199
+
200
+
201
+ def corrective_action_status(record: dict) -> str:
202
+ """Closure stage of a corrective action record (leaf rule).
203
+
204
+ Returns one of: containment-missing, root-cause-incomplete,
205
+ corrective-action-missing, effectiveness-pending, closed. Raises
206
+ ValueError for a non-dict record or a missing required key.
207
+ """
208
+ if not isinstance(record, dict):
209
+ raise ValueError("record must be a dict, got %r" % (record,))
210
+ for key in ("problem", "containment", "whys", "corrective_action"):
211
+ if key not in record:
212
+ raise ValueError("record missing required key: %s" % key)
213
+ if not containment_ok(record.get("containment")):
214
+ return "containment-missing"
215
+ if not root_cause_chain_ok(record.get("whys")):
216
+ return "root-cause-incomplete"
217
+ if not corrective_action_ok(record.get("corrective_action")):
218
+ return "corrective-action-missing"
219
+ if not effectiveness_evidence_ok(
220
+ record.get("effectiveness_evidence"),
221
+ record.get("root_cause_statement")):
222
+ return "effectiveness-pending"
223
+ return "closed"
224
+
225
+
226
+ def verify_closure(corrective_action_taken, root_cause_stated,
227
+ effectiveness_check) -> bool:
228
+ """True only when all three closure evidence elements are set."""
229
+ return bool(corrective_action_taken and root_cause_stated
230
+ and effectiveness_check)
231
+
232
+
233
+ # ---------------------------------------------------------------------------
234
+ # Audit program numbers (internal-quality-audit leaf rules)
235
+ # ---------------------------------------------------------------------------
236
+
237
+ def _add_calendar_months(day: date, total_months: int) -> date:
238
+ """Add whole calendar months, clamping the day to the month end."""
239
+ month_index = day.year * 12 + (day.month - 1) + total_months
240
+ year, zero_month = divmod(month_index, 12)
241
+ month = zero_month + 1
242
+ last_day = calendar.monthrange(year, month)[1]
243
+ return date(year, month, min(day.day, last_day))
244
+
245
+
246
+ def audit_due_date(last_audit_date_iso: str, risk_category: str,
247
+ base_interval_months: float = BASE_INTERVAL_MONTHS) -> str:
248
+ """Due date of the next audit as an ISO date string.
249
+
250
+ The span is base_interval_months times the risk multiplier for the
251
+ process risk category (high risk processes are audited more often),
252
+ added as calendar months with the day clamped to the target month
253
+ end. Raises ValueError for a malformed date or unknown category.
254
+ """
255
+ if risk_category not in RISK_MULTIPLIERS:
256
+ raise ValueError("risk_category must be one of: low, medium, high")
257
+ try:
258
+ last_date = datetime.strptime(last_audit_date_iso,
259
+ "%Y-%m-%d").date()
260
+ except (TypeError, ValueError):
261
+ raise ValueError("last_audit_date_iso must be an ISO date "
262
+ "string YYYY-MM-DD") from None
263
+ span = base_interval_months * RISK_MULTIPLIERS[risk_category]
264
+ return _add_calendar_months(last_date, int(round(span))).isoformat()
265
+
266
+
267
+ def _confidence_factor(confidence_level: float) -> float:
268
+ """Sample factor at the confidence anchors, interpolated between."""
269
+ for anchor, factor in CONFIDENCE_ANCHORS:
270
+ if confidence_level == anchor:
271
+ return factor
272
+ if confidence_level <= 0.90:
273
+ return 0.8
274
+ if confidence_level <= 0.95:
275
+ factor = 0.8 + (confidence_level - 0.90) * 4.0
276
+ elif confidence_level <= 0.99:
277
+ factor = 1.0 + (confidence_level - 0.95) * 5.0
278
+ else:
279
+ factor = 1.2
280
+ return round(factor, 9)
281
+
282
+
283
+ def audit_sample_size(lot_size: int, confidence_level: float = 0.95) -> int:
284
+ """Sample size for a records audit: ceil(sqrt(lot_size) * factor).
285
+
286
+ Square-root sample scaled by the confidence factor (1.0 at 0.95,
287
+ 0.8 at 0.90, 1.2 at 0.99, interpolated between) and rounded up,
288
+ with a floor of 1. Raises ValueError for lot_size below 1 or a
289
+ confidence level outside [0.5, 0.999].
290
+ """
291
+ if lot_size < 1:
292
+ raise ValueError("lot_size must be at least 1")
293
+ if not 0.5 <= confidence_level <= 0.999:
294
+ raise ValueError("confidence_level must be within [0.5, 0.999]")
295
+ factor = _confidence_factor(confidence_level)
296
+ size = math.ceil(round(math.sqrt(lot_size) * factor, 9))
297
+ return max(1, size)
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # Audit item + findings model
302
+ # ---------------------------------------------------------------------------
303
+
304
+ @dataclass
305
+ class AuditItem:
306
+ """Project facts the role needs to build the findings report.
307
+
308
+ issues is a list of observed nonconformities, each a dict with:
309
+ clause (AS9100D id), title, objective_evidence, requirement,
310
+ severity_impact (1-5), systemic (bool), detection (bool), and
311
+ corrective_action (dict with problem, containment, whys,
312
+ corrective_action, effectiveness_evidence, root_cause_statement)
313
+ - observations carry no corrective_action entry.
314
+ """
315
+ supplier_name: str
316
+ audited_processes: list = field(default_factory=list)
317
+ audit_date: str = ""
318
+ auditor: str = ""
319
+ auditee_owner: str = "" # area owner; independence is checked
320
+ process_risk: str = "medium"
321
+ record_population: int = 0
322
+ confidence_level: float = 0.95
323
+ criteria: list = field(default_factory=list)
324
+ issues: list = field(default_factory=list)
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Findings model builder: produces the actual deliverable content
329
+ # ---------------------------------------------------------------------------
330
+
331
+ def _nc_id(index: int) -> str:
332
+ return "NC-%02d" % index
333
+
334
+
335
+ def _classify_issues(item: AuditItem) -> list:
336
+ """Classify every issue and attach its corrective action record."""
337
+ findings = []
338
+ for i, issue in enumerate(item.issues, start=1):
339
+ clause = issue["clause"]
340
+ if clause not in CLAUSE_BY_ID:
341
+ raise ValueError("issue cites unknown AS9100D clause id: %r"
342
+ % (clause,))
343
+ severity = issue["severity_impact"]
344
+ systemic = bool(issue.get("systemic"))
345
+ detection = bool(issue.get("detection"))
346
+ classification = classify_nc(severity, systemic, detection)
347
+ ca = issue.get("corrective_action")
348
+ if ca is not None and not isinstance(ca, dict):
349
+ raise ValueError("corrective_action for %s must be a dict "
350
+ "or None" % _nc_id(i))
351
+ findings.append({
352
+ "nc_id": _nc_id(i),
353
+ "clause": clause,
354
+ "title": issue["title"],
355
+ "classification": classification,
356
+ "classification_reason": nc_classification_reason(
357
+ severity, systemic, detection),
358
+ "severity_impact": severity,
359
+ "systemic": systemic,
360
+ "detection": detection,
361
+ "objective_evidence": issue["objective_evidence"],
362
+ "requirement": issue["requirement"],
363
+ "corrective_action": ca,
364
+ })
365
+ return findings
366
+
367
+
368
+ def audit_findings(item: AuditItem) -> dict:
369
+ """Build the complete findings-report content model from audit facts.
370
+
371
+ Classifies each issue on the finding ladder, scores each corrective
372
+ action record on the closure chain, sizes the record sample, and
373
+ computes the next audit due date from the risk category.
374
+ """
375
+ findings = _classify_issues(item)
376
+ counts = {"major": 0, "minor": 0, "observation": 0}
377
+ for f in findings:
378
+ counts[f["classification"]] += 1
379
+ counts["total"] = len(findings)
380
+ for f in findings:
381
+ ca = f.get("corrective_action")
382
+ f["ca_status"] = (corrective_action_status(ca) if ca
383
+ else "not-required")
384
+ due = audit_due_date(item.audit_date, item.process_risk)
385
+ sample = audit_sample_size(item.record_population,
386
+ item.confidence_level)
387
+ return {
388
+ "document_type": "AS9100 Internal Audit Findings Report",
389
+ "status": "draft-for-review",
390
+ "supplier": item.supplier_name,
391
+ "audit": {
392
+ "audit_date": item.audit_date,
393
+ "auditor": item.auditor,
394
+ "auditee_owner": item.auditee_owner,
395
+ "auditor_independent": item.auditor.strip().lower()
396
+ != item.auditee_owner.strip().lower(),
397
+ "audited_processes": list(item.audited_processes),
398
+ "criteria": list(item.criteria),
399
+ "process_risk": item.process_risk,
400
+ "next_audit_due": due,
401
+ },
402
+ "sampling": {
403
+ "population": item.record_population,
404
+ "confidence_level": item.confidence_level,
405
+ "sample_size": sample,
406
+ "sample_note": ("%d of %d records sampled at %.2f confidence "
407
+ "(square-root rule)"
408
+ % (sample, item.record_population,
409
+ item.confidence_level)),
410
+ },
411
+ "clauses_cited": sorted({f["clause"] for f in findings}),
412
+ "findings": findings,
413
+ "counts": counts,
414
+ "closure_recommendation": (
415
+ "PROPOSED - the QA manager signs audit closure; NC-01/02 "
416
+ "effectiveness verification at the follow-up audit is "
417
+ "required before any closure is recorded"),
418
+ "generated": _today(),
419
+ }
420
+
421
+
422
+ def build_findings(item: AuditItem) -> dict:
423
+ """Synonym for audit_findings (build_ naming parity with the role
424
+ pattern); the engine produces the identical content model."""
425
+ return audit_findings(item)
426
+
427
+
428
+ def _table_cell(text) -> str:
429
+ """Escape a cell for a markdown table (pipes and newlines)."""
430
+ return (text or "").replace("|", "\\|").replace("\n", " ").strip()
431
+
432
+
433
+ def nc_row(nc_id: str, clause: str, classification: str,
434
+ objective_evidence: str, requirement: str) -> list:
435
+ """One findings-table row as a list of cell strings."""
436
+ return [_table_cell(nc_id), _table_cell(clause),
437
+ _table_cell(classification), _table_cell(objective_evidence),
438
+ _table_cell(requirement)]
439
+
440
+
441
+ def _ca_text(ca: dict, key: str) -> str:
442
+ if not ca:
443
+ return ""
444
+ return _table_cell(ca.get(key, ""))
445
+
446
+
447
+ # ---------------------------------------------------------------------------
448
+ # Renderer: findings report markdown deliverable
449
+ # ---------------------------------------------------------------------------
450
+
451
+ def render_findings_markdown(model: dict) -> str:
452
+ """Render the findings content model as the deliverable markdown."""
453
+ a = model["audit"]
454
+ s = model["sampling"]
455
+ c = model["counts"]
456
+ lines = [
457
+ "# AS9100 Internal Audit Findings Report",
458
+ "",
459
+ f"**Supplier:** {model['supplier']}",
460
+ f"**Audit date:** {a['audit_date']}",
461
+ f"**Auditor:** {a['auditor']} "
462
+ f"(independent of area owner {a['auditee_owner']}: "
463
+ f"{'yes' if a['auditor_independent'] else 'NO - independence failure'})",
464
+ f"**Status:** {model['status']}",
465
+ "",
466
+ "## Audit plan",
467
+ "",
468
+ "- Audit scope: " + ", ".join(a["audited_processes"]) + ".",
469
+ "- Criteria: " + ", ".join(a["criteria"]) + ".",
470
+ f"- Audit team: {a['auditor']} (lead auditor).",
471
+ f"- Process risk category: {a['process_risk']}; next audit due "
472
+ f"{a['next_audit_due']} (risk-scaled 12-month base interval).",
473
+ f"- Records sampled: {s['sample_note']}.",
474
+ "",
475
+ "## Findings summary",
476
+ "",
477
+ f"- Major nonconformities: {c['major']}",
478
+ f"- Minor nonconformities: {c['minor']}",
479
+ f"- Observations (opportunities for improvement): {c['observation']}",
480
+ f"- Total findings: {c['total']}",
481
+ f"- AS9100D clauses cited: {', '.join(model['clauses_cited'])}.",
482
+ "",
483
+ "## Findings report",
484
+ "",
485
+ "### Nonconformities",
486
+ "",
487
+ "| NC ID | Clause | Classification | Objective evidence | Requirement |",
488
+ "|---|---|---|---|---|",
489
+ ]
490
+ for f in model["findings"]:
491
+ row = nc_row(f["nc_id"], f["clause"], f["classification"],
492
+ f["objective_evidence"], f["requirement"])
493
+ lines.append("| " + " | ".join(row) + " |")
494
+ lines += [
495
+ "",
496
+ "### Finding details",
497
+ "",
498
+ ]
499
+ for f in model["findings"]:
500
+ ca = f.get("corrective_action")
501
+ lines += [
502
+ f"#### {f['nc_id']} - {f['classification'].upper()} "
503
+ f"(clause {f['clause']})",
504
+ "",
505
+ f"**Title:** {f['title']}",
506
+ f"**Classification rationale:** {f['classification_reason']}.",
507
+ f"**Objective evidence:** {f['objective_evidence']}",
508
+ f"**Requirement violated:** {f['requirement']}",
509
+ ]
510
+ if ca:
511
+ lines += [
512
+ "",
513
+ "Corrective-action record (closure chain status: "
514
+ + f["ca_status"] + "):",
515
+ f"- Containment: {ca.get('containment', '')}",
516
+ f"- Root cause: {' > '.join(ca.get('whys', []))}",
517
+ f"- Corrective action: {ca.get('corrective_action', '')}",
518
+ f"- Effectiveness evidence: "
519
+ f"{ca.get('effectiveness_evidence') or 'pending - not yet recorded'}",
520
+ ]
521
+ else:
522
+ lines += [
523
+ "",
524
+ f"No corrective action required (observation, clause "
525
+ f"{f['clause']}); improvement tracked separately.",
526
+ ]
527
+ lines.append("")
528
+ lines += [
529
+ "### Observations (no violation found, improvement note)",
530
+ "",
531
+ ]
532
+ for f in model["findings"]:
533
+ if f["classification"] == "observation":
534
+ lines.append(
535
+ f"- {f['nc_id']} (clause {f['clause']}): {f['title']}. "
536
+ f"{f['objective_evidence']}")
537
+ lines += [
538
+ "",
539
+ "### Corrective-action follow-up",
540
+ "",
541
+ "| NC ID | Root cause | Containment | Corrective action | Verification | Status |",
542
+ "|---|---|---|---|---|---|",
543
+ ]
544
+ for f in model["findings"]:
545
+ ca = f.get("corrective_action")
546
+ if not ca:
547
+ lines.append(
548
+ "| " + f["nc_id"] + " | n/a - observation | n/a - "
549
+ "observation | n/a - observation | n/a - observation | "
550
+ "observation |")
551
+ continue
552
+ verification = (ca.get("effectiveness_evidence")
553
+ or "pending - effectiveness not yet verified")
554
+ lines.append(
555
+ "| " + f["nc_id"] + " | "
556
+ + _ca_text(ca, "root_cause_statement") + " | "
557
+ + _ca_text(ca, "containment") + " | "
558
+ + _ca_text(ca, "corrective_action") + " | "
559
+ + _table_cell(verification) + " | "
560
+ + f["ca_status"] + " |")
561
+ lines += [
562
+ "",
563
+ "## Closure",
564
+ "",
565
+ f"- Closure recommendation: {model['closure_recommendation']}.",
566
+ f"- No finding is recorded closed: closure requires the corrective "
567
+ f"action taken, the root cause statement and the effectiveness "
568
+ f"check all on file (verification gate).",
569
+ "",
570
+ "---",
571
+ f"*DRAFT - audit evidence report generated by Aero Agent Roles "
572
+ f"as9100-quality-auditor core ({model['generated']}) for human QA "
573
+ f"review. Not a certification, supplier-approval, or closure "
574
+ f"decision; the QA manager signs closure.*",
575
+ ]
576
+ return "\n".join(lines)
577
+
578
+
579
+ # ---------------------------------------------------------------------------
580
+ # Evidence gate checkers: verify a deliverable meets the role's gates
581
+ # ---------------------------------------------------------------------------
582
+
583
+ def check_findings(model: dict) -> dict:
584
+ """Run the evidence gates against a findings model."""
585
+ audit = model.get("audit", {})
586
+ findings = model.get("findings", [])
587
+ major_minor = [f for f in findings
588
+ if f.get("classification") in ("major", "minor")]
589
+ results = {
590
+ "scope_identified": bool(model.get("supplier")) and bool(
591
+ audit.get("audited_processes")),
592
+ "criteria_stated": any("as9100" in str(x).lower()
593
+ for x in audit.get("criteria", [])),
594
+ "clauses_real": all(f.get("clause") in CLAUSE_BY_ID
595
+ for f in findings),
596
+ "nc_classified": all(f.get("classification") in NC_CLASSIFICATIONS
597
+ for f in findings),
598
+ "evidence_present": all(
599
+ f.get("clause") and f.get("objective_evidence")
600
+ and f.get("requirement") for f in findings),
601
+ "ca_chain_present": all(
602
+ isinstance(f.get("corrective_action"), dict)
603
+ and containment_ok(f["corrective_action"].get("containment"))
604
+ and root_cause_chain_ok(f["corrective_action"].get("whys"))
605
+ and corrective_action_ok(
606
+ f["corrective_action"].get("corrective_action"))
607
+ for f in major_minor),
608
+ "no_closure_claimed": all(
609
+ f.get("ca_status") != "closed" for f in findings),
610
+ "closure_honest": "proposed" in str(
611
+ model.get("closure_recommendation", "")).lower(),
612
+ "sign_off_honest": model.get("status") == "draft-for-review",
613
+ }
614
+ results["all_pass"] = all(results.values())
615
+ return results
616
+
617
+
618
+ def check_findings_markdown(md_text: str) -> dict:
619
+ """Gate-check the rendered markdown deliverable."""
620
+ low = md_text.lower()
621
+ checks = {
622
+ "has_title": "as9100 internal audit findings report" in low,
623
+ "has_supplier": "**supplier:**" in low,
624
+ "has_clause_refs": "clause" in low and "nc-01" in low,
625
+ "has_classifications": all(k in low
626
+ for k in ("major", "minor", "observation")),
627
+ "has_evidence": "objective evidence" in low and "requirement" in low,
628
+ "has_ca_chain": all(k in low for k in (
629
+ "root cause", "containment", "corrective action", "verification")),
630
+ "has_proposed_closure": "proposed" in low,
631
+ "has_draft_marker": "draft" in low,
632
+ "not_approval": "not a" in low and "approval" in low,
633
+ }
634
+ checks["all_pass"] = all(checks.values())
635
+ return checks
636
+
637
+
638
+ def validate_deliverable_text(md_text: str) -> dict:
639
+ """Public entry point used by gate tooling: check a findings report."""
640
+ return check_findings_markdown(md_text)
641
+
642
+
643
+ # ---------------------------------------------------------------------------
644
+ # Example audit (for tests and worked-example generation)
645
+ # ---------------------------------------------------------------------------
646
+
647
+ def example_item() -> AuditItem:
648
+ """Synthetic supplier audit: AeroForge Precision Machining, LLC.
649
+
650
+ Five observed issues exercise the whole finding ladder: severity 5
651
+ major (8.7), systemic minor escalated to major (10.2), two minors
652
+ (7.1.5, 8.1.2), and one observation (6.1). All clause ids are
653
+ leaf-verified AS9100D ids.
654
+ """
655
+ return AuditItem(
656
+ supplier_name="AeroForge Precision Machining, LLC",
657
+ audited_processes=[
658
+ "CNC machining and production control (8.5.1)",
659
+ "inspection and test (8.6)",
660
+ "calibration control (7.1.5)",
661
+ "nonconformance disposition (8.7)",
662
+ "corrective action (10.2)",
663
+ ],
664
+ audit_date="2026-09-05",
665
+ auditor="M. Reyes",
666
+ auditee_owner="J. Torres",
667
+ process_risk="high",
668
+ record_population=400,
669
+ confidence_level=0.95,
670
+ criteria=[
671
+ "AS9100D clauses (6.1, 7.1.5, 8.1.2, 8.6, 8.7, 10.2)",
672
+ "customer requirements flow-down, PO-7712",
673
+ ],
674
+ issues=[
675
+ {
676
+ "clause": "8.7",
677
+ "title": ("Use-as-is disposition on a safety-critical part "
678
+ "without customer approval"),
679
+ "objective_evidence": ("NCR-2214: bracket P/N BN-4470-2 "
680
+ "(safety-critical, flight control linkage) dispositioned "
681
+ "use-as-is by the supplier MRB; no customer waiver on "
682
+ "file and the disposition record has no customer-approval "
683
+ "entry. Twelve brackets of the same lot shipped under "
684
+ "delivery DLV-3390."),
685
+ "requirement": ("Nonconforming output must be identified, "
686
+ "segregated, and dispositioned by an authorized "
687
+ "authority; use-as-is outside the original specification "
688
+ "requires customer approval."),
689
+ "severity_impact": 5,
690
+ "systemic": True,
691
+ "detection": True,
692
+ "corrective_action": {
693
+ "problem": ("Use-as-is release of safety-critical "
694
+ "brackets without customer approval"),
695
+ "containment": ("Quarantine the twelve shipped brackets "
696
+ "of lot 2214B at the customer site; "
697
+ "suspend MRB use-as-is disposition "
698
+ "until the procedure is corrected."),
699
+ "whys": [
700
+ "The MRB disposition procedure has no "
701
+ "customer-approval step for use-as-is",
702
+ "The procedure was written when the MRB handled "
703
+ "non-aerospace parts only",
704
+ "Customer approval flow was never added when the "
705
+ "aerospace order book opened",
706
+ "No checklist gate sits between the disposition "
707
+ "decision and product release",
708
+ ],
709
+ "corrective_action": ("Revise the disposition procedure "
710
+ "to require customer approval before any use-as-is "
711
+ "release and add a release checklist gate; retrain "
712
+ "the MRB board."),
713
+ "effectiveness_evidence": "",
714
+ "root_cause_statement": ("use-as-is release without "
715
+ "customer approval on safety-critical parts"),
716
+ },
717
+ },
718
+ {
719
+ "clause": "10.2",
720
+ "title": ("Recurring nonconformity with no root cause or "
721
+ "effectiveness verification"),
722
+ "objective_evidence": ("NCR-2210/2211/2214 record the same "
723
+ "oversize-bore characteristic within six months. CAPA-118 "
724
+ "was closed with effectiveness evidence restating the "
725
+ "root cause rather than an observed result."),
726
+ "requirement": ("On nonconformity, the organization must "
727
+ "react, evaluate the need for action to eliminate the "
728
+ "cause so it does not recur, and verify the "
729
+ "effectiveness of the action taken."),
730
+ "severity_impact": 3,
731
+ "systemic": True,
732
+ "detection": False,
733
+ "corrective_action": {
734
+ "problem": ("Same oversize-bore characteristic recurs "
735
+ "across three NCRs in six months"),
736
+ "containment": ("100% bore inspection of the affected "
737
+ "part numbers in stock and in work."),
738
+ "whys": [
739
+ "Tool wear compensation is not verified between "
740
+ "operator shifts",
741
+ "The compensation check is a manual step with no "
742
+ "scheduled trigger",
743
+ "The CNC program pauses at shift change but no "
744
+ "measurement is required before restart",
745
+ ],
746
+ "corrective_action": ("Add an in-process bore "
747
+ "measurement gate at every shift change, tied to "
748
+ "the SPC record for the characteristic."),
749
+ "effectiveness_evidence": "",
750
+ "root_cause_statement": ("oversize-bore recurrence from "
751
+ "unverified tool-wear compensation at shift change"),
752
+ },
753
+ },
754
+ {
755
+ "clause": "7.1.5",
756
+ "title": ("Calibration sticker expired on a torque wrench "
757
+ "in the tool crib"),
758
+ "objective_evidence": ("Torque wrench TQ-009 in the "
759
+ "assembly tool crib carried a calibration sticker due "
760
+ "2026-08-30; the crib sign-out log shows no issue "
761
+ "after the sticker expiry date."),
762
+ "requirement": ("Monitoring and measuring resources must "
763
+ "be calibrated or verified at defined intervals; "
764
+ "suspect measurements must be assessed for validity."),
765
+ "severity_impact": 2,
766
+ "systemic": False,
767
+ "detection": False,
768
+ "corrective_action": {
769
+ "problem": ("Torque wrench TQ-009 remained in the "
770
+ "active tool crib after its calibration "
771
+ "interval expired"),
772
+ "containment": ("Remove TQ-009 to calibration; verify "
773
+ "the sign-out log to bound the period "
774
+ "of possible use."),
775
+ "whys": [
776
+ "No recall alert fired when the interval expired",
777
+ "Calibration due dates are tracked on a paper "
778
+ "log reviewed monthly",
779
+ "The monthly review ran two weeks late",
780
+ ],
781
+ "corrective_action": ("Move calibration due-date "
782
+ "tracking to the shop system with an automatic "
783
+ "recall alert at interval expiry."),
784
+ "effectiveness_evidence": "",
785
+ "root_cause_statement": ("expired calibration sticker "
786
+ "from a late monthly due-date review"),
787
+ },
788
+ },
789
+ {
790
+ "clause": "8.1.2",
791
+ "title": ("Configuration baseline register not updated for "
792
+ "approved engineering change ECO-4821"),
793
+ "objective_evidence": ("ECO-4821 (drawing BN-4470 rev A to "
794
+ "rev B) approved 2026-08-10; the configuration baseline "
795
+ "register entry for BN-4470 still shows rev A as of the "
796
+ "audit date. No nonconforming product resulted."),
797
+ "requirement": ("Configuration management must establish "
798
+ "and maintain a configuration baseline and control "
799
+ "changes to it."),
800
+ "severity_impact": 3,
801
+ "systemic": False,
802
+ "detection": False,
803
+ "corrective_action": {
804
+ "problem": ("Configuration baseline register lagging "
805
+ "the approved ECO-4821"),
806
+ "containment": ("Update the baseline register for "
807
+ "BN-4470 rev B; audit open ECOs against "
808
+ "the register."),
809
+ "whys": [
810
+ "Baseline register updates are batched monthly",
811
+ "No trigger links ECO approval to the register "
812
+ "update",
813
+ "The register owner has no change-approval "
814
+ "notification",
815
+ ],
816
+ "corrective_action": ("Link ECO approval records to the "
817
+ "baseline register so approved changes update the "
818
+ "register at approval time."),
819
+ "effectiveness_evidence": "",
820
+ "root_cause_statement": ("baseline register updated "
821
+ "monthly instead of at ECO approval"),
822
+ },
823
+ },
824
+ {
825
+ "clause": "6.1",
826
+ "title": ("Opportunity: risk register entries lack "
827
+ "post-mitigation residual scoring"),
828
+ "objective_evidence": ("The operational risk register is "
829
+ "complete and current; mitigation actions for the "
830
+ "coating process change are recorded but no residual "
831
+ "RPN re-score follows the recorded reductions."),
832
+ "requirement": ("No violation found; the register meets "
833
+ "clause 6.1. Re-scoring residual risk closes the "
834
+ "mitigation loop demonstrated elsewhere in the QMS."),
835
+ "severity_impact": 1,
836
+ "systemic": False,
837
+ "detection": False,
838
+ "corrective_action": None,
839
+ },
840
+ ],
841
+ )
842
+
843
+
844
+ def example_findings_markdown() -> str:
845
+ return render_findings_markdown(audit_findings(example_item()))
846
+
847
+
848
+ if __name__ == "__main__":
849
+ item = example_item()
850
+ model = audit_findings(item)
851
+ md = render_findings_markdown(model)
852
+ print("SUPPLIER: %s" % model["supplier"])
853
+ print("SAMPLE SIZE: %d" % model["sampling"]["sample_size"])
854
+ print("NEXT AUDIT DUE: %s" % model["audit"]["next_audit_due"])
855
+ print("COUNTS: %s" % model["counts"])
856
+ print("GATES: %s" % check_findings(model))
857
+ print("RENDERED: %d chars" % len(md))