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.
Files changed (98) hide show
  1. cfa/__init__.py +39 -0
  2. cfa/_lazy.py +39 -0
  3. cfa/adapters/__init__.py +104 -0
  4. cfa/adapters/autogen.py +19 -0
  5. cfa/adapters/crewai.py +19 -0
  6. cfa/adapters/dspy.py +19 -0
  7. cfa/adapters/langgraph.py +19 -0
  8. cfa/adapters/openai_agents.py +19 -0
  9. cfa/audit/__init__.py +15 -0
  10. cfa/audit/context.py +205 -0
  11. cfa/audit/hashing.py +41 -0
  12. cfa/audit/trail.py +194 -0
  13. cfa/backends/__init__.py +132 -0
  14. cfa/backends/dbt.py +338 -0
  15. cfa/backends/pyspark.py +240 -0
  16. cfa/backends/sql.py +270 -0
  17. cfa/behavior/__init__.py +49 -0
  18. cfa/behavior/llm.py +244 -0
  19. cfa/behavior/spec.py +235 -0
  20. cfa/behavior/systematizer.py +222 -0
  21. cfa/cli/__init__.py +296 -0
  22. cfa/cli/__main__.py +6 -0
  23. cfa/cli/_helpers.py +109 -0
  24. cfa/cli/core/__init__.py +0 -0
  25. cfa/cli/core/evaluate.py +72 -0
  26. cfa/cli/core/validate.py +29 -0
  27. cfa/cli/formatters.py +280 -0
  28. cfa/cli/governance/__init__.py +0 -0
  29. cfa/cli/governance/audit.py +65 -0
  30. cfa/cli/governance/catalog.py +28 -0
  31. cfa/cli/governance/policy.py +119 -0
  32. cfa/cli/governance/rules.py +42 -0
  33. cfa/cli/governance/signature.py +31 -0
  34. cfa/cli/infrastructure/__init__.py +0 -0
  35. cfa/cli/infrastructure/backend_list.py +24 -0
  36. cfa/cli/infrastructure/storage.py +87 -0
  37. cfa/cli/project/__init__.py +0 -0
  38. cfa/cli/project/init.py +73 -0
  39. cfa/cli/project/lifecycle.py +92 -0
  40. cfa/cli/project/status.py +75 -0
  41. cfa/cli/project/taxonomy.py +38 -0
  42. cfa/cli/reporting/__init__.py +0 -0
  43. cfa/cli/reporting/report.py +109 -0
  44. cfa/cli/reporting/serve.py +43 -0
  45. cfa/config.py +103 -0
  46. cfa/core/__init__.py +19 -0
  47. cfa/core/codegen.py +65 -0
  48. cfa/core/conditions.py +129 -0
  49. cfa/core/kernel.py +224 -0
  50. cfa/core/phases/__init__.py +0 -0
  51. cfa/core/phases/runner.py +477 -0
  52. cfa/core/planner.py +290 -0
  53. cfa/execution/__init__.py +12 -0
  54. cfa/execution/partial.py +339 -0
  55. cfa/execution/state_projection.py +216 -0
  56. cfa/governance/__init__.py +76 -0
  57. cfa/lifecycle/__init__.py +51 -0
  58. cfa/mcp/__init__.py +347 -0
  59. cfa/mcp/__main__.py +4 -0
  60. cfa/normalizer/__init__.py +15 -0
  61. cfa/normalizer/base.py +441 -0
  62. cfa/normalizer/llm.py +426 -0
  63. cfa/observability/__init__.py +14 -0
  64. cfa/observability/indices.py +177 -0
  65. cfa/observability/metrics.py +91 -0
  66. cfa/observability/notify.py +79 -0
  67. cfa/observability/otel.py +81 -0
  68. cfa/observability/promotion.py +367 -0
  69. cfa/policy/__init__.py +12 -0
  70. cfa/policy/bundle.py +317 -0
  71. cfa/policy/catalog.py +117 -0
  72. cfa/policy/engine.py +306 -0
  73. cfa/reporting/__init__.py +42 -0
  74. cfa/reporting/charts.py +223 -0
  75. cfa/reporting/engine.py +456 -0
  76. cfa/resolution/__init__.py +62 -0
  77. cfa/runtime/__init__.py +13 -0
  78. cfa/runtime/gate.py +287 -0
  79. cfa/sandbox/__init__.py +189 -0
  80. cfa/sandbox/executor.py +92 -0
  81. cfa/sandbox/mock.py +89 -0
  82. cfa/sandbox/panic.py +52 -0
  83. cfa/storage/__init__.py +591 -0
  84. cfa/testing/__init__.py +60 -0
  85. cfa/testing/asserts.py +77 -0
  86. cfa/testing/evaluate.py +168 -0
  87. cfa/testing/fixtures.py +89 -0
  88. cfa/testing/markers.py +36 -0
  89. cfa/types.py +489 -0
  90. cfa/validation/__init__.py +14 -0
  91. cfa/validation/runtime.py +285 -0
  92. cfa/validation/signature.py +146 -0
  93. cfa/validation/static.py +252 -0
  94. cfa_kernel-0.1.0.dist-info/METADATA +32 -0
  95. cfa_kernel-0.1.0.dist-info/RECORD +98 -0
  96. cfa_kernel-0.1.0.dist-info/WHEEL +4 -0
  97. cfa_kernel-0.1.0.dist-info/entry_points.txt +3 -0
  98. 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
+ ]
@@ -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
+ )