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/cli/formatters.py ADDED
@@ -0,0 +1,280 @@
1
+ """
2
+ CFA CLI — formatters
3
+ ====================
4
+ Output formatters for CLI results: table, JSON, summary.
5
+ Zero external dependencies — uses only stdlib.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import sys
13
+ from typing import Any
14
+
15
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
16
+
17
+ # Force UTF-8 on Windows if possible
18
+ if sys.platform == "win32":
19
+ try:
20
+ sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
21
+ except Exception:
22
+ pass
23
+
24
+ # ANSI color codes (only when stdout is a TTY)
25
+ _RESET = "\033[0m"
26
+ _BOLD = "\033[1m"
27
+ _DIM = "\033[2m"
28
+ _RED = "\033[31m"
29
+ _GREEN = "\033[32m"
30
+ _YELLOW = "\033[33m"
31
+ _BLUE = "\033[34m"
32
+ _CYAN = "\033[36m"
33
+
34
+
35
+ def _use_color() -> bool:
36
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
37
+
38
+
39
+ def _c(code: str, text: str) -> str:
40
+ return f"{code}{text}{_RESET}" if _use_color() else text
41
+
42
+
43
+ _ICONS_UTF8 = {
44
+ "approved": "✓", "approved_with_warnings": "⚠", "blocked": "✗",
45
+ "replanned": "↻", "rolled_back": "↩", "quarantined": "⊘",
46
+ "partially_committed": "◐", "promotion_candidate": "★",
47
+ }
48
+ _ICONS_ASCII = {
49
+ "approved": "[OK]", "approved_with_warnings": "[!!]", "blocked": "[XX]",
50
+ "replanned": "[>>]", "rolled_back": "[<<]", "quarantined": "[??]",
51
+ "partially_committed": "[~]", "promotion_candidate": "[**]",
52
+ }
53
+ _BOX_UTF8 = {"tl": "┌", "tr": "┐", "bl": "└", "br": "┘", "h": "─", "v": "│", "ml": "├", "mr": "┤", "x": "┼"}
54
+ _BOX_ASCII = {"tl": "+", "tr": "+", "bl": "+", "br": "+", "h": "-", "v": "|", "ml": "+", "mr": "+", "x": "+"}
55
+
56
+ _UTF8_OK = True
57
+ try:
58
+ "┌─✓".encode(sys.stdout.encoding or "ascii")
59
+ except (UnicodeEncodeError, UnicodeDecodeError):
60
+ _UTF8_OK = False
61
+
62
+ # Force ASCII on Windows unless explicitly using UTF-8 terminal
63
+ if sys.platform == "win32" and (sys.stdout.encoding or "").lower() not in ("utf-8", "utf8"):
64
+ _UTF8_OK = False
65
+
66
+
67
+ def _visible_len(text: str) -> int:
68
+ """String length excluding ANSI color codes."""
69
+ return len(_ANSI_RE.sub("", text))
70
+
71
+
72
+ def _pad_right(text: str, width: int) -> str:
73
+ """Pad text to visible width, accounting for ANSI codes."""
74
+ visible = _visible_len(text)
75
+ if visible >= width:
76
+ return text
77
+ return text + " " * (width - visible)
78
+
79
+
80
+ def _pad_center(text: str, width: int, fillchar: str = " ") -> str:
81
+ """Center text accounting for ANSI codes."""
82
+ visible = _visible_len(text)
83
+ if visible >= width:
84
+ return text
85
+ left = (width - visible) // 2
86
+ right = width - visible - left
87
+ return (fillchar * left) + text + (fillchar * right)
88
+
89
+
90
+ def _icon(state: str) -> str:
91
+ icons = _ICONS_UTF8 if _UTF8_OK else _ICONS_ASCII
92
+ return icons.get(state, "?")
93
+
94
+
95
+ def _box(key: str) -> str:
96
+ b = _BOX_UTF8 if _UTF8_OK else _BOX_ASCII
97
+ return b.get(key, key)
98
+
99
+
100
+ def _status_icon(state: str) -> str:
101
+ icon = _icon(state)
102
+ colors = {
103
+ "✓": _GREEN, "[OK]": _GREEN,
104
+ "⚠": _YELLOW, "[!!]": _YELLOW,
105
+ "✗": _RED, "[XX]": _RED,
106
+ "↻": _YELLOW, "[>>]": _YELLOW,
107
+ "↩": _RED, "[<<]": _RED,
108
+ "⊘": _YELLOW, "[??]": _YELLOW,
109
+ "◐": _YELLOW, "[~]": _YELLOW,
110
+ "★": _GREEN, "[**]": _GREEN,
111
+ }
112
+ code = colors.get(icon, "")
113
+ return _c(code, icon)
114
+
115
+
116
+ def _severity_color(severity: str) -> str:
117
+ return {
118
+ "critical": _c(_RED, severity.upper()),
119
+ "high": _c(_RED, severity.upper()),
120
+ "warning": _c(_YELLOW, severity.upper()),
121
+ "info": _c(_BLUE, severity.upper()),
122
+ }.get(severity, severity.upper())
123
+
124
+
125
+ def _table_line(widths: list[int], cells: list[str], pad: int = 1) -> str:
126
+ parts: list[str] = []
127
+ v = _box("v")
128
+ for w, cell in zip(widths, cells, strict=False):
129
+ if w > 2:
130
+ inner = " " + _pad_right(cell, w - 2) + " "
131
+ else:
132
+ inner = cell
133
+ parts.append(inner)
134
+ return v + v.join(parts) + v
135
+
136
+
137
+ def _table_sep(widths: list[int], left: str | None = None, mid: str | None = None, right: str | None = None) -> str:
138
+ left = left or _box("ml")
139
+ mid = mid or _box("x")
140
+ right = right or _box("mr")
141
+ parts = [left]
142
+ for i, w in enumerate(widths):
143
+ parts.append(_box("h") * w)
144
+ if i < len(widths) - 1:
145
+ parts.append(mid)
146
+ parts.append(right)
147
+ return "".join(parts)
148
+
149
+
150
+ def format_evaluate_table(result: dict[str, Any], faults: list[dict[str, Any]]) -> str:
151
+ """Format an evaluate result as a bordered table."""
152
+ state = result.get("state", "unknown")
153
+ lines: list[str] = []
154
+ h = _box("h")
155
+ tl, tr, bl, br = _box("tl"), _box("tr"), _box("bl"), _box("br")
156
+
157
+ width = 60
158
+ title = " CFA Evaluation Result "
159
+ lines.append(f"{tl}{_pad_center(title, width - 2, h)}{tr}")
160
+ lines.append(_table_line([width], [""]))
161
+ lines.append(_table_line([width], [f"Intent: {result.get('intent', '')[:width - 12]}"]))
162
+ icon = _status_icon(state)
163
+ lines.append(_table_line([width], [f"State: {icon} {state}"]))
164
+ hash_val = result.get("signature_hash", "")
165
+ if hash_val:
166
+ lines.append(_table_line([width], [f"Hash: {hash_val}"]))
167
+ lines.append(_table_line([width], [f"Policy: {result.get('policy_bundle', '')} | Replans: {result.get('replan_count', 0)}"]))
168
+ lines.append(_table_line([width], [""]))
169
+
170
+ if faults:
171
+ lines.append(_table_line([width], [_c(_BOLD, "Faults")]))
172
+ lines.append(_table_line([width], [""]))
173
+ for f in faults:
174
+ sev = _severity_color(f.get("severity", "high"))
175
+ code = f.get("code", "")
176
+ msg = f.get("message", "")
177
+ lines.append(_table_line([width], [f" {sev} {code}"]))
178
+ lines.append(_table_line([width], [f" {msg[:width - 10]}"]))
179
+ remediation = f.get("remediation", [])
180
+ if remediation:
181
+ for i, r in enumerate(remediation[:3]):
182
+ lines.append(_table_line([width], [f" {_c(_DIM, f'{i+1}. {r[:width - 12]}')}"]))
183
+ lines.append(_table_line([width], [""]))
184
+
185
+ lines.append(f"{bl}{h * (width - 2)}{br}")
186
+ return "\n".join(lines)
187
+
188
+
189
+ def format_json(data: Any) -> str:
190
+ return json.dumps(data, indent=2, default=str, ensure_ascii=False)
191
+
192
+
193
+ def format_summary(result: dict[str, Any], faults: list[dict[str, Any]]) -> str:
194
+ """Human-readable paragraph summary."""
195
+ state = result.get("state", "unknown")
196
+ icon = _status_icon(state)
197
+ lines = [
198
+ f"{icon} CFA: {state.upper()}",
199
+ f"Intent: {result.get('intent', '')}",
200
+ f"Hash: {result.get('signature_hash', 'n/a')}",
201
+ f"Policy: {result.get('policy_bundle', '')}",
202
+ f"Replans: {result.get('replan_count', 0)}",
203
+ ]
204
+ if faults:
205
+ lines.append(f"Faults: {len(faults)}")
206
+ for f in faults:
207
+ lines.append(f" [{_severity_color(f.get('severity', 'high'))}] {f.get('code', '')}")
208
+ return "\n".join(lines)
209
+
210
+
211
+ def format_rules_table(rules: list[dict[str, str]]) -> str:
212
+ h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
213
+ width = 80
214
+ lines: list[str] = []
215
+ title = " CFA Policy Rules "
216
+ lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
217
+
218
+ header_w = [28, 12, 20, 8, 8]
219
+ lines.append(_table_line(header_w, ["NAME", "ACTION", "FAULT CODE", "SEVERITY", "FAMILY"]))
220
+ lines.append(_table_sep(header_w))
221
+ for r in rules:
222
+ lines.append(_table_line(header_w, [
223
+ r.get("name", "")[:26],
224
+ r.get("action", "").upper()[:10],
225
+ r.get("fault_code", "")[:18],
226
+ r.get("severity", "")[:6],
227
+ r.get("family", "")[:6],
228
+ ]))
229
+ lines.append(f"{bl}{h * (width - 2)}{br}")
230
+ return "\n".join(lines)
231
+
232
+
233
+ def format_backends_list(backends: list[dict[str, Any]]) -> str:
234
+ h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
235
+ width = 60
236
+ lines: list[str] = []
237
+ title = " CFA Registered Backends "
238
+ lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
239
+ header_w = [20, 18, 18]
240
+ lines.append(_table_line(header_w, ["NAME", "MERGE", "ANONYMIZE"]))
241
+ lines.append(_table_sep(header_w))
242
+ ok, fail = _c(_GREEN, _icon("approved")), _c(_RED, _icon("blocked"))
243
+ for b in backends:
244
+ lines.append(_table_line(header_w, [
245
+ b.get("name", "")[:18],
246
+ ok if b.get("supports_merge") else fail,
247
+ ok if b.get("supports_anonymization") else fail,
248
+ ]))
249
+ lines.append(f"{bl}{h * (width - 2)}{br}")
250
+ return "\n".join(lines)
251
+
252
+
253
+ def format_audit_table(events: list[dict[str, Any]], chain_intact: bool = True) -> str:
254
+ h, tl, tr, bl, br = _box("h"), _box("tl"), _box("tr"), _box("bl"), _box("br")
255
+ width = 80
256
+ lines: list[str] = []
257
+ title = " CFA Audit Trail "
258
+ lines.append(f"{tl}{title.center(width - 2, h)}{tr}")
259
+
260
+ chain_status = _c(_GREEN, f"{_icon('approved')} INTACT") if chain_intact else _c(_RED, f"{_icon('blocked')} BROKEN")
261
+ lines.append(_table_line([width], [f"Chain: {chain_status} | Events: {len(events)}"]))
262
+ lines.append(_table_line([width], [""]))
263
+
264
+ if events:
265
+ header_w = [4, 21, 16, 14, 19]
266
+ lines.append(_table_line(header_w, ["#", "TIMESTAMP", "PHASE", "EVENT", "OUTCOME"]))
267
+ lines.append(_table_sep(header_w))
268
+ for i, e in enumerate(events[:50]):
269
+ ts = e.get("timestamp", "")[:19]
270
+ lines.append(_table_line(header_w, [
271
+ str(i + 1),
272
+ ts,
273
+ e.get("phase", e.get("stage", ""))[:14],
274
+ e.get("event_type", "")[:12],
275
+ e.get("outcome", "")[:17],
276
+ ]))
277
+ if len(events) > 50:
278
+ lines.append(_table_line([width], [f" ... and {len(events) - 50} more events"]))
279
+ lines.append(f"{bl}{h * (width - 2)}{br}")
280
+ return "\n".join(lines)
File without changes
@@ -0,0 +1,65 @@
1
+ """cfa audit — audit trail operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def cmd_audit_show(args) -> int:
10
+ from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
11
+ from cfa.cli.formatters import format_audit_table, format_json
12
+
13
+ audit_path = args.file or args.data_dir
14
+ storage = JsonLinesAuditStorage(audit_path) if audit_path else None
15
+ trail = AuditTrail(storage=storage)
16
+ events = trail.get_events_for_intent(args.id)
17
+ event_dicts = [
18
+ {
19
+ "timestamp": e.timestamp.isoformat() if hasattr(e.timestamp, "isoformat") else str(e.timestamp),
20
+ "phase": e.stage,
21
+ "event_type": e.event_type,
22
+ "outcome": e.outcome,
23
+ "intent_id": e.intent_id,
24
+ }
25
+ for e in events
26
+ ]
27
+ chain_ok = trail.verify_chain()
28
+
29
+ fmt = args.format or "table"
30
+ if fmt == "json":
31
+ print(format_json({"intent_id": args.id, "chain_intact": chain_ok, "events": event_dicts}))
32
+ else:
33
+ print(format_audit_table(event_dicts, chain_ok))
34
+
35
+ if args.output:
36
+ out = Path(args.output)
37
+ out.write_text(format_json({"intent_id": args.id, "chain_intact": chain_ok, "events": event_dicts}), encoding="utf-8")
38
+ print(f"Saved to {out}")
39
+ return 0
40
+
41
+
42
+ def cmd_audit_verify(args) -> int:
43
+ from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
44
+
45
+ audit_path = args.file or args.data_dir
46
+ storage = JsonLinesAuditStorage(audit_path) if audit_path else None
47
+ trail = AuditTrail(storage=storage)
48
+
49
+ if args.id:
50
+ events = trail.get_events_for_intent(args.id)
51
+ chain_ok = trail.verify_chain()
52
+ if chain_ok:
53
+ print(f"\u2713 Chain INTACT for intent {args.id} ({len(events)} events)")
54
+ return 0
55
+ else:
56
+ print(f"\u2717 Chain BROKEN for intent {args.id}", file=sys.stderr)
57
+ return 1
58
+ else:
59
+ all_ok = trail.verify_chain()
60
+ if all_ok:
61
+ print(f"\u2713 Chain INTACT ({trail.event_count} events total)")
62
+ return 0
63
+ else:
64
+ print("\u2717 Chain BROKEN", file=sys.stderr)
65
+ return 1
@@ -0,0 +1,28 @@
1
+ """cfa catalog — validate catalog files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .._helpers import load_catalog
6
+
7
+
8
+ def cmd_catalog_validate(args) -> int:
9
+ from cfa.cli.formatters import format_json
10
+ from cfa.policy.catalog import validate_catalog
11
+
12
+ catalog = load_catalog(args.path)
13
+ result = validate_catalog(catalog, require_datasets=args.require_datasets)
14
+ output = {
15
+ "path": args.path,
16
+ "valid": result.valid,
17
+ "issue_count": len(result.issues),
18
+ "issues": [{"path": i.path, "message": i.message} for i in result.issues],
19
+ }
20
+
21
+ if args.format == "json":
22
+ print(format_json(output))
23
+ else:
24
+ status = "VALID" if result.valid else "INVALID"
25
+ print(f"Catalog {status}: {args.path}")
26
+ for msg in result.messages:
27
+ print(f" - {msg}")
28
+ return 0 if result.valid else 1
@@ -0,0 +1,119 @@
1
+ """cfa policy — evaluate and validate policy bundles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from pathlib import Path
7
+
8
+ from .._helpers import load_catalog, load_policy, load_structured_file
9
+
10
+
11
+ def _cross_validate_signature_against_catalog(signature, catalog: dict) -> list[dict[str, str]]:
12
+ errors: list[dict[str, str]] = []
13
+ catalog_datasets = catalog.get("datasets", {})
14
+ if not isinstance(catalog_datasets, dict):
15
+ errors.append({"path": "catalog.datasets", "message": "must be an object keyed by dataset name"})
16
+ return errors
17
+ for idx, ds in enumerate(signature.datasets):
18
+ base = f"signature.datasets[{idx}]"
19
+ cat_entry = catalog_datasets.get(ds.name)
20
+ if cat_entry is None:
21
+ errors.append({"path": f"{base}.name", "message": f"dataset '{ds.name}' not found in catalog"})
22
+ continue
23
+ if not isinstance(cat_entry, dict):
24
+ errors.append({"path": f"{base}.name", "message": f"catalog entry for '{ds.name}' is not an object"})
25
+ continue
26
+ cat_classification = cat_entry.get("classification", "internal")
27
+ if ds.classification.value != cat_classification:
28
+ errors.append({"path": f"{base}.classification", "message": f"signature says '{ds.classification.value}' but catalog says '{cat_classification}'"})
29
+ cat_pii = set(cat_entry.get("pii_columns", []))
30
+ sig_pii = set(ds.pii_columns)
31
+ if cat_pii - sig_pii:
32
+ errors.append({"path": f"{base}.pii_columns", "message": f"signature missing PII columns from catalog: {sorted(cat_pii - sig_pii)}"})
33
+ if sig_pii - cat_pii:
34
+ errors.append({"path": f"{base}.pii_columns", "message": f"signature declares PII columns not in catalog: {sorted(sig_pii - cat_pii)}"})
35
+ cat_partition = cat_entry.get("partition_column")
36
+ if cat_partition and ds.partition_column and cat_partition != ds.partition_column:
37
+ errors.append({"path": f"{base}.partition_column", "message": f"signature says '{ds.partition_column}' but catalog says '{cat_partition}'"})
38
+ return errors
39
+
40
+
41
+ def cmd_policy_check(args) -> int:
42
+ from cfa.audit.hashing import hash_file_content, hash_governance_artifact
43
+ from cfa.audit.trail import AuditTrail, JsonLinesAuditStorage
44
+ from cfa.cli.formatters import format_json
45
+ from cfa.policy.engine import PolicyEngine
46
+ from cfa.types import PolicyAction, StateSignature
47
+ from cfa.validation.signature import unwrap_signature_data, validate_signature_data
48
+
49
+ data = load_structured_file(args.signature, "Error: PyYAML required for YAML signatures.")
50
+ validation = validate_signature_data(data, require_datasets=args.require_datasets)
51
+ if not validation.valid:
52
+ output = {"signature": args.signature, "valid": False, "issue_count": len(validation.issues), "issues": [{"path": i.path, "message": i.message} for i in validation.issues]}
53
+ if args.format == "json": print(format_json(output))
54
+ else:
55
+ print(f"StateSignature INVALID: {args.signature}")
56
+ for msg in validation.messages: print(f" - {msg}")
57
+ return 1
58
+
59
+ signature_data = unwrap_signature_data(data)
60
+ signature = StateSignature.from_dict(signature_data)
61
+ catalog = load_catalog(args.catalog)
62
+
63
+ if args.strict and catalog:
64
+ strict_errors = _cross_validate_signature_against_catalog(signature, catalog)
65
+ if strict_errors:
66
+ output = {"signature": args.signature, "valid": False, "issue_count": len(strict_errors), "issues": strict_errors}
67
+ if args.format == "json": print(format_json(output))
68
+ else:
69
+ print(f"StateSignature/catalog mismatch: {args.signature}")
70
+ for issue in strict_errors: print(f" - {issue['path']}: {issue['message']}")
71
+ return 1
72
+
73
+ policy_rules, bundle_version = load_policy(args.policy_bundle)
74
+ engine = PolicyEngine(rules=policy_rules, policy_bundle_version=bundle_version)
75
+ result = engine.evaluate(signature)
76
+ decision_id = str(uuid.uuid4())
77
+
78
+ catalog_hash = hash_governance_artifact(catalog) if catalog else ""
79
+ policy_bundle_hash = ""
80
+ if Path(args.policy_bundle).suffix in (".yaml", ".yml", ".json"):
81
+ policy_bundle_hash = hash_file_content(args.policy_bundle)
82
+
83
+ faults = [{"code": f.code, "severity": f.severity.value, "family": f.family.value, "message": f.message, "remediation": list(f.remediation)} for f in result.faults]
84
+ audit_event_hash = ""
85
+ if args.audit_log:
86
+ audit = AuditTrail(storage=JsonLinesAuditStorage(args.audit_log))
87
+ event = audit.record(intent_id=signature.intent_id, stage="policy_check", event_type="policy_evaluation", outcome=result.action.value, policy_bundle_version=engine.policy_bundle_version, decision_id=decision_id, signature_hash=signature.signature_hash, catalog_hash=catalog_hash, policy_bundle_hash=policy_bundle_hash, faults=[f["code"] for f in faults], interventions=result.interventions, reasoning=result.reasoning)
88
+ audit_event_hash = event.event_hash
89
+
90
+ output = {"schema_version": "cfa.policy_check.v1", "decision_id": decision_id, "signature_hash": signature.signature_hash, "policy_bundle": engine.policy_bundle_version, "catalog_hash": catalog_hash, "policy_bundle_hash": policy_bundle_hash, "action": result.action.value, "passed": result.action == PolicyAction.APPROVE, "faults": faults, "interventions": result.interventions, "reasoning": result.reasoning, "audit_event_hash": audit_event_hash}
91
+
92
+ if args.format == "json":
93
+ print(format_json(output))
94
+ else:
95
+ print(f"Policy check {result.action.value.upper()}: {args.signature}")
96
+ print(f" decision_id: {decision_id}")
97
+ print(f" signature_hash: {signature.signature_hash}")
98
+ print(f" policy_bundle: {engine.policy_bundle_version}")
99
+ if catalog_hash: print(f" catalog_hash: {catalog_hash}")
100
+ if policy_bundle_hash: print(f" policy_bundle_hash: {policy_bundle_hash}")
101
+ if audit_event_hash: print(f" audit_hash: {audit_event_hash}")
102
+ if result.reasoning: print(f" reasoning: {result.reasoning}")
103
+ for fault in faults: print(f" - [{fault['severity']}] {fault['code']}: {fault['message']}")
104
+ return 1 if (args.exit_code and result.action != PolicyAction.APPROVE) else 0
105
+
106
+
107
+ def cmd_policy_validate(args) -> int:
108
+ from cfa.cli.formatters import format_json
109
+ from cfa.policy.bundle import validate_policy_bundle_data
110
+
111
+ data = load_structured_file(args.path, "Error: PyYAML required for YAML policy bundles.")
112
+ result = validate_policy_bundle_data(data)
113
+ output = {"path": args.path, "valid": result.valid, "issue_count": len(result.issues), "issues": [{"path": i.path, "message": i.message} for i in result.issues]}
114
+ if args.format == "json": print(format_json(output))
115
+ else:
116
+ status = "VALID" if result.valid else "INVALID"
117
+ print(f"Policy bundle {status}: {args.path}")
118
+ for msg in result.messages: print(f" - {msg}")
119
+ return 0 if result.valid else 1
@@ -0,0 +1,42 @@
1
+ """cfa rules — policy rule operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+
8
+ def cmd_rules_list(args) -> int:
9
+ from cfa.cli.formatters import format_json, format_rules_table
10
+ from cfa.policy.engine import PolicyEngine
11
+
12
+ engine = PolicyEngine(policy_bundle_version=args.policy_bundle)
13
+ rule_dicts = engine.describe_rules()
14
+ fmt = args.format or "table"
15
+
16
+ if fmt == "json":
17
+ print(format_json(rule_dicts))
18
+ else:
19
+ print(format_rules_table(rule_dicts))
20
+ print(f"\nPolicy bundle: {args.policy_bundle} | Rules: {len(rule_dicts)}")
21
+ return 0
22
+
23
+
24
+ def cmd_rules_explain(args) -> int:
25
+ from cfa.policy.engine import PolicyEngine
26
+
27
+ engine = PolicyEngine(policy_bundle_version=args.policy_bundle)
28
+ for r in engine.rules:
29
+ if r.fault_code == args.code:
30
+ print(f"Fault Code: {r.fault_code}")
31
+ print(f"Rule: {r.name}")
32
+ print(f"Action: {r.action.value.upper()}")
33
+ print(f"Severity: {r.severity.value.upper()}")
34
+ print(f"Family: {r.fault_family.value}")
35
+ print(f"Message: {r.message}")
36
+ if r.remediation:
37
+ print("Remediation:")
38
+ for i, rem in enumerate(r.remediation):
39
+ print(f" {i+1}. {rem}")
40
+ return 0
41
+ print(f"Unknown fault code: {args.code}", file=sys.stderr)
42
+ return 1
@@ -0,0 +1,31 @@
1
+ """cfa signature — validate StateSignature files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .._helpers import load_structured_file
6
+
7
+
8
+ def cmd_signature_validate(args) -> int:
9
+ from cfa.cli.formatters import format_json
10
+ from cfa.validation.signature import validate_signature_data
11
+
12
+ data = load_structured_file(
13
+ args.path,
14
+ "Error: PyYAML required for YAML signatures. Install: pip install pyyaml",
15
+ )
16
+ result = validate_signature_data(data, require_datasets=args.require_datasets)
17
+ output = {
18
+ "path": args.path,
19
+ "valid": result.valid,
20
+ "issue_count": len(result.issues),
21
+ "issues": [{"path": i.path, "message": i.message} for i in result.issues],
22
+ }
23
+
24
+ if args.format == "json":
25
+ print(format_json(output))
26
+ else:
27
+ status = "VALID" if result.valid else "INVALID"
28
+ print(f"StateSignature {status}: {args.path}")
29
+ for msg in result.messages:
30
+ print(f" - {msg}")
31
+ return 0 if result.valid else 1
File without changes
@@ -0,0 +1,24 @@
1
+ """cfa backend list — list registered backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def cmd_backend_list(args) -> int:
9
+ from cfa.backends import BackendRegistry
10
+ from cfa.cli.formatters import format_backends_list, format_json
11
+
12
+ registry = BackendRegistry.singleton()
13
+ names = registry.list()
14
+ backends: list[dict[str, Any]] = []
15
+ for name in names:
16
+ factory = registry.get(name)
17
+ backend = factory()
18
+ caps = backend.get_capabilities() if hasattr(backend, "get_capabilities") else None
19
+ backends.append({"name": name, "supports_merge": caps.supports_merge if caps else False, "supports_anonymization": caps.supports_anonymization if caps else False, "supports_partition_overwrite": caps.supports_partition_overwrite if caps else False, "cost_model_available": caps.cost_model_available if caps else False})
20
+
21
+ fmt = args.format or "table"
22
+ if fmt == "json": print(format_json(backends))
23
+ else: print(format_backends_list(backends))
24
+ return 0