forked from ChelseaKR/ceqa-preflight
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrule_engine.py
More file actions
129 lines (100 loc) · 4.2 KB
/
Copy pathrule_engine.py
File metadata and controls
129 lines (100 loc) · 4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""Deterministic execution of declarative CEQA Preflight rules."""
from __future__ import annotations
from collections.abc import Callable, Iterable, Mapping
from enum import StrEnum
from typing import Any
from pydantic import Field
from ceqa_preflight.models import (
Confidence,
Evidence,
FilingType,
Finding,
FindingStatus,
StrictModel,
)
from ceqa_preflight.rule_catalog import RuleCatalog, RuleDefinition, RuleLifecycle
class RuleOutcomeStatus(StrEnum):
"""Check-local outcomes before conservative report-status mapping."""
# This is a report outcome, not a credential.
PASS = "pass" # nosec B105
WARNING = "warning"
FAILURE = "failure"
INDETERMINATE = "indeterminate"
class RuleContext(StrictModel):
"""Typed non-secret facts supplied to rule functions."""
filing_type: FilingType
facts: dict[str, Any] = Field(default_factory=dict)
class RuleOutcome(StrictModel):
"""A check result that is transformed into a stable report finding."""
status: RuleOutcomeStatus
message: str = Field(min_length=1)
document: str | None = None
page: int | None = Field(default=None, ge=1)
field: str | None = None
evidence: Evidence = Field(default_factory=Evidence)
remediation: str = Field(min_length=1)
confidence: Confidence = Confidence.HIGH
class RuleRun(StrictModel):
"""Results and process status for a complete deterministic rule run."""
findings: list[Finding] = Field(default_factory=list)
exit_code: int = Field(ge=0, le=2)
RuleCheck = Callable[[RuleContext, RuleDefinition], Iterable[RuleOutcome]]
class RuleEngine:
"""Runs only allow-listed rule functions in source order of the catalog."""
def __init__(self, catalog: RuleCatalog, registry: Mapping[str, RuleCheck]) -> None:
self._catalog = catalog
self._registry = dict(registry)
unknown = sorted({rule.check for rule in catalog.rules if rule.check not in self._registry})
if unknown:
raise ValueError(f"catalog references unknown check names: {', '.join(unknown)}")
def run(self, context: RuleContext, *, include_experimental: bool = False) -> RuleRun:
"""Run applicable rules in catalog order, isolating individual check failures."""
findings: list[Finding] = []
exit_code = 0
for rule in self._catalog.rules:
if not _is_enabled(rule, context.filing_type, include_experimental):
continue
try:
outcomes = self._registry[rule.check](context, rule)
findings.extend(_finding_from_outcome(rule, outcome) for outcome in outcomes)
except Exception:
findings.append(_internal_error_finding(rule))
exit_code = 2
return RuleRun(findings=findings, exit_code=exit_code)
def _is_enabled(rule: RuleDefinition, filing_type: FilingType, include_experimental: bool) -> bool:
if filing_type not in rule.filing_types:
return False
if rule.lifecycle is RuleLifecycle.ACTIVE:
return True
return rule.lifecycle is RuleLifecycle.EXPERIMENTAL and include_experimental
def _finding_from_outcome(rule: RuleDefinition, outcome: RuleOutcome) -> Finding:
status = (
FindingStatus.MANUAL
if outcome.status is RuleOutcomeStatus.INDETERMINATE
else FindingStatus(outcome.status.value)
)
return Finding(
rule_id=rule.id,
rule_version=rule.version,
status=status,
title=rule.title,
message=outcome.message,
document=outcome.document,
page=outcome.page,
field=outcome.field,
evidence=outcome.evidence,
remediation=outcome.remediation,
source=rule.source,
confidence=outcome.confidence,
)
def _internal_error_finding(rule: RuleDefinition) -> Finding:
return Finding(
rule_id=rule.id,
rule_version=rule.version,
status=FindingStatus.WARNING,
title=f"{rule.title}: internal rule error",
message="This check could not complete. No package conclusion was made.",
remediation="Review this item manually and report the rule identifier if it recurs.",
source=rule.source,
confidence=Confidence.LOW,
)