cfa-kernel 0.1.0__py3-none-any.whl
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.
- cfa/__init__.py +39 -0
- cfa/_lazy.py +39 -0
- cfa/adapters/__init__.py +104 -0
- cfa/adapters/autogen.py +19 -0
- cfa/adapters/crewai.py +19 -0
- cfa/adapters/dspy.py +19 -0
- cfa/adapters/langgraph.py +19 -0
- cfa/adapters/openai_agents.py +19 -0
- cfa/audit/__init__.py +15 -0
- cfa/audit/context.py +205 -0
- cfa/audit/hashing.py +41 -0
- cfa/audit/trail.py +194 -0
- cfa/backends/__init__.py +132 -0
- cfa/backends/dbt.py +338 -0
- cfa/backends/pyspark.py +240 -0
- cfa/backends/sql.py +270 -0
- cfa/behavior/__init__.py +49 -0
- cfa/behavior/llm.py +244 -0
- cfa/behavior/spec.py +235 -0
- cfa/behavior/systematizer.py +222 -0
- cfa/cli/__init__.py +296 -0
- cfa/cli/__main__.py +6 -0
- cfa/cli/_helpers.py +109 -0
- cfa/cli/core/__init__.py +0 -0
- cfa/cli/core/evaluate.py +72 -0
- cfa/cli/core/validate.py +29 -0
- cfa/cli/formatters.py +280 -0
- cfa/cli/governance/__init__.py +0 -0
- cfa/cli/governance/audit.py +65 -0
- cfa/cli/governance/catalog.py +28 -0
- cfa/cli/governance/policy.py +119 -0
- cfa/cli/governance/rules.py +42 -0
- cfa/cli/governance/signature.py +31 -0
- cfa/cli/infrastructure/__init__.py +0 -0
- cfa/cli/infrastructure/backend_list.py +24 -0
- cfa/cli/infrastructure/storage.py +87 -0
- cfa/cli/project/__init__.py +0 -0
- cfa/cli/project/init.py +73 -0
- cfa/cli/project/lifecycle.py +92 -0
- cfa/cli/project/status.py +75 -0
- cfa/cli/project/taxonomy.py +38 -0
- cfa/cli/reporting/__init__.py +0 -0
- cfa/cli/reporting/report.py +109 -0
- cfa/cli/reporting/serve.py +43 -0
- cfa/config.py +103 -0
- cfa/core/__init__.py +19 -0
- cfa/core/codegen.py +65 -0
- cfa/core/conditions.py +129 -0
- cfa/core/kernel.py +224 -0
- cfa/core/phases/__init__.py +0 -0
- cfa/core/phases/runner.py +477 -0
- cfa/core/planner.py +290 -0
- cfa/execution/__init__.py +12 -0
- cfa/execution/partial.py +339 -0
- cfa/execution/state_projection.py +216 -0
- cfa/governance/__init__.py +76 -0
- cfa/lifecycle/__init__.py +51 -0
- cfa/mcp/__init__.py +347 -0
- cfa/mcp/__main__.py +4 -0
- cfa/normalizer/__init__.py +15 -0
- cfa/normalizer/base.py +441 -0
- cfa/normalizer/llm.py +426 -0
- cfa/observability/__init__.py +14 -0
- cfa/observability/indices.py +177 -0
- cfa/observability/metrics.py +91 -0
- cfa/observability/notify.py +79 -0
- cfa/observability/otel.py +81 -0
- cfa/observability/promotion.py +367 -0
- cfa/policy/__init__.py +12 -0
- cfa/policy/bundle.py +317 -0
- cfa/policy/catalog.py +117 -0
- cfa/policy/engine.py +306 -0
- cfa/reporting/__init__.py +42 -0
- cfa/reporting/charts.py +223 -0
- cfa/reporting/engine.py +456 -0
- cfa/resolution/__init__.py +62 -0
- cfa/runtime/__init__.py +13 -0
- cfa/runtime/gate.py +287 -0
- cfa/sandbox/__init__.py +189 -0
- cfa/sandbox/executor.py +92 -0
- cfa/sandbox/mock.py +89 -0
- cfa/sandbox/panic.py +52 -0
- cfa/storage/__init__.py +591 -0
- cfa/testing/__init__.py +60 -0
- cfa/testing/asserts.py +77 -0
- cfa/testing/evaluate.py +168 -0
- cfa/testing/fixtures.py +89 -0
- cfa/testing/markers.py +36 -0
- cfa/types.py +489 -0
- cfa/validation/__init__.py +14 -0
- cfa/validation/runtime.py +285 -0
- cfa/validation/signature.py +146 -0
- cfa/validation/static.py +252 -0
- cfa_kernel-0.1.0.dist-info/METADATA +32 -0
- cfa_kernel-0.1.0.dist-info/RECORD +98 -0
- cfa_kernel-0.1.0.dist-info/WHEEL +4 -0
- cfa_kernel-0.1.0.dist-info/entry_points.txt +3 -0
- cfa_kernel-0.1.0.dist-info/licenses/LICENSE +21 -0
cfa/policy/engine.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFA Policy Engine
|
|
3
|
+
=================
|
|
4
|
+
Applies governance, FinOps and contract rules BEFORE execution.
|
|
5
|
+
No plan is executed without passing through here.
|
|
6
|
+
|
|
7
|
+
Principles:
|
|
8
|
+
- Rules are declarative: condition + action + fault_code
|
|
9
|
+
- Versioned via policy_bundle_version
|
|
10
|
+
- Result: approve / replan / block
|
|
11
|
+
- Max 3 replans before terminal block
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
from cfa.types import (
|
|
20
|
+
DatasetClassification,
|
|
21
|
+
Fault,
|
|
22
|
+
FaultFamily,
|
|
23
|
+
FaultSeverity,
|
|
24
|
+
PolicyAction,
|
|
25
|
+
PolicyResult,
|
|
26
|
+
StateSignature,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
MAX_REPLAN_ATTEMPTS = 3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── Rule contract ────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class PolicyRule:
|
|
37
|
+
"""
|
|
38
|
+
Minimal unit of a governance rule.
|
|
39
|
+
condition: returns True if the rule fires.
|
|
40
|
+
action: what to do when it fires.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
condition: Callable[[StateSignature], bool]
|
|
45
|
+
action: PolicyAction
|
|
46
|
+
fault_code: str
|
|
47
|
+
fault_family: FaultFamily
|
|
48
|
+
severity: FaultSeverity
|
|
49
|
+
message: str
|
|
50
|
+
remediation: tuple[str, ...] = ()
|
|
51
|
+
|
|
52
|
+
def evaluate(self, signature: StateSignature) -> Fault | None:
|
|
53
|
+
if self.condition(signature):
|
|
54
|
+
return Fault(
|
|
55
|
+
code=self.fault_code,
|
|
56
|
+
family=self.fault_family,
|
|
57
|
+
severity=self.severity,
|
|
58
|
+
stage="policy_engine",
|
|
59
|
+
message=self.message,
|
|
60
|
+
mandatory_action=self.action,
|
|
61
|
+
remediation=self.remediation,
|
|
62
|
+
)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── Default ruleset ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_default_ruleset() -> list[PolicyRule]:
|
|
70
|
+
"""
|
|
71
|
+
Default CFA rule set.
|
|
72
|
+
In production, loaded from a versioned policy_bundle.
|
|
73
|
+
"""
|
|
74
|
+
return [
|
|
75
|
+
# ── Governance / PII ────────────────────────────────────────────
|
|
76
|
+
PolicyRule(
|
|
77
|
+
name="forbid_raw_pii_in_silver_or_gold",
|
|
78
|
+
condition=lambda sig: (
|
|
79
|
+
sig.writes_to_protected_layer
|
|
80
|
+
and sig.contains_pii
|
|
81
|
+
and not sig.constraints.no_pii_raw
|
|
82
|
+
),
|
|
83
|
+
action=PolicyAction.REPLAN,
|
|
84
|
+
fault_code="GOVERNANCE_RAW_PII_IN_PROTECTED_LAYER",
|
|
85
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
86
|
+
severity=FaultSeverity.CRITICAL,
|
|
87
|
+
message="PII detected without treatment in write to protected layer.",
|
|
88
|
+
remediation=(
|
|
89
|
+
"Apply sha256() on PII columns before join",
|
|
90
|
+
"Or use drop() to remove sensitive columns",
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
PolicyRule(
|
|
94
|
+
name="require_pii_anonymization_declaration",
|
|
95
|
+
condition=lambda sig: (
|
|
96
|
+
sig.contains_pii and not sig.constraints.no_pii_raw
|
|
97
|
+
),
|
|
98
|
+
action=PolicyAction.BLOCK,
|
|
99
|
+
fault_code="GOVERNANCE_PII_WITHOUT_POLICY",
|
|
100
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
101
|
+
severity=FaultSeverity.CRITICAL,
|
|
102
|
+
message="Datasets with PII present but no_pii_raw=False without justification.",
|
|
103
|
+
remediation=(
|
|
104
|
+
"Set constraints.no_pii_raw=True explicitly",
|
|
105
|
+
"Or add PII treatment justification to domain",
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
# ── FinOps ──────────────────────────────────────────────────────
|
|
109
|
+
PolicyRule(
|
|
110
|
+
name="require_partition_filter_for_high_volume",
|
|
111
|
+
condition=lambda sig: (
|
|
112
|
+
any(d.classification == DatasetClassification.HIGH_VOLUME for d in sig.datasets)
|
|
113
|
+
and len(sig.constraints.partition_by) == 0
|
|
114
|
+
),
|
|
115
|
+
action=PolicyAction.REPLAN,
|
|
116
|
+
fault_code="FINOPS_MISSING_TEMPORAL_PREDICATE",
|
|
117
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
118
|
+
severity=FaultSeverity.HIGH,
|
|
119
|
+
message="High volume dataset without partition filter — full scan risk.",
|
|
120
|
+
remediation=(
|
|
121
|
+
"Add constraints.partition_by with temporal column",
|
|
122
|
+
"Example: partition_by: [processing_date]",
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
PolicyRule(
|
|
126
|
+
name="warn_on_sensitive_without_partition",
|
|
127
|
+
condition=lambda sig: (
|
|
128
|
+
any(d.classification == DatasetClassification.SENSITIVE for d in sig.datasets)
|
|
129
|
+
and len(sig.constraints.partition_by) == 0
|
|
130
|
+
),
|
|
131
|
+
action=PolicyAction.REPLAN,
|
|
132
|
+
fault_code="FINOPS_SENSITIVE_WITHOUT_PARTITION",
|
|
133
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
134
|
+
severity=FaultSeverity.WARNING,
|
|
135
|
+
message="Sensitive dataset without declared partitioning.",
|
|
136
|
+
remediation=("Add partition_by to limit processing scope.",),
|
|
137
|
+
),
|
|
138
|
+
# ── Data Contract ───────────────────────────────────────────────
|
|
139
|
+
PolicyRule(
|
|
140
|
+
name="require_merge_key_for_silver_gold",
|
|
141
|
+
condition=lambda sig: (
|
|
142
|
+
sig.writes_to_protected_layer and not sig.constraints.merge_key_required
|
|
143
|
+
),
|
|
144
|
+
action=PolicyAction.BLOCK,
|
|
145
|
+
fault_code="CONTRACT_MISSING_MERGE_KEY",
|
|
146
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
147
|
+
severity=FaultSeverity.CRITICAL,
|
|
148
|
+
message="Write to Silver/Gold without merge_key — direct append forbidden.",
|
|
149
|
+
remediation=(
|
|
150
|
+
"Set constraints.merge_key_required=True",
|
|
151
|
+
"Ensure the Planner uses merge, not append",
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
PolicyRule(
|
|
155
|
+
name="enforce_type_checking",
|
|
156
|
+
condition=lambda sig: (
|
|
157
|
+
sig.writes_to_protected_layer and not sig.constraints.enforce_types
|
|
158
|
+
),
|
|
159
|
+
action=PolicyAction.REPLAN,
|
|
160
|
+
fault_code="CONTRACT_TYPE_ENFORCEMENT_DISABLED",
|
|
161
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
162
|
+
severity=FaultSeverity.HIGH,
|
|
163
|
+
message="enforce_types=False in write to protected layer.",
|
|
164
|
+
remediation=(
|
|
165
|
+
"Enable constraints.enforce_types=True",
|
|
166
|
+
"Define expected schema in catalog",
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
# ── Cost Ceiling ────────────────────────────────────────────────
|
|
170
|
+
PolicyRule(
|
|
171
|
+
name="enforce_cost_ceiling",
|
|
172
|
+
condition=lambda sig: (
|
|
173
|
+
sig.constraints.max_cost_dbu is not None
|
|
174
|
+
and sig.constraints.max_cost_dbu <= 0
|
|
175
|
+
),
|
|
176
|
+
action=PolicyAction.BLOCK,
|
|
177
|
+
fault_code="FINOPS_INVALID_COST_CEILING",
|
|
178
|
+
fault_family=FaultFamily.SEMANTIC,
|
|
179
|
+
severity=FaultSeverity.HIGH,
|
|
180
|
+
message="max_cost_dbu invalid (must be > 0).",
|
|
181
|
+
remediation=("Set constraints.max_cost_dbu with positive value.",),
|
|
182
|
+
),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ── Policy Engine ────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PolicyEngine:
|
|
190
|
+
"""
|
|
191
|
+
Evaluates all governance rules against a State Signature.
|
|
192
|
+
|
|
193
|
+
Flow:
|
|
194
|
+
1. Evaluate each rule against the Signature
|
|
195
|
+
2. Collect all Faults
|
|
196
|
+
3. Determine action: approve / replan / block
|
|
197
|
+
4. Control replan limit
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
rules: list[PolicyRule] | None = None,
|
|
203
|
+
policy_bundle_version: str = "v1.0",
|
|
204
|
+
max_replan_attempts: int = MAX_REPLAN_ATTEMPTS,
|
|
205
|
+
) -> None:
|
|
206
|
+
self.rules = rules if rules is not None else build_default_ruleset()
|
|
207
|
+
self.policy_bundle_version = policy_bundle_version
|
|
208
|
+
self.max_replan_attempts = max_replan_attempts
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_bundle(cls, path: str, max_replan_attempts: int = MAX_REPLAN_ATTEMPTS) -> PolicyEngine:
|
|
212
|
+
"""Create a PolicyEngine from a YAML/JSON policy bundle file.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
path: Path to .yaml or .json policy bundle file.
|
|
216
|
+
max_replan_attempts: Maximum replan cycles before terminal block.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
PolicyEngine configured with the bundle's rules.
|
|
220
|
+
"""
|
|
221
|
+
from cfa.policy.bundle import PolicyBundle
|
|
222
|
+
bundle = PolicyBundle.from_yaml(path) if path.endswith((".yaml", ".yml")) else PolicyBundle.from_json(path)
|
|
223
|
+
return cls(
|
|
224
|
+
rules=bundle.rules,
|
|
225
|
+
policy_bundle_version=bundle.version,
|
|
226
|
+
max_replan_attempts=max_replan_attempts,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def evaluate(
|
|
230
|
+
self, signature: StateSignature, replan_count: int = 0
|
|
231
|
+
) -> PolicyResult:
|
|
232
|
+
if replan_count >= self.max_replan_attempts:
|
|
233
|
+
return PolicyResult(
|
|
234
|
+
action=PolicyAction.BLOCK,
|
|
235
|
+
replan_count=replan_count,
|
|
236
|
+
reasoning=f"Replan limit ({self.max_replan_attempts}) reached. Manual intervention required.",
|
|
237
|
+
faults=[
|
|
238
|
+
Fault(
|
|
239
|
+
code="POLICY_MAX_REPLAN_EXCEEDED",
|
|
240
|
+
family=FaultFamily.SEMANTIC,
|
|
241
|
+
severity=FaultSeverity.CRITICAL,
|
|
242
|
+
stage="policy_engine",
|
|
243
|
+
message=f"Max {self.max_replan_attempts} replans exceeded.",
|
|
244
|
+
mandatory_action=PolicyAction.BLOCK,
|
|
245
|
+
remediation=("Review the intent manually and fix previous faults.",),
|
|
246
|
+
)
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
faults: list[Fault] = []
|
|
251
|
+
for rule in self.rules:
|
|
252
|
+
fault = rule.evaluate(signature)
|
|
253
|
+
if fault:
|
|
254
|
+
faults.append(fault)
|
|
255
|
+
|
|
256
|
+
action = self._determine_action(faults)
|
|
257
|
+
|
|
258
|
+
interventions: list[str] = []
|
|
259
|
+
if action == PolicyAction.REPLAN:
|
|
260
|
+
for fault in faults:
|
|
261
|
+
interventions.extend(fault.remediation)
|
|
262
|
+
|
|
263
|
+
return PolicyResult(
|
|
264
|
+
action=action,
|
|
265
|
+
faults=faults,
|
|
266
|
+
interventions=list(dict.fromkeys(interventions)),
|
|
267
|
+
replan_count=replan_count,
|
|
268
|
+
reasoning=self._build_reasoning(action, faults, replan_count),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _determine_action(self, faults: list[Fault]) -> PolicyAction:
|
|
272
|
+
"""BLOCK > REPLAN > APPROVE."""
|
|
273
|
+
if not faults:
|
|
274
|
+
return PolicyAction.APPROVE
|
|
275
|
+
if any(f.mandatory_action == PolicyAction.BLOCK for f in faults):
|
|
276
|
+
return PolicyAction.BLOCK
|
|
277
|
+
if any(f.mandatory_action == PolicyAction.REPLAN for f in faults):
|
|
278
|
+
return PolicyAction.REPLAN
|
|
279
|
+
return PolicyAction.APPROVE
|
|
280
|
+
|
|
281
|
+
def _build_reasoning(
|
|
282
|
+
self, action: PolicyAction, faults: list[Fault], replan_count: int
|
|
283
|
+
) -> str:
|
|
284
|
+
if not faults:
|
|
285
|
+
return "All rules passed. Execution approved."
|
|
286
|
+
fault_summary = "; ".join(f.code for f in faults)
|
|
287
|
+
base = f"action={action.value} | faults=[{fault_summary}]"
|
|
288
|
+
if action == PolicyAction.REPLAN:
|
|
289
|
+
base += f" | replan {replan_count + 1}/{self.max_replan_attempts}"
|
|
290
|
+
elif action == PolicyAction.BLOCK:
|
|
291
|
+
base += " | TERMINAL"
|
|
292
|
+
return base
|
|
293
|
+
|
|
294
|
+
def add_rule(self, rule: PolicyRule) -> None:
|
|
295
|
+
self.rules.append(rule)
|
|
296
|
+
|
|
297
|
+
def describe_rules(self) -> list[dict[str, str]]:
|
|
298
|
+
return [
|
|
299
|
+
{
|
|
300
|
+
"name": r.name,
|
|
301
|
+
"action": r.action.value,
|
|
302
|
+
"fault_code": r.fault_code,
|
|
303
|
+
"severity": r.severity.value,
|
|
304
|
+
}
|
|
305
|
+
for r in self.rules
|
|
306
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFA Reporting — Rich HTML reports
|
|
3
|
+
==================================
|
|
4
|
+
Self-contained HTML reports with Chart.js charts.
|
|
5
|
+
|
|
6
|
+
Report types:
|
|
7
|
+
- execution: pipeline execution with timeline, metrics, faults
|
|
8
|
+
- audit: hash chain visualization with event timeline
|
|
9
|
+
- lifecycle: IFo/IFs/IFg/IDI dashboard with trend charts
|
|
10
|
+
- compliance: governance health summary for auditors
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from cfa.reporting import generate_report
|
|
14
|
+
|
|
15
|
+
generate_report("execution", "report.html", intent="...", state="approved", ...)
|
|
16
|
+
generate_report("audit", "audit.html", intent_id="...", events=[...], chain_intact=True)
|
|
17
|
+
generate_report("lifecycle", "dashboard.html", period_days=90, skills=[...], ...)
|
|
18
|
+
generate_report("compliance", "compliance.html", policy_bundle="prod-v1", ...)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .charts import (
|
|
24
|
+
ChartConfig,
|
|
25
|
+
cost_trend_chart,
|
|
26
|
+
decisions_pie_chart,
|
|
27
|
+
faults_bar_chart,
|
|
28
|
+
lifecycle_trend_chart,
|
|
29
|
+
severity_pie_chart,
|
|
30
|
+
)
|
|
31
|
+
from .engine import ReportEngine, generate_report
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"ReportEngine",
|
|
35
|
+
"generate_report",
|
|
36
|
+
"ChartConfig",
|
|
37
|
+
"lifecycle_trend_chart",
|
|
38
|
+
"decisions_pie_chart",
|
|
39
|
+
"faults_bar_chart",
|
|
40
|
+
"cost_trend_chart",
|
|
41
|
+
"severity_pie_chart",
|
|
42
|
+
]
|
cfa/reporting/charts.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CFA Reporting — charts data preparation
|
|
3
|
+
=======================================
|
|
4
|
+
Prepares data structures for Chart.js inline chart rendering.
|
|
5
|
+
Zero Python dependencies — generates JSON configs for Chart.js CDN.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ChartConfig:
|
|
16
|
+
"""JSON-serializable Chart.js configuration."""
|
|
17
|
+
chart_type: str # "line", "bar", "pie", "doughnut"
|
|
18
|
+
labels: list[str]
|
|
19
|
+
datasets: list[dict[str, Any]]
|
|
20
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def to_json(self) -> str:
|
|
23
|
+
import json
|
|
24
|
+
return json.dumps({
|
|
25
|
+
"type": self.chart_type,
|
|
26
|
+
"data": {
|
|
27
|
+
"labels": self.labels,
|
|
28
|
+
"datasets": self.datasets,
|
|
29
|
+
},
|
|
30
|
+
"options": self.options,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
COLORS = {
|
|
35
|
+
"green": "rgba(34, 197, 94, {a})",
|
|
36
|
+
"red": "rgba(239, 68, 68, {a})",
|
|
37
|
+
"yellow": "rgba(234, 179, 8, {a})",
|
|
38
|
+
"blue": "rgba(59, 130, 246, {a})",
|
|
39
|
+
"purple": "rgba(168, 85, 247, {a})",
|
|
40
|
+
"cyan": "rgba(6, 182, 212, {a})",
|
|
41
|
+
"orange": "rgba(249, 115, 22, {a})",
|
|
42
|
+
"gray": "rgba(156, 163, 175, {a})",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def lifecycle_trend_chart(
|
|
47
|
+
dates: list[str],
|
|
48
|
+
ifo_values: list[float],
|
|
49
|
+
ifs_values: list[float],
|
|
50
|
+
idi_values: list[float],
|
|
51
|
+
ifg_values: list[float],
|
|
52
|
+
) -> ChartConfig:
|
|
53
|
+
"""Multi-line chart showing IFo, IFs, IDI, IFg over time."""
|
|
54
|
+
return ChartConfig(
|
|
55
|
+
chart_type="line",
|
|
56
|
+
labels=dates,
|
|
57
|
+
datasets=[
|
|
58
|
+
{
|
|
59
|
+
"label": "IFo (Fluidez Operacional)",
|
|
60
|
+
"data": ifo_values,
|
|
61
|
+
"borderColor": COLORS["blue"].format(a="1"),
|
|
62
|
+
"backgroundColor": COLORS["blue"].format(a="0.1"),
|
|
63
|
+
"tension": 0.3,
|
|
64
|
+
"fill": False,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"label": "IFs (Fidelidade Semântica)",
|
|
68
|
+
"data": ifs_values,
|
|
69
|
+
"borderColor": COLORS["purple"].format(a="1"),
|
|
70
|
+
"backgroundColor": COLORS["purple"].format(a="0.1"),
|
|
71
|
+
"tension": 0.3,
|
|
72
|
+
"fill": False,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"label": "IDI (Intent Drift)",
|
|
76
|
+
"data": idi_values,
|
|
77
|
+
"borderColor": COLORS["cyan"].format(a="1"),
|
|
78
|
+
"backgroundColor": COLORS["cyan"].format(a="0.1"),
|
|
79
|
+
"tension": 0.3,
|
|
80
|
+
"fill": False,
|
|
81
|
+
"borderDash": [5, 5],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"label": "IFg (Governança)",
|
|
85
|
+
"data": ifg_values,
|
|
86
|
+
"borderColor": COLORS["green"].format(a="1"),
|
|
87
|
+
"backgroundColor": COLORS["green"].format(a="0.1"),
|
|
88
|
+
"tension": 0.3,
|
|
89
|
+
"fill": False,
|
|
90
|
+
"stepped": True,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
options={
|
|
94
|
+
"responsive": True,
|
|
95
|
+
"plugins": {
|
|
96
|
+
"legend": {"position": "bottom", "labels": {"color": "#d1d5db"}},
|
|
97
|
+
},
|
|
98
|
+
"scales": {
|
|
99
|
+
"x": {"ticks": {"color": "#9ca3af"}, "grid": {"color": "rgba(75,85,99,0.2)"}},
|
|
100
|
+
"y": {
|
|
101
|
+
"min": 0, "max": 1,
|
|
102
|
+
"ticks": {"color": "#9ca3af", "callback": "value => (value * 100).toFixed(0) + '%'"},
|
|
103
|
+
"grid": {"color": "rgba(75,85,99,0.2)"},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def decisions_pie_chart(approved: int, replanned: int, blocked: int) -> ChartConfig:
|
|
111
|
+
"""Doughnut chart of policy decisions distribution."""
|
|
112
|
+
return ChartConfig(
|
|
113
|
+
chart_type="doughnut",
|
|
114
|
+
labels=["Approved", "Replanned", "Blocked"],
|
|
115
|
+
datasets=[{
|
|
116
|
+
"data": [approved, replanned, blocked],
|
|
117
|
+
"backgroundColor": [
|
|
118
|
+
COLORS["green"].format(a="0.8"),
|
|
119
|
+
COLORS["yellow"].format(a="0.8"),
|
|
120
|
+
COLORS["red"].format(a="0.8"),
|
|
121
|
+
],
|
|
122
|
+
"borderColor": [
|
|
123
|
+
COLORS["green"].format(a="1"),
|
|
124
|
+
COLORS["yellow"].format(a="1"),
|
|
125
|
+
COLORS["red"].format(a="1"),
|
|
126
|
+
],
|
|
127
|
+
"borderWidth": 1,
|
|
128
|
+
}],
|
|
129
|
+
options={
|
|
130
|
+
"responsive": True,
|
|
131
|
+
"plugins": {
|
|
132
|
+
"legend": {"position": "bottom", "labels": {"color": "#d1d5db"}},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def faults_bar_chart(fault_counts: dict[str, int]) -> ChartConfig:
|
|
139
|
+
"""Horizontal bar chart of top faults by frequency."""
|
|
140
|
+
sorted_faults = sorted(fault_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
141
|
+
return ChartConfig(
|
|
142
|
+
chart_type="bar",
|
|
143
|
+
labels=[f[0] for f in sorted_faults],
|
|
144
|
+
datasets=[{
|
|
145
|
+
"label": "Occurrences",
|
|
146
|
+
"data": [f[1] for f in sorted_faults],
|
|
147
|
+
"backgroundColor": COLORS["red"].format(a="0.7"),
|
|
148
|
+
"borderColor": COLORS["red"].format(a="1"),
|
|
149
|
+
"borderWidth": 1,
|
|
150
|
+
}],
|
|
151
|
+
options={
|
|
152
|
+
"indexAxis": "y",
|
|
153
|
+
"responsive": True,
|
|
154
|
+
"plugins": {
|
|
155
|
+
"legend": {"display": False},
|
|
156
|
+
},
|
|
157
|
+
"scales": {
|
|
158
|
+
"x": {"ticks": {"color": "#9ca3af"}, "grid": {"color": "rgba(75,85,99,0.2)"}},
|
|
159
|
+
"y": {"ticks": {"color": "#9ca3af"}, "grid": {"color": "rgba(75,85,99,0.2)"}},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cost_trend_chart(dates: list[str], costs: list[float], budget_line: float = 50.0) -> ChartConfig:
|
|
166
|
+
"""Line chart of cost over time with budget threshold."""
|
|
167
|
+
return ChartConfig(
|
|
168
|
+
chart_type="line",
|
|
169
|
+
labels=dates,
|
|
170
|
+
datasets=[
|
|
171
|
+
{
|
|
172
|
+
"label": "Cost (DBU)",
|
|
173
|
+
"data": costs,
|
|
174
|
+
"borderColor": COLORS["orange"].format(a="1"),
|
|
175
|
+
"backgroundColor": COLORS["orange"].format(a="0.1"),
|
|
176
|
+
"tension": 0.3,
|
|
177
|
+
"fill": True,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"label": "Budget Limit",
|
|
181
|
+
"data": [budget_line] * len(dates),
|
|
182
|
+
"borderColor": COLORS["red"].format(a="0.5"),
|
|
183
|
+
"borderDash": [8, 4],
|
|
184
|
+
"fill": False,
|
|
185
|
+
"pointRadius": 0,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
options={
|
|
189
|
+
"responsive": True,
|
|
190
|
+
"plugins": {
|
|
191
|
+
"legend": {"position": "bottom", "labels": {"color": "#d1d5db"}},
|
|
192
|
+
},
|
|
193
|
+
"scales": {
|
|
194
|
+
"x": {"ticks": {"color": "#9ca3af"}, "grid": {"color": "rgba(75,85,99,0.2)"}},
|
|
195
|
+
"y": {"ticks": {"color": "#9ca3af"}, "grid": {"color": "rgba(75,85,99,0.2)"}},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def severity_pie_chart(critical: int, high: int, medium: int, warning: int, info: int) -> ChartConfig:
|
|
202
|
+
"""Pie chart of fault severity distribution."""
|
|
203
|
+
return ChartConfig(
|
|
204
|
+
chart_type="pie",
|
|
205
|
+
labels=["Critical", "High", "Medium", "Warning", "Info"],
|
|
206
|
+
datasets=[{
|
|
207
|
+
"data": [critical, high, medium, warning, info],
|
|
208
|
+
"backgroundColor": [
|
|
209
|
+
COLORS["red"].format(a="0.8"),
|
|
210
|
+
COLORS["orange"].format(a="0.8"),
|
|
211
|
+
COLORS["yellow"].format(a="0.8"),
|
|
212
|
+
COLORS["blue"].format(a="0.8"),
|
|
213
|
+
COLORS["gray"].format(a="0.8"),
|
|
214
|
+
],
|
|
215
|
+
"borderWidth": 1,
|
|
216
|
+
}],
|
|
217
|
+
options={
|
|
218
|
+
"responsive": True,
|
|
219
|
+
"plugins": {
|
|
220
|
+
"legend": {"position": "bottom", "labels": {"color": "#d1d5db"}},
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
)
|