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/__init__.py ADDED
@@ -0,0 +1,296 @@
1
+ """
2
+ CFA CLI — main entry point
3
+ ===========================
4
+ argparse-based CLI with zero external dependencies.
5
+
6
+ Commands are organized by family:
7
+ - core/ evaluate, validate
8
+ - governance/ rules, audit, catalog, signature, policy
9
+ - reporting/ report, serve
10
+ - project/ init, taxonomy
11
+ - infrastructure/ backend_list
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+
19
+
20
+ def build_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(
22
+ prog="cfa",
23
+ description="CFA — Contextual Flux Architecture. Governed execution for AI and data systems.",
24
+ )
25
+ sub = parser.add_subparsers(dest="command")
26
+
27
+ p_eval = sub.add_parser("evaluate", help="Evaluate an intent through the governance pipeline")
28
+ p_eval.add_argument("intent", help="Natural language intent to evaluate")
29
+ p_eval.add_argument("--config", help="Path to cfa.yaml config file")
30
+ p_eval.add_argument("--catalog", "-c", help="Path to catalog JSON/YAML")
31
+ p_eval.add_argument("--policy-bundle", "-p", default="v1.0", help="Policy bundle version")
32
+ p_eval.add_argument("--backend", "-b", default="pyspark", help="Codegen backend name")
33
+ p_eval.add_argument("--format", "-f", choices=["table", "json", "summary"], default="table")
34
+ p_eval.add_argument("--output", "-o", help="Save result to file")
35
+ p_eval.add_argument("--warnings-blocking", action="store_true", help="Treat warnings as blocking")
36
+ p_eval.add_argument("--exit-code", action="store_true", help="Exit 1 if BLOCKED")
37
+ p_eval.add_argument("--normalizer", default="auto", choices=["auto", "rule_based", "mock", "openai", "deepseek", "llm"], help="Normalizer backend")
38
+ p_eval.add_argument("--strict", action="store_true", help="Block ambiguous intents")
39
+ p_eval.add_argument("--llm-model", help="LLM model name")
40
+ p_eval.add_argument("--llm-api-key", help="LLM API key")
41
+ p_eval.add_argument("--llm-base-url", help="LLM API base URL")
42
+ p_eval.add_argument("--llm-strict", action="store_true", help="Require LLM output to match catalog exactly")
43
+
44
+ p_val = sub.add_parser("validate", help="Validate an intent against a behavior spec")
45
+ p_val.add_argument("--spec", "-s", required=True, help="Path to behavior spec YAML")
46
+ p_val.add_argument("--intent", "-i", required=True, help="Intent to validate")
47
+ p_val.add_argument("--backend", "-b", default="pyspark")
48
+ p_val.add_argument("--exit-code", action="store_true", help="Exit 1 if BLOCKED")
49
+
50
+ p_rules = sub.add_parser("rules", help="Policy rule operations")
51
+ rules_sub = p_rules.add_subparsers(dest="rules_command")
52
+ p_list = rules_sub.add_parser("list", help="List all policy rules")
53
+ p_list.add_argument("--policy-bundle", "-p", default="v1.0")
54
+ p_list.add_argument("--format", "-f", choices=["table", "json"], default="table")
55
+ p_explain = rules_sub.add_parser("explain", help="Explain a fault code")
56
+ p_explain.add_argument("code", help="Fault code to explain")
57
+ p_explain.add_argument("--policy-bundle", "-p", default="v1.0")
58
+
59
+ p_audit = sub.add_parser("audit", help="Audit trail operations")
60
+ audit_sub = p_audit.add_subparsers(dest="audit_command")
61
+ p_show = audit_sub.add_parser("show", help="Show audit trail for an intent")
62
+ p_show.add_argument("--id", "-i", required=True, help="Intent ID")
63
+ p_show.add_argument("--file", help="Path to audit JSONL file")
64
+ p_show.add_argument("--data-dir", help="Path to audit data directory")
65
+ p_show.add_argument("--format", "-f", choices=["table", "json"], default="table")
66
+ p_show.add_argument("--output", "-o", help="Save to file")
67
+ p_verify = audit_sub.add_parser("verify", help="Verify audit chain integrity")
68
+ p_verify.add_argument("--id", "-i", help="Intent ID (omit for all)")
69
+ p_verify.add_argument("--file", help="Path to audit JSONL file")
70
+ p_verify.add_argument("--data-dir", help="Path to audit data directory")
71
+
72
+ p_tax = sub.add_parser("taxonomy", help="Behavior taxonomy operations")
73
+ tax_sub = p_tax.add_subparsers(dest="taxonomy_command")
74
+ p_gen = tax_sub.add_parser("generate", help="Generate taxonomy from behavior spec")
75
+ p_gen.add_argument("--spec", "-s", required=True, help="Path to behavior spec YAML")
76
+ p_gen.add_argument("--output", "-o", help="Save to file")
77
+ p_ti = tax_sub.add_parser("test-intents", help="Generate test intents from behavior spec")
78
+ p_ti.add_argument("--spec", "-s", required=True, help="Path to behavior spec YAML")
79
+ p_ti.add_argument("--count", "-n", type=int, default=5, help="Number of intents per category")
80
+ p_ti.add_argument("--output", "-o", help="Save to file")
81
+
82
+ p_be = sub.add_parser("backend", help="Backend operations")
83
+ be_sub = p_be.add_subparsers(dest="backend_command")
84
+ p_blist = be_sub.add_parser("list", help="List registered backends")
85
+ p_blist.add_argument("--format", "-f", choices=["table", "json"], default="table")
86
+
87
+ p_cat = sub.add_parser("catalog", help="Catalog operations")
88
+ cat_sub = p_cat.add_subparsers(dest="catalog_command")
89
+ p_cat_val = cat_sub.add_parser("validate", help="Validate a catalog JSON/YAML file")
90
+ p_cat_val.add_argument("path", help="Path to catalog JSON/YAML")
91
+ p_cat_val.add_argument("--require-datasets", action="store_true", help="Require at least one dataset")
92
+ p_cat_val.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
93
+
94
+ p_sig = sub.add_parser("signature", help="StateSignature operations")
95
+ sig_sub = p_sig.add_subparsers(dest="signature_command")
96
+ p_sig_val = sig_sub.add_parser("validate", help="Validate a StateSignature JSON/YAML file")
97
+ p_sig_val.add_argument("path", help="Path to StateSignature JSON/YAML")
98
+ p_sig_val.add_argument("--require-datasets", action="store_true", help="Require at least one dataset")
99
+ p_sig_val.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
100
+
101
+ p_pol = sub.add_parser("policy", help="Policy bundle operations")
102
+ pol_sub = p_pol.add_subparsers(dest="policy_command")
103
+ p_pol_val = pol_sub.add_parser("validate", help="Validate a policy bundle JSON/YAML file")
104
+ p_pol_val.add_argument("path", help="Path to policy bundle JSON/YAML")
105
+ p_pol_val.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
106
+ p_pol_check = pol_sub.add_parser("check", help="Evaluate a StateSignature JSON/YAML against policy")
107
+ p_pol_check.add_argument("--config", help="Path to cfa.yaml config file")
108
+ p_pol_check.add_argument("--signature", "-s", required=True, help="Path to StateSignature JSON/YAML")
109
+ p_pol_check.add_argument("--catalog", help="Path to catalog JSON/YAML")
110
+ p_pol_check.add_argument("--policy-bundle", "-p", default="v1.0", help="Policy bundle version or path")
111
+ p_pol_check.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
112
+ p_pol_check.add_argument("--exit-code", action="store_true", help="Exit 1 when action is not approve")
113
+ p_pol_check.add_argument("--require-datasets", action="store_true")
114
+ p_pol_check.add_argument("--strict", action="store_true", help="Validate signature datasets against catalog")
115
+ p_pol_check.add_argument("--audit-log", help="Append decision to audit JSONL file")
116
+
117
+ p_rep = sub.add_parser("report", help="Generate rich HTML reports")
118
+ rep_sub = p_rep.add_subparsers(dest="report_command")
119
+ p_rex = rep_sub.add_parser("execution", help="Execution report")
120
+ p_rex.add_argument("--intent", required=True, help="Intent text")
121
+ p_rex.add_argument("--intent-id", default="", help="Intent ID")
122
+ p_rex.add_argument("--state", default="approved", help="Decision state")
123
+ p_rex.add_argument("--signature-hash", default="", help="Signature hash")
124
+ p_rex.add_argument("--policy-bundle", default="v1.0", help="Policy bundle")
125
+ p_rex.add_argument("--replan-count", type=int, default=0)
126
+ p_rex.add_argument("--output", "-o", default="cfa_execution_report.html")
127
+ p_raud = rep_sub.add_parser("audit", help="Audit trail report")
128
+ p_raud.add_argument("--intent-id", required=True, help="Intent ID")
129
+ p_raud.add_argument("--policy-bundle", default="v1.0")
130
+ p_raud.add_argument("--output", "-o", default="cfa_audit_report.html")
131
+ p_rlife = rep_sub.add_parser("lifecycle", help="Lifecycle dashboard")
132
+ p_rlife.add_argument("--period", type=int, default=90, help="Period in days")
133
+ p_rlife.add_argument("--audit-file", help="Path to audit JSONL file for real data")
134
+ p_rlife.add_argument("--output", "-o", default="cfa_lifecycle_dashboard.html")
135
+ p_rcomp = rep_sub.add_parser("compliance", help="Compliance summary for auditors")
136
+ p_rcomp.add_argument("--policy-bundle", default="v1.0")
137
+ p_rcomp.add_argument("--audit-file", help="Path to audit JSONL file for real data")
138
+ p_rcomp.add_argument("--output", "-o", default="cfa_compliance_report.html")
139
+ p_rdash = rep_sub.add_parser("dashboard", help="Multi-pipeline dashboard")
140
+ p_rdash.add_argument("--period", type=int, default=90, help="Period in days")
141
+ p_rdash.add_argument("--audit-file", help="Path to audit JSONL file for real data")
142
+ p_rdash.add_argument("--output", "-o", default="cfa_dashboard.html")
143
+
144
+ p_srv = sub.add_parser("serve", help="Start live metrics/health server")
145
+ p_srv.add_argument("--port", "-p", type=int, default=8765)
146
+ p_srv.add_argument("--metrics-port", type=int, default=0, help="Enable /metrics on separate port")
147
+
148
+ p_init = sub.add_parser("init", help="Initialize CFA in current directory")
149
+ p_init.add_argument("--dir", "-d", default=".cfa", help="Target directory")
150
+
151
+ p_status = sub.add_parser("status", help="Show CFA project health and storage stats")
152
+ p_status.add_argument("--config", help="Path to cfa.yaml config file")
153
+ p_status.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
154
+
155
+ # ── lifecycle ──────────────────────────────────────────────────────────
156
+ p_life = sub.add_parser("lifecycle", help="Lifecycle management operations")
157
+ life_sub = p_life.add_subparsers(dest="lifecycle_command")
158
+ p_life_eval = life_sub.add_parser("evaluate", help="Evaluate lifecycle indices for all skills")
159
+ p_life_eval.add_argument("--db", help="Path to SQLite database with execution records")
160
+ p_life_eval.add_argument("--policy-bundle", default="v1.0")
161
+ p_life_eval.add_argument("--window", type=int, default=30, help="Evaluation window in days")
162
+ p_life_eval.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
163
+ p_life_list = life_sub.add_parser("list", help="List all tracked skills")
164
+ p_life_list.add_argument("--db", help="Path to SQLite database")
165
+ p_life_list.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
166
+
167
+ # ── storage ────────────────────────────────────────────────────────────
168
+ p_stor = sub.add_parser("storage", help="Storage management operations")
169
+ stor_sub = p_stor.add_subparsers(dest="storage_command")
170
+ p_stor_stats = stor_sub.add_parser("stats", help="Show storage statistics")
171
+ p_stor_stats.add_argument("--db", help="Path to SQLite database")
172
+ p_stor_stats.add_argument("--dir", help="Path to JSONL storage directory")
173
+ p_stor_stats.add_argument("--format", "-f", choices=["summary", "json"], default="summary")
174
+ p_stor_cleanup = stor_sub.add_parser("cleanup", help="Remove records older than threshold")
175
+ p_stor_cleanup.add_argument("--db", help="Path to SQLite database")
176
+ p_stor_cleanup.add_argument("--dir", help="Path to JSONL storage directory")
177
+ p_stor_cleanup.add_argument("--before", help="ISO datetime cutoff (e.g. 2026-01-01T00:00:00)")
178
+ p_stor_cleanup.add_argument("--retention", type=int, dest="retention_days", help="Keep records from last N days")
179
+ p_stor_vacuum = stor_sub.add_parser("vacuum", help="Optimize/compact SQLite database")
180
+ p_stor_vacuum.add_argument("--db", required=True, help="Path to SQLite database")
181
+
182
+ return parser
183
+
184
+
185
+ def main(args: list[str] | None = None) -> int:
186
+ parser = build_parser()
187
+ ns = parser.parse_args(args)
188
+
189
+ if ns.command is None:
190
+ parser.print_help()
191
+ return 0
192
+
193
+ from .core.evaluate import cmd_evaluate
194
+ from .core.validate import cmd_validate
195
+ from .governance.audit import cmd_audit_show, cmd_audit_verify
196
+ from .governance.catalog import cmd_catalog_validate
197
+ from .governance.policy import cmd_policy_check, cmd_policy_validate
198
+ from .governance.rules import cmd_rules_explain, cmd_rules_list
199
+ from .governance.signature import cmd_signature_validate
200
+ from .infrastructure.backend_list import cmd_backend_list
201
+ from .infrastructure.storage import cmd_storage_cleanup, cmd_storage_stats, cmd_storage_vacuum
202
+ from .project.init import cmd_init
203
+ from .project.lifecycle import cmd_lifecycle_evaluate, cmd_lifecycle_list
204
+ from .project.status import cmd_status
205
+ from .project.taxonomy import cmd_taxonomy_generate, cmd_taxonomy_test_intents
206
+ from .reporting.report import (
207
+ cmd_report_audit,
208
+ cmd_report_compliance,
209
+ cmd_report_dashboard,
210
+ cmd_report_execution,
211
+ cmd_report_lifecycle,
212
+ )
213
+ from .reporting.serve import cmd_serve
214
+
215
+ dispatch = {
216
+ "evaluate": cmd_evaluate,
217
+ "validate": cmd_validate,
218
+ "rules": lambda ns: (
219
+ cmd_rules_list(ns) if ns.rules_command == "list"
220
+ else cmd_rules_explain(ns) if ns.rules_command == "explain"
221
+ else _unknown(parser, "rules", ns.rules_command)
222
+ ),
223
+ "audit": lambda ns: (
224
+ cmd_audit_show(ns) if ns.audit_command == "show"
225
+ else cmd_audit_verify(ns) if ns.audit_command == "verify"
226
+ else _unknown(parser, "audit", ns.audit_command)
227
+ ),
228
+ "taxonomy": lambda ns: (
229
+ cmd_taxonomy_generate(ns) if ns.taxonomy_command == "generate"
230
+ else cmd_taxonomy_test_intents(ns) if ns.taxonomy_command == "test-intents"
231
+ else _unknown(parser, "taxonomy", ns.taxonomy_command)
232
+ ),
233
+ "backend": lambda ns: (
234
+ cmd_backend_list(ns) if ns.backend_command == "list"
235
+ else _unknown(parser, "backend", ns.backend_command)
236
+ ),
237
+ "catalog": lambda ns: (
238
+ cmd_catalog_validate(ns) if ns.catalog_command == "validate"
239
+ else _unknown(parser, "catalog", ns.catalog_command)
240
+ ),
241
+ "signature": lambda ns: (
242
+ cmd_signature_validate(ns) if ns.signature_command == "validate"
243
+ else _unknown(parser, "signature", ns.signature_command)
244
+ ),
245
+ "policy": lambda ns: (
246
+ cmd_policy_validate(ns) if ns.policy_command == "validate"
247
+ else cmd_policy_check(ns) if ns.policy_command == "check"
248
+ else _unknown(parser, "policy", ns.policy_command)
249
+ ),
250
+ "report": lambda ns: (
251
+ cmd_report_execution(ns) if ns.report_command == "execution"
252
+ else cmd_report_audit(ns) if ns.report_command == "audit"
253
+ else cmd_report_lifecycle(ns) if ns.report_command == "lifecycle"
254
+ else cmd_report_compliance(ns) if ns.report_command == "compliance"
255
+ else cmd_report_dashboard(ns) if ns.report_command == "dashboard"
256
+ else _unknown(parser, "report", ns.report_command)
257
+ ),
258
+ "serve": cmd_serve,
259
+ "init": cmd_init,
260
+ "status": cmd_status,
261
+ "lifecycle": lambda ns: (
262
+ cmd_lifecycle_evaluate(ns) if ns.lifecycle_command == "evaluate"
263
+ else cmd_lifecycle_list(ns) if ns.lifecycle_command == "list"
264
+ else _unknown(parser, "lifecycle", ns.lifecycle_command)
265
+ ),
266
+ "storage": lambda ns: (
267
+ cmd_storage_stats(ns) if ns.storage_command == "stats"
268
+ else cmd_storage_cleanup(ns) if ns.storage_command == "cleanup"
269
+ else cmd_storage_vacuum(ns) if ns.storage_command == "vacuum"
270
+ else _unknown(parser, "storage", ns.storage_command)
271
+ ),
272
+ }
273
+
274
+ handler = dispatch.get(ns.command)
275
+ if handler is None:
276
+ parser.print_help()
277
+ return 1
278
+
279
+ try:
280
+ return handler(ns)
281
+ except KeyboardInterrupt:
282
+ print("\nInterrupted.", file=sys.stderr)
283
+ return 130
284
+ except Exception as e:
285
+ print(f"Error: {e}", file=sys.stderr)
286
+ return 1
287
+
288
+
289
+ def _unknown(parser: argparse.ArgumentParser, group: str, command: str | None) -> int:
290
+ print(f"Unknown {group} command: {command}", file=sys.stderr)
291
+ parser.print_help()
292
+ return 1
293
+
294
+
295
+ if __name__ == "__main__":
296
+ sys.exit(main())
cfa/cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m cfa.cli"""
2
+ import sys
3
+
4
+ from cfa.cli import main
5
+
6
+ sys.exit(main())
cfa/cli/_helpers.py ADDED
@@ -0,0 +1,109 @@
1
+ """Shared helpers for CFA CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def load_catalog(path: str | None) -> dict[str, Any] | None:
10
+ if not path:
11
+ return None
12
+ import json
13
+ raw = Path(path).read_text(encoding="utf-8")
14
+ if path.endswith(".yaml") or path.endswith(".yml"):
15
+ try:
16
+ import yaml
17
+ return yaml.safe_load(raw)
18
+ except ImportError:
19
+ import sys
20
+ print("Error: PyYAML required for YAML catalogs. Install: pip install pyyaml", file=sys.stderr)
21
+ sys.exit(1)
22
+ return json.loads(raw)
23
+
24
+
25
+ def load_policy(path_or_version: str) -> tuple[list | None, str]:
26
+ p = Path(path_or_version)
27
+ if p.suffix in (".yaml", ".yml", ".json"):
28
+ from cfa.policy.bundle import PolicyBundle
29
+ bundle = PolicyBundle.from_yaml(str(p)) if p.suffix != ".json" else PolicyBundle.from_json(str(p))
30
+ return bundle.rules, bundle.version
31
+ return None, path_or_version
32
+
33
+
34
+ def load_structured_file(path: str, yaml_error: str) -> dict[str, Any]:
35
+ import json
36
+
37
+ raw = Path(path).read_text(encoding="utf-8")
38
+ if path.endswith((".yaml", ".yml")):
39
+ try:
40
+ import yaml
41
+ return yaml.safe_load(raw)
42
+ except ImportError:
43
+ import sys
44
+ print(yaml_error, file=sys.stderr)
45
+ sys.exit(1)
46
+ return json.loads(raw)
47
+
48
+
49
+ def apply_config_defaults(args, config):
50
+ """Apply config defaults to args for fields not explicitly set by user.
51
+
52
+ Only fills in None values — never overrides explicit CLI flags.
53
+ """
54
+ if config is None:
55
+ return
56
+ if hasattr(args, "catalog") and args.catalog is None:
57
+ cat = config.defaults.catalog
58
+ if cat and Path(cat).exists():
59
+ args.catalog = cat
60
+ if hasattr(args, "policy_bundle") and args.policy_bundle in ("v1.0", None):
61
+ pol = config.defaults.policy_bundle
62
+ if pol and Path(pol).exists():
63
+ args.policy_bundle = pol
64
+ if hasattr(args, "backend") and args.backend in ("pyspark", None):
65
+ args.backend = config.defaults.backend
66
+
67
+
68
+ def resolve_normalizer(args) -> object:
69
+ import os
70
+ import sys
71
+
72
+ if args.normalizer == "mock":
73
+ return None
74
+
75
+ has_openai = os.environ.get("OPENAI_API_KEY")
76
+ has_deepseek = os.environ.get("DEEPSEEK_API_KEY")
77
+
78
+ if args.normalizer in ("openai", "llm") or (args.normalizer == "auto" and has_openai and not has_deepseek):
79
+ from cfa.normalizer.llm import LLMNormalizerBackend, OpenAILMProvider
80
+ provider = OpenAILMProvider(
81
+ model=args.llm_model or "gpt-4o-mini",
82
+ api_key=args.llm_api_key or None,
83
+ base_url=args.llm_base_url or None,
84
+ )
85
+ return LLMNormalizerBackend(provider=provider, strict=args.llm_strict)
86
+
87
+ if args.normalizer == "deepseek" or (args.normalizer == "auto" and has_deepseek):
88
+ from cfa.normalizer.llm import LLMNormalizerBackend, OpenAILMProvider
89
+ provider = OpenAILMProvider(
90
+ model=args.llm_model or "deepseek-chat",
91
+ base_url=args.llm_base_url or "https://api.deepseek.com",
92
+ api_key=args.llm_api_key or has_deepseek or None,
93
+ )
94
+ return LLMNormalizerBackend(provider=provider, strict=args.llm_strict)
95
+
96
+ if args.normalizer == "auto":
97
+ print("Note: No LLM API key found. Using keyword-matching normalizer.", file=sys.stderr)
98
+ print("Set OPENAI_API_KEY or DEEPSEEK_API_KEY for semantic understanding.", file=sys.stderr)
99
+
100
+ return None
101
+
102
+
103
+ def resolve_config(args) -> object | None:
104
+ """Discover and return CFA config if --config is provided or auto-detect."""
105
+ from cfa.config import CfaConfig
106
+ if hasattr(args, "config") and args.config:
107
+ if Path(args.config).exists():
108
+ return CfaConfig.from_yaml(args.config)
109
+ return CfaConfig.discover()
File without changes
@@ -0,0 +1,72 @@
1
+ """cfa evaluate — run an intent through the governance pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ def cmd_evaluate(args) -> int:
10
+ from cfa.cli._helpers import apply_config_defaults, load_catalog, load_policy, resolve_config, resolve_normalizer
11
+ from cfa.cli.formatters import format_evaluate_table, format_json, format_summary
12
+ from cfa.core.kernel import KernelConfig, KernelOrchestrator
13
+
14
+ config = resolve_config(args)
15
+ apply_config_defaults(args, config)
16
+ catalog = load_catalog(args.catalog)
17
+ policy_rules, bundle_version = load_policy(args.policy_bundle)
18
+ normalizer_backend = resolve_normalizer(args)
19
+
20
+ config = KernelConfig(
21
+ policy_bundle_version=bundle_version,
22
+ backend=args.backend,
23
+ warnings_are_blocking=args.warnings_blocking,
24
+ normalizer=args.normalizer,
25
+ strict_normalization=args.strict,
26
+ )
27
+
28
+ kernel = KernelOrchestrator(
29
+ catalog=catalog, config=config, policy_rules=policy_rules,
30
+ normalizer_backend=normalizer_backend,
31
+ )
32
+ result = kernel.process(args.intent)
33
+
34
+ faults: list[dict[str, Any]] = []
35
+ if result.policy_result:
36
+ for f in result.policy_result.faults:
37
+ faults.append({
38
+ "code": f.code,
39
+ "severity": f.severity.value,
40
+ "family": f.family.value,
41
+ "message": f.message,
42
+ "remediation": list(f.remediation),
43
+ })
44
+
45
+ output: dict[str, Any] = {
46
+ "intent": args.intent,
47
+ "intent_id": result.intent_id,
48
+ "state": result.state.value,
49
+ "signature_hash": result.signature.signature_hash if result.signature else "",
50
+ "replan_count": len(result.replan_history),
51
+ "policy_bundle": config.policy_bundle_version,
52
+ "faults": faults,
53
+ }
54
+
55
+ fmt = args.format or "table"
56
+ if fmt == "json":
57
+ print(format_json(output))
58
+ elif fmt == "summary":
59
+ print(format_summary(output, faults))
60
+ else:
61
+ print(format_evaluate_table(output, faults))
62
+
63
+ if args.output:
64
+ out_path = Path(args.output)
65
+ if out_path.suffix == ".json":
66
+ out_path.write_text(format_json(output), encoding="utf-8")
67
+ else:
68
+ out_path.write_text(format_summary(output, faults), encoding="utf-8")
69
+
70
+ if args.exit_code and result.state.value == "blocked":
71
+ return 1
72
+ return 0
@@ -0,0 +1,29 @@
1
+ """cfa validate — validate an intent against a behavior spec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+
8
+ def cmd_validate(args) -> int:
9
+ from cfa.behavior import BehaviorSpec, Systematizer
10
+ from cfa.cli.formatters import _icon
11
+ from cfa.core.kernel import KernelConfig, KernelOrchestrator
12
+
13
+ try:
14
+ spec = BehaviorSpec.from_yaml(args.spec)
15
+ except Exception as e:
16
+ print(f"Error loading spec: {e}", file=sys.stderr)
17
+ return 1
18
+
19
+ _, rules = Systematizer().systematize(spec)
20
+ config = KernelConfig(policy_bundle_version=spec.name, backend=args.backend)
21
+ kernel = KernelOrchestrator(config=config, policy_rules=rules)
22
+ result = kernel.process(args.intent)
23
+
24
+ if result.is_executable:
25
+ print(f"{_icon('approved')} PASSED | {result.state.value} | {args.intent[:60]}")
26
+ return 0
27
+ else:
28
+ print(f"{_icon('blocked')} BLOCKED | {result.blocked_reason}")
29
+ return 1 if args.exit_code else 0