11"""PFMEA <-> Control Plan consistency checker (MVP module).
22
3- This is the single module shipped in v0.1.0. It parses both documents, matches rows by operation,
4- applies the explicit checks below and returns a scored `ValidationResult`. Each check is intentionally
5- simple and documented; the tool surfaces *potential* findings for a human to judge.
3+ This is the single module shipped in v0.1. It parses both documents, matches rows by operation,
4+ applies the explicit checks below and returns a scored `ValidationResult`. Each check is
5+ intentionally simple and documented; the tool surfaces *potential* findings for a human to judge.
66
7- Finding types implemented:
7+ Rule **metadata** (severity + message template) is loaded from
8+ `rules/pfmea_control_plan_rules.yaml`; the **detection logic** for each finding type stays in this
9+ module (this is not a generic rule engine).
10+
11+ Finding types:
812- UNMATCHED_PROCESS_STEP (warning) operation present in one document only
913- MISSING_CONTROL (critical) matched operation has no control method
1014- SPECIAL_CHARACTERISTIC_NOT_CONTROLLED (critical) PFMEA special char not marked in Control Plan
2125from ..core .matching import MatchResult , match_rows
2226from ..models import ControlPlanRow , Finding , PFMEARow , ValidationResult
2327from ..parsers .excel import parse_control_plan , parse_pfmea
28+ from ..rules import load_rule_specs
2429
2530HIGH_SEVERITY_THRESHOLD = 8
26-
27- # Phrases that indicate a subjective / low-reliability detection method. Kept deliberately
28- # specific (phrases, not bare words like "operator" or "manual") to limit false positives:
29- # e.g. "manual gauge" or "operator runs CMM" are NOT weak, but "manual inspection" is.
30- # Because these checks are the most false-positive-prone, both rules they feed are WARNINGS (D3).
3131WEAK_METHOD_KEYWORDS = (
3232 "visual" ,
3333 "by eye" ,
3939 "manual check" ,
4040)
4141
42+ # Rule metadata (severity + message template) is the single source of truth in the YAML.
43+ _RULES = load_rule_specs ()
44+
45+
46+ def _make (rule_id : str , op : str , ** context : object ) -> Finding :
47+ """Build a Finding using the YAML metadata for severity and the message template."""
48+ spec = _RULES [rule_id ]
49+ return Finding (
50+ finding_type = rule_id ,
51+ level = spec ["severity" ],
52+ operation_id = op ,
53+ message = spec ["message_template" ].format (op = op , ** context ),
54+ )
55+
4256
4357def _is_weak_method (method : str | None ) -> bool :
4458 if not method :
@@ -68,69 +82,21 @@ def _check_operation(
6882 weak = any (_is_weak_method (m ) for m in control_methods )
6983
7084 if not has_control :
71- findings .append (
72- Finding (
73- finding_type = "MISSING_CONTROL" ,
74- level = "critical" ,
75- operation_id = op_label ,
76- message = (
77- f"Operation { op_label } has PFMEA failure mode(s) but no control method "
78- f"in the Control Plan."
79- ),
80- )
81- )
85+ findings .append (_make ("MISSING_CONTROL" , op_label ))
8286
8387 if pf_special and not cp_special :
84- findings .append (
85- Finding (
86- finding_type = "SPECIAL_CHARACTERISTIC_NOT_CONTROLLED" ,
87- level = "critical" ,
88- operation_id = op_label ,
89- message = (
90- f"Operation { op_label } is flagged as a special characteristic in the PFMEA "
91- f"but is not marked/controlled as special in the Control Plan."
92- ),
93- )
94- )
88+ findings .append (_make ("SPECIAL_CHARACTERISTIC_NOT_CONTROLLED" , op_label ))
9589
9690 if has_control and not has_reaction and max_sev is not None and max_sev >= HIGH_SEVERITY_THRESHOLD :
97- findings .append (
98- Finding (
99- finding_type = "MISSING_REACTION_PLAN" ,
100- level = "critical" ,
101- operation_id = op_label ,
102- message = (
103- f"Operation { op_label } has a high-severity failure mode (S={ max_sev } ) "
104- f"but the Control Plan control has no reaction plan."
105- ),
106- )
107- )
91+ findings .append (_make ("MISSING_REACTION_PLAN" , op_label , severity = max_sev ))
10892
10993 if weak :
11094 findings .append (
111- Finding (
112- finding_type = "WEAK_DETECTION_METHOD" ,
113- level = "warning" ,
114- operation_id = op_label ,
115- message = (
116- f"Operation { op_label } relies on a weak detection method "
117- f"({ ', ' .join (control_methods )} )."
118- ),
119- )
95+ _make ("WEAK_DETECTION_METHOD" , op_label , methods = ", " .join (control_methods ))
12096 )
12197
12298 if weak and max_sev is not None and max_sev >= HIGH_SEVERITY_THRESHOLD :
123- findings .append (
124- Finding (
125- finding_type = "HIGH_SEVERITY_WEAK_CONTROL" ,
126- level = "warning" ,
127- operation_id = op_label ,
128- message = (
129- f"Operation { op_label } has a high-severity failure mode (S={ max_sev } ) "
130- f"paired with a weak control method."
131- ),
132- )
133- )
99+ findings .append (_make ("HIGH_SEVERITY_WEAK_CONTROL" , op_label , severity = max_sev ))
134100
135101 return findings
136102
@@ -142,22 +108,12 @@ def evaluate(match: MatchResult) -> list[Finding]:
142108 for key in match .pfmea_only_ops :
143109 op = match .display_op (key )
144110 findings .append (
145- Finding (
146- finding_type = "UNMATCHED_PROCESS_STEP" ,
147- level = "warning" ,
148- operation_id = op ,
149- message = f"PFMEA operation { op } has no matching row in the Control Plan." ,
150- )
111+ _make ("UNMATCHED_PROCESS_STEP" , op , source = "PFMEA" , target = "Control Plan" )
151112 )
152113 for key in match .control_only_ops :
153114 op = match .display_op (key )
154115 findings .append (
155- Finding (
156- finding_type = "UNMATCHED_PROCESS_STEP" ,
157- level = "warning" ,
158- operation_id = op ,
159- message = f"Control Plan operation { op } has no matching row in the PFMEA." ,
160- )
116+ _make ("UNMATCHED_PROCESS_STEP" , op , source = "Control Plan" , target = "PFMEA" )
161117 )
162118
163119 for key in match .matched_ops :
0 commit comments