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/sandbox/panic.py ADDED
@@ -0,0 +1,52 @@
1
+ """Panic sandbox backend — simulates environmental faults for resilience testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from cfa.core.planner import ExecutionStep
8
+ from cfa.types import Fault, FaultFamily, FaultSeverity, PolicyAction
9
+
10
+ from . import SandboxCapabilities, StepOutcome, StepResult
11
+ from .mock import MockSandboxBackend
12
+
13
+
14
+ class PanicSandboxBackend(MockSandboxBackend):
15
+ """Backend that simulates an environmental fault mid-execution."""
16
+
17
+ def __init__(self, panic_on_step: str, panic_reason: str = "cluster_node_lost", **kwargs: Any):
18
+ super().__init__(**kwargs)
19
+ self._panic_step = panic_on_step
20
+ self._panic_reason = panic_reason
21
+
22
+ def get_capabilities(self) -> SandboxCapabilities:
23
+ return SandboxCapabilities(
24
+ backend_name="panic",
25
+ backend_version="panic-1.0",
26
+ execution_mode="simulation",
27
+ supports_rollback=True,
28
+ supports_metrics=True,
29
+ supports_environment_check=True,
30
+ )
31
+
32
+ def execute_step(
33
+ self, step: ExecutionStep, code: str, context: dict[str, Any]
34
+ ) -> StepResult:
35
+ if step.id == self._panic_step:
36
+ return StepResult(
37
+ step_id=step.id,
38
+ outcome=StepOutcome.INTERRUPTED,
39
+ error=f"Environmental fault: {self._panic_reason}",
40
+ faults=[
41
+ Fault(
42
+ code="ENVIRONMENTAL_PANIC",
43
+ family=FaultFamily.ENVIRONMENT,
44
+ severity=FaultSeverity.CRITICAL,
45
+ stage="sandbox",
46
+ message=f"Environmental fault during {step.id}: {self._panic_reason}",
47
+ mandatory_action=PolicyAction.BLOCK,
48
+ detected_before_execution=False,
49
+ )
50
+ ],
51
+ )
52
+ return super().execute_step(step, code, context)
@@ -0,0 +1,591 @@
1
+ """
2
+ CFA Storage
3
+ ===========
4
+ Portable storage for all CFA governance data.
5
+
6
+ Backends:
7
+ SqliteStorage — SQLite (stdlib), recommended for production
8
+ JsonLinesStorage — JSONL files, zero-dependency alternative
9
+
10
+ Both backends share the same management interface: stats(), cleanup(), vacuum().
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import sqlite3
17
+ import threading
18
+ from dataclasses import dataclass
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from cfa.audit.context import ContextStorageBackend
24
+ from cfa.audit.trail import AuditEvent, AuditStorageBackend
25
+
26
+ SCHEMA_VERSION = 1
27
+
28
+ _DDL = """
29
+ CREATE TABLE IF NOT EXISTS _schema (
30
+ version INTEGER PRIMARY KEY,
31
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS audit_events (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ intent_id TEXT NOT NULL,
37
+ stage TEXT NOT NULL,
38
+ event_type TEXT NOT NULL,
39
+ outcome TEXT NOT NULL,
40
+ policy_bundle_version TEXT NOT NULL DEFAULT '',
41
+ details_json TEXT NOT NULL DEFAULT '{}',
42
+ timestamp TEXT NOT NULL,
43
+ event_hash TEXT NOT NULL DEFAULT '',
44
+ previous_hash TEXT NOT NULL DEFAULT ''
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_audit_intent ON audit_events(intent_id);
48
+ CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_events(timestamp);
49
+
50
+ CREATE TABLE IF NOT EXISTS execution_records (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ signature_hash TEXT NOT NULL,
53
+ timestamp TEXT NOT NULL,
54
+ success INTEGER NOT NULL DEFAULT 1,
55
+ replanned INTEGER NOT NULL DEFAULT 0,
56
+ cost_dbu REAL NOT NULL DEFAULT 0.0,
57
+ duration_seconds REAL NOT NULL DEFAULT 0.0,
58
+ faults_json TEXT NOT NULL DEFAULT '[]',
59
+ schema_match INTEGER NOT NULL DEFAULT 1,
60
+ pii_exposure INTEGER NOT NULL DEFAULT 0,
61
+ policy_compliant INTEGER NOT NULL DEFAULT 1,
62
+ layer_adherent INTEGER NOT NULL DEFAULT 1,
63
+ max_expected_duration REAL NOT NULL DEFAULT 300.0,
64
+ max_expected_cost REAL NOT NULL DEFAULT 50.0
65
+ );
66
+
67
+ CREATE INDEX IF NOT EXISTS idx_exec_sig_hash ON execution_records(signature_hash);
68
+ CREATE INDEX IF NOT EXISTS idx_exec_timestamp ON execution_records(timestamp);
69
+
70
+ CREATE TABLE IF NOT EXISTS skill_records (
71
+ signature_hash TEXT PRIMARY KEY,
72
+ state TEXT NOT NULL DEFAULT 'candidate',
73
+ generation_metadata_json TEXT NOT NULL DEFAULT '{}',
74
+ last_evaluation TEXT,
75
+ demotion_reason TEXT NOT NULL DEFAULT '',
76
+ consecutive_inactive_windows INTEGER NOT NULL DEFAULT 0,
77
+ history_json TEXT NOT NULL DEFAULT '[]'
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS context_state (
81
+ key TEXT PRIMARY KEY,
82
+ value_json TEXT NOT NULL DEFAULT '{}',
83
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS metrics (
87
+ metric_key TEXT PRIMARY KEY,
88
+ metric_type TEXT NOT NULL DEFAULT 'counter',
89
+ value REAL NOT NULL DEFAULT 0.0,
90
+ labels_json TEXT NOT NULL DEFAULT '{}',
91
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
92
+ );
93
+ """
94
+
95
+
96
+ class SqliteStorage:
97
+ """Unified SQLite storage for all CFA governance data.
98
+
99
+ Usage::
100
+
101
+ store = SqliteStorage("cfa.db")
102
+ store.ensure_schema()
103
+
104
+ # Audit
105
+ store.audit_append(event)
106
+ events = store.audit_load_all()
107
+
108
+ # Execution records (lifecycle)
109
+ store.execution_append(record)
110
+ records = store.execution_load_by_hash(signature_hash)
111
+
112
+ # Skills
113
+ store.skill_upsert(signature_hash, skill_data)
114
+ skills = store.skill_load_all()
115
+
116
+ # Context
117
+ store.context_save(state_dict)
118
+ state = store.context_load()
119
+
120
+ # Metrics
121
+ store.metric_upsert("cfa_policy_evaluations_total", 1, {"decision": "approved"})
122
+ """
123
+
124
+ def __init__(self, db_path: str | Path) -> None:
125
+ self._path = Path(db_path)
126
+ self._local = threading.local()
127
+
128
+ @property
129
+ def _conn(self) -> sqlite3.Connection:
130
+ if not hasattr(self._local, "conn") or self._local.conn is None:
131
+ self._local.conn = sqlite3.connect(str(self._path))
132
+ self._local.conn.execute("PRAGMA journal_mode=WAL")
133
+ self._local.conn.execute("PRAGMA foreign_keys=ON")
134
+ self._local.conn.row_factory = sqlite3.Row
135
+ return self._local.conn
136
+
137
+ # ── Schema ────────────────────────────────────────────────────────────
138
+
139
+ def ensure_schema(self) -> None:
140
+ conn = self._conn
141
+ conn.executescript(_DDL)
142
+ current = conn.execute(
143
+ "SELECT MAX(version) FROM _schema"
144
+ ).fetchone()[0] or 0
145
+ if current < SCHEMA_VERSION:
146
+ conn.execute(
147
+ "INSERT OR REPLACE INTO _schema (version) VALUES (?)",
148
+ (SCHEMA_VERSION,),
149
+ )
150
+ conn.commit()
151
+
152
+ # ── Audit ─────────────────────────────────────────────────────────────
153
+
154
+ def audit_append(self, event: AuditEvent) -> None:
155
+ conn = self._conn
156
+ conn.execute(
157
+ """INSERT INTO audit_events
158
+ (intent_id, stage, event_type, outcome, policy_bundle_version,
159
+ details_json, timestamp, event_hash, previous_hash)
160
+ VALUES (?,?,?,?,?,?,?,?,?)""",
161
+ (
162
+ event.intent_id, event.stage, event.event_type, event.outcome,
163
+ event.policy_bundle_version,
164
+ json.dumps(event.details, default=str),
165
+ event.timestamp, event.event_hash, event.previous_hash,
166
+ ),
167
+ )
168
+ conn.commit()
169
+
170
+ def audit_load_all(self) -> list[AuditEvent]:
171
+ rows = self._conn.execute(
172
+ "SELECT * FROM audit_events ORDER BY id"
173
+ ).fetchall()
174
+ return [_row_to_audit_event(r) for r in rows]
175
+
176
+ def audit_load_by_intent(self, intent_id: str) -> list[AuditEvent]:
177
+ rows = self._conn.execute(
178
+ "SELECT * FROM audit_events WHERE intent_id = ? ORDER BY id",
179
+ (intent_id,),
180
+ ).fetchall()
181
+ return [_row_to_audit_event(r) for r in rows]
182
+
183
+ def audit_count(self) -> int:
184
+ return self._conn.execute(
185
+ "SELECT COUNT(*) FROM audit_events"
186
+ ).fetchone()[0]
187
+
188
+ # ── Execution Records (Lifecycle) ─────────────────────────────────────
189
+
190
+ def execution_append(self, record_dict: dict[str, Any]) -> None:
191
+ conn = self._conn
192
+ conn.execute(
193
+ """INSERT INTO execution_records
194
+ (signature_hash, timestamp, success, replanned, cost_dbu,
195
+ duration_seconds, faults_json, schema_match, pii_exposure,
196
+ policy_compliant, layer_adherent, max_expected_duration,
197
+ max_expected_cost)
198
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
199
+ (
200
+ record_dict.get("signature_hash", ""),
201
+ record_dict.get("timestamp", ""),
202
+ int(record_dict.get("success", True)),
203
+ int(record_dict.get("replanned", False)),
204
+ float(record_dict.get("cost_dbu", 0.0)),
205
+ float(record_dict.get("duration_seconds", 0.0)),
206
+ json.dumps(record_dict.get("faults", [])),
207
+ int(record_dict.get("schema_match", True)),
208
+ int(record_dict.get("pii_exposure", False)),
209
+ int(record_dict.get("policy_compliant", True)),
210
+ int(record_dict.get("layer_adherent", True)),
211
+ float(record_dict.get("max_expected_duration", 300.0)),
212
+ float(record_dict.get("max_expected_cost", 50.0)),
213
+ ),
214
+ )
215
+ conn.commit()
216
+
217
+ def execution_load_all(self) -> list[dict[str, Any]]:
218
+ rows = self._conn.execute(
219
+ "SELECT * FROM execution_records ORDER BY id"
220
+ ).fetchall()
221
+ return [_row_to_exec_dict(r) for r in rows]
222
+
223
+ def execution_load_by_hash(self, signature_hash: str) -> list[dict[str, Any]]:
224
+ rows = self._conn.execute(
225
+ "SELECT * FROM execution_records WHERE signature_hash = ? ORDER BY id",
226
+ (signature_hash,),
227
+ ).fetchall()
228
+ return [_row_to_exec_dict(r) for r in rows]
229
+
230
+ def execution_count_by_outcome(self) -> dict[str, int]:
231
+ rows = self._conn.execute(
232
+ "SELECT outcome, COUNT(*) as cnt FROM audit_events WHERE event_type='policy_evaluation' GROUP BY outcome"
233
+ ).fetchall()
234
+ return {r["outcome"]: r["cnt"] for r in rows}
235
+
236
+ # ── Skills ────────────────────────────────────────────────────────────
237
+
238
+ def skill_upsert(self, signature_hash: str, skill_data: dict[str, Any]) -> None:
239
+ conn = self._conn
240
+ conn.execute(
241
+ """INSERT OR REPLACE INTO skill_records
242
+ (signature_hash, state, generation_metadata_json,
243
+ last_evaluation, demotion_reason, consecutive_inactive_windows, history_json)
244
+ VALUES (?,?,?,?,?,?,?)""",
245
+ (
246
+ signature_hash,
247
+ skill_data.get("state", "candidate"),
248
+ json.dumps(skill_data.get("generation_metadata", {})),
249
+ skill_data.get("last_evaluation", ""),
250
+ skill_data.get("demotion_reason", ""),
251
+ skill_data.get("consecutive_inactive_windows", 0),
252
+ json.dumps(skill_data.get("history", [])),
253
+ ),
254
+ )
255
+ conn.commit()
256
+
257
+ def skill_load_all(self) -> list[dict[str, Any]]:
258
+ rows = self._conn.execute(
259
+ "SELECT * FROM skill_records ORDER BY signature_hash"
260
+ ).fetchall()
261
+ return [_row_to_skill_dict(r) for r in rows]
262
+
263
+ def skill_load(self, signature_hash: str) -> dict[str, Any] | None:
264
+ row = self._conn.execute(
265
+ "SELECT * FROM skill_records WHERE signature_hash = ?",
266
+ (signature_hash,),
267
+ ).fetchone()
268
+ return _row_to_skill_dict(row) if row else None
269
+
270
+ # ── Context ───────────────────────────────────────────────────────────
271
+
272
+ def context_save(self, state: dict[str, Any]) -> None:
273
+ conn = self._conn
274
+ for key, value in state.items():
275
+ conn.execute(
276
+ """INSERT OR REPLACE INTO context_state (key, value_json)
277
+ VALUES (?,?)""",
278
+ (key, json.dumps(value, default=str)),
279
+ )
280
+ conn.commit()
281
+
282
+ def context_load(self) -> dict[str, Any]:
283
+ rows = self._conn.execute(
284
+ "SELECT key, value_json FROM context_state"
285
+ ).fetchall()
286
+ result: dict[str, Any] = {}
287
+ for r in rows:
288
+ try:
289
+ result[r["key"]] = json.loads(r["value_json"])
290
+ except (json.JSONDecodeError, TypeError):
291
+ result[r["key"]] = r["value_json"]
292
+ return result
293
+
294
+ def context_save_snapshot(self, version_id: str, state: dict[str, Any]) -> None:
295
+ self._conn.execute(
296
+ """INSERT OR REPLACE INTO context_state (key, value_json)
297
+ VALUES (?,?)""",
298
+ (f"_snapshot_{version_id}", json.dumps(state, default=str)),
299
+ )
300
+ self._conn.commit()
301
+
302
+ def context_load_snapshot(self, version_id: str) -> dict[str, Any] | None:
303
+ row = self._conn.execute(
304
+ "SELECT value_json FROM context_state WHERE key = ?",
305
+ (f"_snapshot_{version_id}",),
306
+ ).fetchone()
307
+ if row:
308
+ try:
309
+ return json.loads(row["value_json"])
310
+ except (json.JSONDecodeError, TypeError):
311
+ pass
312
+ return None
313
+
314
+ def context_list_snapshots(self) -> list[str]:
315
+ rows = self._conn.execute(
316
+ "SELECT key FROM context_state WHERE key LIKE '_snapshot_%'"
317
+ ).fetchall()
318
+ return [r["key"].replace("_snapshot_", "") for r in rows]
319
+
320
+ # ── Metrics ───────────────────────────────────────────────────────────
321
+
322
+ def metric_upsert(
323
+ self, key: str, delta: float = 0.0, metric_type: str = "counter",
324
+ labels: dict[str, str] | None = None,
325
+ ) -> None:
326
+ conn = self._conn
327
+ labels_json = json.dumps(labels or {})
328
+ existing = conn.execute(
329
+ "SELECT value FROM metrics WHERE metric_key = ?", (key,)
330
+ ).fetchone()
331
+ if existing:
332
+ conn.execute(
333
+ "UPDATE metrics SET value = value + ?, updated_at = datetime('now') WHERE metric_key = ?",
334
+ (delta, key),
335
+ )
336
+ else:
337
+ conn.execute(
338
+ """INSERT INTO metrics (metric_key, metric_type, value, labels_json)
339
+ VALUES (?,?,?,?)""",
340
+ (key, metric_type, delta, labels_json),
341
+ )
342
+ conn.commit()
343
+
344
+ def metric_get_all(self) -> dict[str, dict[str, Any]]:
345
+ rows = self._conn.execute("SELECT * FROM metrics").fetchall()
346
+ result: dict[str, dict[str, Any]] = {}
347
+ for r in rows:
348
+ result[r["metric_key"]] = {
349
+ "type": r["metric_type"],
350
+ "value": r["value"],
351
+ "labels": json.loads(r["labels_json"]) if r["labels_json"] else {},
352
+ }
353
+ return result
354
+
355
+ # ── Maintenance ───────────────────────────────────────────────────────
356
+
357
+ def close(self) -> None:
358
+ if hasattr(self._local, "conn") and self._local.conn:
359
+ try:
360
+ self._local.conn.execute("PRAGMA journal_mode=DELETE")
361
+ self._local.conn.commit()
362
+ except Exception:
363
+ pass
364
+ self._local.conn.close()
365
+ self._local.conn = None
366
+
367
+ def vacuum(self) -> None:
368
+ self._conn.execute("VACUUM")
369
+
370
+
371
+ # ── SQLite-backed storage adapters ──────────────────────────────────────────
372
+
373
+
374
+ class SqliteAuditStorage(AuditStorageBackend):
375
+ """AuditStorageBackend backed by SqliteStorage."""
376
+
377
+ def __init__(self, store: SqliteStorage) -> None:
378
+ self._store = store
379
+
380
+ def append(self, event: AuditEvent) -> None:
381
+ self._store.audit_append(event)
382
+
383
+ def load_all(self) -> list[AuditEvent]:
384
+ return self._store.audit_load_all()
385
+
386
+ def load_by_intent(self, intent_id: str) -> list[AuditEvent]:
387
+ return self._store.audit_load_by_intent(intent_id)
388
+
389
+
390
+ class SqliteContextStorage(ContextStorageBackend):
391
+ """ContextStorageBackend backed by SqliteStorage."""
392
+
393
+ def __init__(self, store: SqliteStorage) -> None:
394
+ self._store = store
395
+
396
+ def load(self) -> dict[str, Any]:
397
+ return self._store.context_load()
398
+
399
+ def save(self, state: dict[str, Any]) -> None:
400
+ self._store.context_save(state)
401
+
402
+ def save_snapshot(self, version_id: str, state: dict[str, Any]) -> None:
403
+ self._store.context_save_snapshot(version_id, state)
404
+
405
+ def load_snapshot(self, version_id: str) -> dict[str, Any] | None:
406
+ return self._store.context_load_snapshot(version_id)
407
+
408
+ def list_snapshots(self) -> list[str]:
409
+ return self._store.context_list_snapshots()
410
+
411
+
412
+ # ── Helpers ─────────────────────────────────────────────────────────────────
413
+
414
+
415
+ def _row_to_audit_event(row: sqlite3.Row) -> AuditEvent:
416
+ return AuditEvent(
417
+ intent_id=row["intent_id"],
418
+ stage=row["stage"],
419
+ event_type=row["event_type"],
420
+ outcome=row["outcome"],
421
+ policy_bundle_version=row["policy_bundle_version"],
422
+ details=json.loads(row["details_json"]) if row["details_json"] else {},
423
+ timestamp=row["timestamp"],
424
+ event_hash=row["event_hash"],
425
+ previous_hash=row["previous_hash"],
426
+ )
427
+
428
+
429
+ def _row_to_exec_dict(row: sqlite3.Row) -> dict[str, Any]:
430
+ return {
431
+ "signature_hash": row["signature_hash"],
432
+ "timestamp": row["timestamp"],
433
+ "success": bool(row["success"]),
434
+ "replanned": bool(row["replanned"]),
435
+ "cost_dbu": row["cost_dbu"],
436
+ "duration_seconds": row["duration_seconds"],
437
+ "faults": json.loads(row["faults_json"]) if row["faults_json"] else [],
438
+ "schema_match": bool(row["schema_match"]),
439
+ "pii_exposure": bool(row["pii_exposure"]),
440
+ "policy_compliant": bool(row["policy_compliant"]),
441
+ "layer_adherent": bool(row["layer_adherent"]),
442
+ "max_expected_duration": row["max_expected_duration"],
443
+ "max_expected_cost": row["max_expected_cost"],
444
+ }
445
+
446
+
447
+ def _row_to_skill_dict(row: sqlite3.Row) -> dict[str, Any]:
448
+ return {
449
+ "signature_hash": row["signature_hash"],
450
+ "state": row["state"],
451
+ "generation_metadata": json.loads(row["generation_metadata_json"]) if row["generation_metadata_json"] else {},
452
+ "last_evaluation": row["last_evaluation"],
453
+ "demotion_reason": row["demotion_reason"],
454
+ "consecutive_inactive_windows": row["consecutive_inactive_windows"],
455
+ "history": json.loads(row["history_json"]) if row["history_json"] else [],
456
+ }
457
+
458
+
459
+ # ── Storage Stats ───────────────────────────────────────────────────────────
460
+
461
+
462
+ @dataclass
463
+ class StorageStats:
464
+ backend: str = ""
465
+ path: str = ""
466
+ file_size_bytes: int = 0
467
+ audit_events_count: int = 0
468
+ execution_records_count: int = 0
469
+ skill_records_count: int = 0
470
+ metrics_count: int = 0
471
+ oldest_record: str = ""
472
+ newest_record: str = ""
473
+
474
+ def to_dict(self) -> dict[str, Any]:
475
+ return {
476
+ "backend": self.backend,
477
+ "path": self.path,
478
+ "file_size_bytes": self.file_size_bytes,
479
+ "audit_events_count": self.audit_events_count,
480
+ "execution_records_count": self.execution_records_count,
481
+ "skill_records_count": self.skill_records_count,
482
+ "metrics_count": self.metrics_count,
483
+ "oldest_record": self.oldest_record,
484
+ "newest_record": self.newest_record,
485
+ }
486
+
487
+
488
+ # ── SqliteStorage management methods ────────────────────────────────────────
489
+
490
+
491
+ def _sqlite_storage_stats(store: SqliteStorage) -> StorageStats:
492
+ conn = store._conn
493
+ stats = StorageStats(backend="sqlite", path=str(store._path))
494
+ if store._path.exists():
495
+ stats.file_size_bytes = store._path.stat().st_size
496
+ stats.audit_events_count = conn.execute("SELECT COUNT(*) FROM audit_events").fetchone()[0]
497
+ stats.execution_records_count = conn.execute("SELECT COUNT(*) FROM execution_records").fetchone()[0]
498
+ stats.skill_records_count = conn.execute("SELECT COUNT(*) FROM skill_records").fetchone()[0]
499
+ stats.metrics_count = conn.execute("SELECT COUNT(*) FROM metrics").fetchone()[0]
500
+ oldest = conn.execute("SELECT MIN(timestamp) FROM audit_events").fetchone()[0]
501
+ newest = conn.execute("SELECT MAX(timestamp) FROM audit_events").fetchone()[0]
502
+ stats.oldest_record = oldest or ""
503
+ stats.newest_record = newest or ""
504
+ return stats
505
+
506
+
507
+ def _sqlite_storage_cleanup(store: SqliteStorage, before: str) -> int:
508
+ conn = store._conn
509
+ total = 0
510
+ for table in ("audit_events", "execution_records"):
511
+ assert table in ("audit_events", "execution_records") # whitelist
512
+ result = conn.execute(f"DELETE FROM {table} WHERE timestamp < ?", (before,))
513
+ total += result.rowcount
514
+ if total > 0:
515
+ conn.commit()
516
+ return total
517
+
518
+
519
+ # ── JsonLines Storage ───────────────────────────────────────────────────────
520
+
521
+
522
+ class JsonLinesStorage:
523
+ """JSONL-file storage with the same management interface as SqliteStorage.
524
+
525
+ Each domain writes to a separate .jsonl file inside a directory.
526
+ No schema migrations needed — the format is append-only JSON lines.
527
+
528
+ This is the zero-dependency alternative to SQLite.
529
+ """
530
+
531
+ def __init__(self, directory: str | Path) -> None:
532
+ self._dir = Path(directory)
533
+ self._dir.mkdir(parents=True, exist_ok=True)
534
+
535
+ @property
536
+ def path(self) -> Path:
537
+ return self._dir
538
+
539
+ def stats(self) -> StorageStats:
540
+ stats = StorageStats(backend="jsonl", path=str(self._dir))
541
+ total_size = 0
542
+ for pattern, count_attr in [
543
+ ("audit_*.jsonl", "audit_events_count"),
544
+ ("execution_*.jsonl", "execution_records_count"),
545
+ ("skills_*.jsonl", "skill_records_count"),
546
+ ("metrics_*.jsonl", "metrics_count"),
547
+ ]:
548
+ count = 0
549
+ for f in self._dir.glob(pattern):
550
+ total_size += f.stat().st_size
551
+ count += sum(1 for _ in _read_jsonl_lines(f))
552
+ setattr(stats, count_attr, count)
553
+ stats.file_size_bytes = total_size
554
+ return stats
555
+
556
+ def cleanup(self, before: str) -> int:
557
+ total = 0
558
+ before_dt = datetime.fromisoformat(before)
559
+ for pattern in ("audit_*.jsonl", "execution_*.jsonl"):
560
+ for f in self._dir.glob(pattern):
561
+ lines = list(_read_jsonl_lines(f))
562
+ kept = []
563
+ for line in lines:
564
+ try:
565
+ ts = json.loads(line).get("timestamp", "")
566
+ if ts:
567
+ try:
568
+ record_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
569
+ if record_dt < before_dt:
570
+ total += 1
571
+ continue
572
+ except (ValueError, TypeError):
573
+ pass
574
+ except json.JSONDecodeError:
575
+ pass
576
+ kept.append(line)
577
+ if len(kept) < len(lines):
578
+ f.write_text("\n".join(kept) + ("\n" if kept else ""), encoding="utf-8")
579
+ return total
580
+
581
+ def vacuum(self) -> None:
582
+ pass
583
+
584
+
585
+ def _read_jsonl_lines(path: Path):
586
+ if not path.exists():
587
+ return
588
+ for line in path.read_text(encoding="utf-8").splitlines():
589
+ line = line.strip()
590
+ if line:
591
+ yield line