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/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
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()
|
cfa/cli/core/__init__.py
ADDED
|
File without changes
|
cfa/cli/core/evaluate.py
ADDED
|
@@ -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
|
cfa/cli/core/validate.py
ADDED
|
@@ -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
|