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
@@ -0,0 +1,477 @@
1
+ """CFA Kernel Phases — the 5-phase governance pipeline executor.
2
+
3
+ Extracted from KernelOrchestrator to keep the orchestrator thin.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import uuid
9
+ from typing import Any
10
+
11
+ from cfa.audit.context import ContextRegistry
12
+ from cfa.audit.trail import AuditTrail
13
+ from cfa.core.codegen import CodeGenBackend
14
+ from cfa.core.kernel import KernelConfig, PipelinePhase
15
+ from cfa.core.planner import ExecutionPlanner
16
+ from cfa.execution.partial import (
17
+ PartialExecutionManager,
18
+ PublishState,
19
+ )
20
+ from cfa.execution.state_projection import StateProjectionProtocol
21
+ from cfa.normalizer.base import (
22
+ ConfirmationOrchestrator,
23
+ IntentNormalizer,
24
+ )
25
+ from cfa.observability.indices import ExecutionRecord
26
+ from cfa.observability.promotion import PromotionEngine, SkillState
27
+ from cfa.policy.catalog import validate_catalog
28
+ from cfa.policy.engine import PolicyEngine
29
+ from cfa.sandbox.executor import SandboxExecutor
30
+ from cfa.types import (
31
+ DecisionState,
32
+ Fault,
33
+ FaultFamily,
34
+ FaultSeverity,
35
+ KernelResult,
36
+ PolicyAction,
37
+ PolicyResult,
38
+ SemanticResolution,
39
+ StateSignature,
40
+ _utcnow,
41
+ )
42
+ from cfa.validation.runtime import RuntimeValidator
43
+ from cfa.validation.static import StaticValidator
44
+
45
+
46
+ class KernelPhases:
47
+ """Executes the 5-phase CFA governance pipeline.
48
+
49
+ Created by KernelOrchestrator with all dependencies injected.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ config: KernelConfig,
55
+ context_registry: ContextRegistry,
56
+ audit_trail: AuditTrail,
57
+ catalog: dict[str, Any],
58
+ schema_contract: dict[str, Any] | None,
59
+ normalizer: IntentNormalizer,
60
+ confirmation: ConfirmationOrchestrator,
61
+ policy: PolicyEngine,
62
+ planner: ExecutionPlanner,
63
+ codegen: CodeGenBackend,
64
+ static_validator: StaticValidator,
65
+ sandbox_executor: SandboxExecutor,
66
+ runtime_validator: RuntimeValidator,
67
+ partial_execution_manager: PartialExecutionManager,
68
+ state_projection: StateProjectionProtocol,
69
+ promotion_engine: PromotionEngine,
70
+ ) -> None:
71
+ self.config = config
72
+ self.context_registry = context_registry
73
+ self.audit_trail = audit_trail
74
+ self.catalog = catalog
75
+ self.schema_contract = schema_contract
76
+ self.normalizer = normalizer
77
+ self.confirmation = confirmation
78
+ self.policy = policy
79
+ self.planner = planner
80
+ self.codegen = codegen
81
+ self.static_validator = static_validator
82
+ self.sandbox_executor = sandbox_executor
83
+ self.runtime_validator = runtime_validator
84
+ self.partial_execution_manager = partial_execution_manager
85
+ self.state_projection = state_projection
86
+ self.promotion_engine = promotion_engine
87
+
88
+ # ── Public API ────────────────────────────────────────────────────────────
89
+
90
+ def process(self, raw_intent: str) -> KernelResult:
91
+ intent_id = str(uuid.uuid4())
92
+ result = KernelResult(intent_id=intent_id, state=DecisionState.BLOCKED)
93
+
94
+ if self.config.phase_formalize:
95
+ signature, early = self._phase_formalize(intent_id, raw_intent, result)
96
+ if early:
97
+ return result
98
+ result.signature = signature
99
+ else:
100
+ return result
101
+
102
+ if self.config.phase_govern:
103
+ signature, policy_ok, replan_count = self._phase_govern(intent_id, result)
104
+ if not policy_ok:
105
+ return result
106
+ else:
107
+ replan_count = 0
108
+
109
+ if self.config.phase_generate and self.config.enable_planning:
110
+ early = self._phase_generate(intent_id, signature, result)
111
+ if early:
112
+ return result
113
+ if self.config.phase_execute and self.config.enable_sandbox:
114
+ early = self._phase_execute(intent_id, signature, result, replan_count)
115
+ if early:
116
+ return result
117
+
118
+ self._phase_validate(intent_id, signature, result, replan_count)
119
+ return result
120
+
121
+ # ── Phase 1: Formalize ────────────────────────────────────────────────────
122
+
123
+ def _phase_formalize(
124
+ self, intent_id: str, raw_intent: str, result: KernelResult
125
+ ) -> tuple[StateSignature | None, bool]:
126
+ if self.config.strict_normalization:
127
+ catalog_result = validate_catalog(self.catalog, require_datasets=True)
128
+ if not catalog_result.valid:
129
+ fault = catalog_result.to_fault()
130
+ result.state = DecisionState.BLOCKED
131
+ result.blocked_reason = fault.message
132
+ result.policy_result = PolicyResult(
133
+ action=PolicyAction.BLOCK, faults=[fault],
134
+ reasoning="; ".join(catalog_result.messages),
135
+ )
136
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "catalog_validation", "blocked",
137
+ issues=catalog_result.messages)
138
+ result.add_event(PipelinePhase.FORMALIZE, "catalog_validation", "blocked",
139
+ issues=catalog_result.messages)
140
+ return None, True
141
+
142
+ environment_state = self.context_registry.get_environment_state()
143
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "environment_state_consulted", "ok",
144
+ version_id=self.context_registry.version_id)
145
+ result.add_event(PipelinePhase.FORMALIZE, "environment_state_consulted", "ok",
146
+ version_id=self.context_registry.version_id)
147
+
148
+ try:
149
+ resolution = self.normalizer.normalize(
150
+ raw_intent=raw_intent, environment_state=environment_state,
151
+ catalog=self.catalog,
152
+ context_registry_version_id=self.context_registry.version_id,
153
+ )
154
+ result.resolution = resolution
155
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "semantic_resolution", "resolved",
156
+ confidence=resolution.confidence_score,
157
+ confirmation_mode=resolution.confirmation_mode.value)
158
+ result.add_event(PipelinePhase.FORMALIZE, "semantic_resolution", "resolved",
159
+ confidence=resolution.confidence_score,
160
+ confirmation_mode=resolution.confirmation_mode.value)
161
+ except (ValueError, TypeError, ImportError, RuntimeError, KeyError) as e:
162
+ result.blocked_reason = f"Normalization failed: {e}"
163
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "normalization_error", "error", error=str(e))
164
+ result.add_event(PipelinePhase.FORMALIZE, "normalization_error", "error", error=str(e))
165
+ return None, True
166
+
167
+ if self.config.strict_normalization:
168
+ strict_fault = self._strict_normalization_fault(resolution)
169
+ if strict_fault:
170
+ result.state = DecisionState.BLOCKED
171
+ result.blocked_reason = strict_fault.message
172
+ result.policy_result = PolicyResult(
173
+ action=PolicyAction.BLOCK, faults=[strict_fault],
174
+ reasoning=strict_fault.message,
175
+ )
176
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "strict_normalization", "blocked",
177
+ fault=strict_fault.code)
178
+ result.add_event(PipelinePhase.FORMALIZE, "strict_normalization", "blocked",
179
+ fault=strict_fault.code)
180
+ return None, True
181
+
182
+ approved, confirm_reason, confirm_fault = self.confirmation.process(resolution)
183
+ self._audit(intent_id, PipelinePhase.FORMALIZE, "confirmation",
184
+ "approved" if approved else "rejected",
185
+ mode=resolution.confirmation_mode.value, reason=confirm_reason)
186
+ result.add_event(PipelinePhase.FORMALIZE, "confirmation",
187
+ "approved" if approved else "rejected",
188
+ mode=resolution.confirmation_mode.value, reason=confirm_reason)
189
+
190
+ if not approved:
191
+ result.state = DecisionState.BLOCKED
192
+ result.blocked_reason = confirm_reason
193
+ if confirm_fault:
194
+ result.policy_result = PolicyResult(
195
+ action=PolicyAction.BLOCK, faults=[confirm_fault],
196
+ reasoning=confirm_reason,
197
+ )
198
+ return None, True
199
+
200
+ return resolution.signature, False
201
+
202
+ # ── Phase 2: Govern ───────────────────────────────────────────────────────
203
+
204
+ def _phase_govern(
205
+ self, intent_id: str, result: KernelResult
206
+ ) -> tuple[StateSignature, bool, int]:
207
+ signature = result.resolution.signature # type: ignore[union-attr]
208
+ replan_count = 0
209
+
210
+ while True:
211
+ policy_result = self.policy.evaluate(signature, replan_count=replan_count)
212
+ result.policy_result = policy_result
213
+ self._audit(intent_id, PipelinePhase.GOVERN, "policy_evaluation",
214
+ policy_result.action.value, replan_count=replan_count,
215
+ faults=[f.code for f in policy_result.faults])
216
+ result.add_event(PipelinePhase.GOVERN, "policy_evaluation",
217
+ policy_result.action.value, replan_count=replan_count,
218
+ faults=[f.code for f in policy_result.faults])
219
+
220
+ if policy_result.action == PolicyAction.APPROVE:
221
+ return signature, True, replan_count
222
+ if policy_result.action == PolicyAction.BLOCK:
223
+ result.state = DecisionState.BLOCKED
224
+ result.blocked_reason = policy_result.reasoning
225
+ return signature, False, replan_count
226
+
227
+ result.replan_history.append(policy_result)
228
+ replan_count += 1
229
+ new_signature = _apply_interventions(signature, policy_result)
230
+ if new_signature is None:
231
+ result.state = DecisionState.BLOCKED
232
+ result.blocked_reason = "Replan failed: interventions not applicable."
233
+ return signature, False, replan_count
234
+
235
+ signature = new_signature
236
+ self._audit(intent_id, PipelinePhase.GOVERN, "replan_applied", "replanned",
237
+ replan_count=replan_count, interventions=policy_result.interventions)
238
+ result.add_event(PipelinePhase.GOVERN, "replan_applied", "replanned",
239
+ replan_count=replan_count, interventions=policy_result.interventions)
240
+
241
+ # ── Phase 3: Generate ─────────────────────────────────────────────────────
242
+
243
+ def _phase_generate(
244
+ self, intent_id: str, signature: StateSignature, result: KernelResult
245
+ ) -> bool:
246
+ plan = self.planner.plan(signature)
247
+ result.execution_plan = plan
248
+ self._audit(intent_id, PipelinePhase.GENERATE, "plan_generated", "ok",
249
+ step_count=plan.step_count, consistency_unit=plan.consistency_unit.value,
250
+ write_mode=plan.write_mode.value)
251
+ result.add_event(PipelinePhase.GENERATE, "plan_generated", "ok",
252
+ step_count=plan.step_count, consistency_unit=plan.consistency_unit.value,
253
+ write_mode=plan.write_mode.value)
254
+
255
+ if not self.config.enable_codegen:
256
+ return False
257
+
258
+ generated = self.codegen.generate(plan)
259
+ result.generated_code = generated
260
+ self._audit(intent_id, PipelinePhase.GENERATE, "code_generated", "ok",
261
+ language=generated.language, line_count=generated.line_count)
262
+ result.add_event(PipelinePhase.GENERATE, "code_generated", "ok",
263
+ language=generated.language, line_count=generated.line_count)
264
+
265
+ if not self.config.enable_static_validation:
266
+ return False
267
+
268
+ sv_result = self.static_validator.validate(
269
+ generated, signature, self.schema_contract, backend=self.codegen)
270
+ result.static_validation = sv_result
271
+ sv_outcome = "passed" if sv_result.passed else "blocked"
272
+ self._audit(intent_id, PipelinePhase.GENERATE, "static_validation", sv_outcome,
273
+ checks=sv_result.checks_performed, faults=[f.code for f in sv_result.faults])
274
+ result.add_event(PipelinePhase.GENERATE, "static_validation", sv_outcome,
275
+ checks=sv_result.checks_performed, faults=[f.code for f in sv_result.faults])
276
+
277
+ if not sv_result.passed:
278
+ result.state = DecisionState.BLOCKED
279
+ result.blocked_reason = f"Static validation failed: {', '.join(sv_result.fault_codes)}"
280
+ result.signature = signature
281
+ return True
282
+ return False
283
+
284
+ # ── Phase 4: Execute ──────────────────────────────────────────────────────
285
+
286
+ def _phase_execute(
287
+ self, intent_id: str, signature: StateSignature, result: KernelResult, replan_count: int,
288
+ ) -> bool:
289
+ plan = result.execution_plan
290
+ generated = result.generated_code
291
+ exec_state = self.partial_execution_manager.execute(
292
+ plan, generated, signature, self.schema_contract)
293
+ result.execution_state = exec_state
294
+ result.sandbox_result = exec_state.sandbox_result
295
+ result.runtime_validation = exec_state.runtime_validation
296
+
297
+ self._audit(intent_id, PipelinePhase.EXECUTE, "execution_completed",
298
+ exec_state.publish_state.value, publish_state=exec_state.publish_state.value,
299
+ quarantined=exec_state.quarantined_steps,
300
+ committed=exec_state.committed_steps)
301
+ result.add_event(PipelinePhase.EXECUTE, "execution_completed",
302
+ exec_state.publish_state.value, publish_state=exec_state.publish_state.value,
303
+ quarantined=exec_state.quarantined_steps,
304
+ committed=exec_state.committed_steps)
305
+
306
+ projection = self.state_projection.project(signature, exec_state)
307
+ self._audit(intent_id, PipelinePhase.EXECUTE, "state_projected",
308
+ projection.projection_type, snapshot_version=projection.snapshot_version,
309
+ datasets_updated=projection.dataset_states_updated,
310
+ projected=projection.projected, audit_only=projection.audit_only)
311
+ result.add_event(PipelinePhase.EXECUTE, "state_projected",
312
+ projection.projection_type, snapshot_version=projection.snapshot_version,
313
+ datasets_updated=projection.dataset_states_updated,
314
+ projected=projection.projected, audit_only=projection.audit_only)
315
+
316
+ if exec_state.publish_state in (PublishState.ROLLED_BACK, PublishState.QUARANTINED,
317
+ PublishState.COMMITTED_NOT_PUBLISHED):
318
+ state_map = {
319
+ PublishState.ROLLED_BACK: (DecisionState.ROLLED_BACK, "Execution rolled back."),
320
+ PublishState.QUARANTINED: (DecisionState.QUARANTINED,
321
+ f"Steps quarantined: {exec_state.quarantined_steps}"),
322
+ PublishState.COMMITTED_NOT_PUBLISHED: (DecisionState.PARTIALLY_COMMITTED, ""),
323
+ }
324
+ d_state, reason = state_map[exec_state.publish_state]
325
+ result.state = d_state
326
+ result.blocked_reason = reason
327
+ result.signature = signature
328
+ self._finalize_execution_result(result, signature, replan_count)
329
+ return True
330
+ return False
331
+
332
+ # ── Phase 5: Validate ─────────────────────────────────────────────────────
333
+
334
+ def _phase_validate(
335
+ self, intent_id: str, signature: StateSignature, result: KernelResult, replan_count: int,
336
+ ) -> None:
337
+ if not self.config.phase_validate:
338
+ return
339
+
340
+ policy_result = result.policy_result
341
+ has_warnings = (
342
+ policy_result is not None
343
+ and any(f.severity == FaultSeverity.WARNING for f in policy_result.faults)
344
+ )
345
+
346
+ if has_warnings and self.config.warnings_are_blocking:
347
+ result.state = DecisionState.BLOCKED
348
+ result.blocked_reason = "Warnings treated as blocking (config)."
349
+ elif has_warnings or replan_count > 0:
350
+ result.state = DecisionState.APPROVED_WITH_WARNINGS
351
+ else:
352
+ result.state = DecisionState.APPROVED
353
+
354
+ result.signature = signature
355
+ self._finalize_execution_result(result, signature, replan_count)
356
+
357
+ # ── Internal helpers ──────────────────────────────────────────────────────
358
+
359
+ def _strict_normalization_fault(self, resolution: SemanticResolution) -> Fault | None:
360
+ sig = resolution.signature
361
+ if not sig.datasets:
362
+ return Fault(
363
+ code="NORMALIZATION_NO_CATALOG_DATASET_MATCH", family=FaultFamily.SEMANTIC,
364
+ severity=FaultSeverity.CRITICAL, stage="intent_normalizer",
365
+ message="Strict normalization blocked the intent because no catalog dataset was matched.",
366
+ mandatory_action=PolicyAction.BLOCK,
367
+ remediation=("Reference datasets that exist in the catalog or update the catalog.",),
368
+ )
369
+ if resolution.confidence_score < self.config.min_normalizer_confidence:
370
+ return Fault(
371
+ code="NORMALIZATION_LOW_CONFIDENCE", family=FaultFamily.SEMANTIC,
372
+ severity=FaultSeverity.HIGH, stage="intent_normalizer",
373
+ message=(
374
+ f"Strict normalization blocked: confidence {resolution.confidence_score:.2f} "
375
+ f"below {self.config.min_normalizer_confidence:.2f}."
376
+ ),
377
+ mandatory_action=PolicyAction.BLOCK,
378
+ remediation=("Make the requested datasets, target layer, and operation explicit.",),
379
+ )
380
+ return None
381
+
382
+ def _audit(
383
+ self, intent_id: str, stage: str, event_type: str, outcome: str, **details: Any
384
+ ) -> None:
385
+ self.audit_trail.record(
386
+ intent_id=intent_id, stage=str(stage), event_type=event_type, outcome=outcome,
387
+ policy_bundle_version=self.config.policy_bundle_version, **details,
388
+ )
389
+
390
+ def _finalize_execution_result(
391
+ self, result: KernelResult, signature: StateSignature, replan_count: int,
392
+ ) -> None:
393
+ intent_id = result.intent_id
394
+ final_state = result.state
395
+ exec_state = result.execution_state
396
+ policy_result = result.policy_result
397
+
398
+ self.context_registry.record_execution(
399
+ intent_id=intent_id, outcome=final_state.value,
400
+ signature_hash=signature.signature_hash,
401
+ )
402
+ self._audit(intent_id, PipelinePhase.VALIDATE, "final_decision",
403
+ final_state.value, signature_hash=signature.signature_hash,
404
+ replan_count=replan_count,
405
+ warnings=final_state == DecisionState.APPROVED_WITH_WARNINGS)
406
+ result.add_event(PipelinePhase.VALIDATE, "final_decision",
407
+ final_state.value, signature_hash=signature.signature_hash,
408
+ replan_count=replan_count,
409
+ warnings=final_state == DecisionState.APPROVED_WITH_WARNINGS)
410
+
411
+ if not self.config.enable_promotion:
412
+ return
413
+
414
+ all_faults: list[str] = []
415
+ if policy_result:
416
+ all_faults.extend(f.code for f in policy_result.faults)
417
+ if exec_state:
418
+ all_faults.extend(f.code for f in exec_state.faults)
419
+
420
+ cost_dbu = 0.0
421
+ duration_seconds = 0.0
422
+ if exec_state and exec_state.sandbox_result:
423
+ cost_dbu = exec_state.sandbox_result.aggregate_metrics.cost_dbu
424
+ duration_seconds = exec_state.sandbox_result.aggregate_metrics.duration_seconds
425
+
426
+ exec_record = ExecutionRecord(
427
+ signature_hash=signature.signature_hash, timestamp=_utcnow(),
428
+ success=final_state in (DecisionState.APPROVED, DecisionState.APPROVED_WITH_WARNINGS),
429
+ replanned=replan_count > 0, cost_dbu=cost_dbu, duration_seconds=duration_seconds,
430
+ faults=all_faults,
431
+ policy_compliant=final_state not in (DecisionState.ROLLED_BACK, DecisionState.QUARANTINED),
432
+ pii_exposure=any("PII" in code for code in all_faults),
433
+ layer_adherent=final_state != DecisionState.ROLLED_BACK,
434
+ )
435
+ self.promotion_engine.record_execution(exec_record)
436
+
437
+ skill, scores = self.promotion_engine.evaluate(
438
+ signature.signature_hash,
439
+ policy_bundle_version=self.config.policy_bundle_version,
440
+ catalog_snapshot_version=self.config.catalog_snapshot_version,
441
+ )
442
+ self._audit(intent_id, PipelinePhase.VALIDATE, "promotion_evaluation",
443
+ skill.state.value, ifo=scores.ifo, ifs=scores.ifs,
444
+ ifg=scores.ifg, idi=scores.idi, execution_count=scores.execution_count,
445
+ skill_state=skill.state.value)
446
+ result.add_event(PipelinePhase.VALIDATE, "promotion_evaluation",
447
+ skill.state.value, ifo=scores.ifo, ifs=scores.ifs,
448
+ ifg=scores.ifg, idi=scores.idi, execution_count=scores.execution_count,
449
+ skill_state=skill.state.value)
450
+
451
+ if skill.state == SkillState.ACTIVE and final_state == DecisionState.APPROVED:
452
+ result.state = DecisionState.PROMOTION_CANDIDATE
453
+
454
+
455
+ # ── Standalone helpers ───────────────────────────────────────────────────────
456
+
457
+
458
+ def _apply_interventions(
459
+ signature: StateSignature, policy_result: PolicyResult,
460
+ ) -> StateSignature | None:
461
+ fault_codes = {f.code for f in policy_result.faults}
462
+ overrides: dict[str, Any] = {}
463
+ if "GOVERNANCE_RAW_PII_IN_PROTECTED_LAYER" in fault_codes:
464
+ overrides["no_pii_raw"] = True
465
+ if "FINOPS_MISSING_TEMPORAL_PREDICATE" in fault_codes:
466
+ if not signature.constraints.partition_by:
467
+ overrides["partition_by"] = ("processing_date",)
468
+ if "CONTRACT_TYPE_ENFORCEMENT_DISABLED" in fault_codes:
469
+ overrides["enforce_types"] = True
470
+ if "CONTRACT_MISSING_MERGE_KEY" in fault_codes:
471
+ overrides["merge_key_required"] = True
472
+ if "FINOPS_SENSITIVE_WITHOUT_PARTITION" in fault_codes:
473
+ if not signature.constraints.partition_by:
474
+ overrides["partition_by"] = ("processing_date",)
475
+ if not overrides:
476
+ return None
477
+ return signature.with_constraints(**overrides)