File size: 27,196 Bytes
5d10875
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
"""Deterministic, hash-versioned stop-ship policy engine.

This module is the launch authority. It is pure deterministic code: no LLM
output enters it, and its inputs are typed measurements/verdicts produced by
deterministic services (plus clearly-tagged agent hypotheses, which the
engine treats only as *escalation* signals, never as evidence of safety).

Semantics
---------
* Policies are JSON documents persisted under runs/agentic/policies/ and
  addressed by the SHA-256 of their canonical serialization. Editing a policy
  produces a new version; evaluations record the exact version they used.
* Outcomes are four-way (AUTOMATIC_STOP_SHIP / LAUNCH_REVIEW_REQUIRED /
  CONTINUE_INVESTIGATION / NO_LAUNCH_IMPACT) plus the INDETERMINATE
  fail-safe. INDETERMINATE fires whenever required evidence is missing
  (ground truth, lineage, policy version, telemetry denominator), sensor
  fusion verification failed, or agents conflict — and is NEVER a pass; it
  forces human escalation.
* AUTOMATIC_STOP_SHIP fires only on pre-authorized conditions written in the
  policy document; nothing else can trigger it.
* The option-selection matrix is evaluated top-down; the first row whose
  condition holds selects the recommended option. Expected loss is computed
  for every option for transparency, but SAFETY CONSTRAINTS ARE HARD
  CONSTRAINTS: infeasible (unsafe) options are excluded from selection no
  matter how cheap they are — business optimization happens only inside the
  safe feasible region.

All numeric thresholds in DEFAULT_POLICY are EXAMPLE PLACEHOLDERS (flagged
`placeholder_values: true`); a real deployment must set organization-approved
values through the versioned policy API.
"""

from __future__ import annotations

import hashlib
import json
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field

from sensorflow.agentic import store as store_mod
from sensorflow.agentic.models import now_iso

SEVERITY_ORDER = ["S0", "S1", "S2", "S3", "S4", "S5"]
CONFIDENCE_ORDER = ["INSUFFICIENT_EVIDENCE", "POSSIBLE", "LIKELY", "CONFIRMED"]

# Concentration dimension -> ODD taxonomy dimension used for the
# "reliably detectable" check in Option C (see sensorflow.safety.odd.taxonomy).
ODD_DIMENSION_MAP = {
    "construction_zone": "scenario",   # proxy: construction handled as a scenario/dynamic element
    "lighting": "lighting",
    "geo_bucket": "geography",         # declared but NOT instrumented in the taxonomy
}

DEFAULT_POLICY: Dict[str, Any] = {
    "policy_name": "example-stop-ship-policy",
    "policy_semver": "1.0.0",
    "placeholder_values": True,
    "note": ("ALL numeric thresholds below are EXAMPLE PLACEHOLDERS shipped "
             "for demonstration; they are not invented safety claims and must "
             "be replaced with organization-approved values."),
    "severity_taxonomy": {
        "S0": "No functional impact (cosmetic / logging only).",
        "S1": "Functional degradation with no safety relevance.",
        "S2": "Perception error on a non-safety-critical object, or a "
              "safety-critical class error that is fully contained downstream.",
        "S3": "Perception error on a safety-critical class (VRU) with "
              "uncertain or degraded downstream behavior.",
        "S4": "Observed unsafe downstream behavior (hard braking / evasive "
              "action / failure to yield) attributable to the error.",
        "S5": "Observed collision or imminent-collision behavior "
              "attributable to the error.",
    },
    # Ordered, exact criteria: the first matching rule assigns the severity.
    "severity_rules": [
        {"severity": "S5", "when": {"collision_observed": True}},
        {"severity": "S4", "when": {"safety_critical_class": True,
                                    "behavioral_evidence": "observed_unsafe"}},
        {"severity": "S3", "when": {"safety_critical_class": True,
                                    "downstream_contained": False}},
        {"severity": "S2", "when": {"safety_critical_class": True,
                                    "downstream_contained": True}},
        {"severity": "S1", "when": {"functional_impact": True}},
        {"severity": "S0", "when": {}},
    ],
    # Pre-authorized automatic stop-ship conditions. ONLY these can fire
    # AUTOMATIC_STOP_SHIP.
    "automatic_stop_ship": [
        {"condition_id": "ASS-1",
         "description": "Confirmed S4+ regression at statistically significant "
                        "frequency",
         "min_severity": "S4", "min_confidence": "CONFIRMED",
         "requires_significant": True},
        {"condition_id": "ASS-2",
         "description": "Confirmed S3+ novel regression with frequency lower "
                        "bound above the example ceiling",
         "min_severity": "S3", "min_confidence": "CONFIRMED",
         "requires_significant": True, "requires_novel": True,
         "min_rate_lower_bound": 1.0e-4},
    ],
    # Four-way outcome rules over Severity x Exposure x Frequency x
    # Confidence x Novelty x DownstreamConsequence (evaluated after the
    # stop-ship conditions; first match wins).
    "outcome_rules": [
        {"rule_id": "OR-1", "outcome": "LAUNCH_REVIEW_REQUIRED",
         "description": "significant S3+ regression",
         "min_severity": "S3", "requires_significant": True,
         "min_confidence": "LIKELY"},
        {"rule_id": "OR-2", "outcome": "LAUNCH_REVIEW_REQUIRED",
         "description": "novel safety-critical pattern with uncertain "
                        "downstream consequence",
         "min_severity": "S3", "requires_novel": True,
         "downstream_in": ["uncertain", "observed_degraded",
                           "observed_critical"]},
        {"rule_id": "OR-3", "outcome": "CONTINUE_INVESTIGATION",
         "description": "possible regression, evidence not yet sufficient",
         "min_severity": "S2", "max_confidence": "POSSIBLE"},
        {"rule_id": "OR-4", "outcome": "CONTINUE_INVESTIGATION",
         "description": "significant but low-severity change",
         "min_severity": "S1", "requires_significant": True},
        {"rule_id": "OR-5", "outcome": "NO_LAUNCH_IMPACT",
         "description": "not significant, not novel, no safety relevance",
         "max_severity": "S1"},
    ],
    # Option selection matrix — all seven rows, evaluated top-down.
    "option_matrix": [
        {"row": 1, "condition": "GATE_VIOLATED", "option": "STOP_SHIP",
         "description": "a release gate or pre-authorized stop-ship condition fired"},
        {"row": 2, "condition": "HIGH_CONFIDENCE_UNCONTAINED_CRITICAL",
         "option": "OPTION_A_DELAY",
         "description": "confirmed/likely S3+ failure with no validated "
                        "containment: delay launch until fixed"},
        {"row": 3, "condition": "VALIDATED_MITIGATION",
         "option": "OPTION_B_MITIGATION",
         "description": "a validated mitigation exists: ship with mitigation"},
        {"row": 4, "condition": "RELIABLY_DETECTABLE_ODD_CONCENTRATION",
         "option": "OPTION_C_REDUCED_ODD",
         "description": "failure concentrates in an ODD region that is "
                        "reliably detectable at runtime: ship with reduced ODD"},
        {"row": 5, "condition": "INSUFFICIENT_EVIDENCE",
         "option": "EXPAND_EVALUATION",
         "description": "evidence insufficient for any launch claim"},
        {"row": 6, "condition": "AGENT_OR_MODALITY_CONFLICT",
         "option": "HUMAN_SAFETY_REVIEW",
         "description": "agents or sensor modalities conflict"},
        {"row": 7, "condition": "NO_SAFETY_IMPACT", "option": "PROCEED",
         "description": "no material safety impact established"},
    ],
    # Option C parameters (EXAMPLE placeholders; never invented at runtime).
    "option_c": {
        "MAX_ACCEPTABLE_FAILURE_PROBABILITY": 1.0e-5,
        "MIN_REQUIRED_SAFETY_RECALL": 0.995,
        "MAX_ACCEPTABLE_RESIDUAL_RISK": 5.0e-5,
        "MIN_REQUIRED_EVIDENCE_CONFIDENCE": "LIKELY",
        "MAX_ALLOWED_ODD_EXCLUSION": 0.20,
        "placeholder_values": True,
    },
    # Expected-loss model (EXAMPLE placeholder unit costs).
    "expected_loss": {
        "unit": "abstract cost units (example placeholders)",
        "delay_cost": 100.0,
        "mitigation_cost": 40.0,
        "odd_reduction_cost_per_exposure_share": 300.0,
        "expand_evaluation_cost": 25.0,
        "incident_cost": 2_000_000.0,
        "exposure_events_per_release": 1_000_000,
        "mitigation_effectiveness": 0.9,
    },
    # Hard safety constraint applied to option feasibility.
    "hard_constraints": {
        "MAX_ACCEPTABLE_RESIDUAL_RISK": 5.0e-5,
        "applies_from_severity": "S3",
        "note": "options whose residual failure rate exceeds this are "
                "infeasible regardless of expected loss",
    },
}


# ------------------------------------------------------------------ versioning


def policy_hash(doc: Dict) -> str:
    blob = json.dumps({k: v for k, v in doc.items()
                       if k not in ("policy_version", "created_at")},
                      sort_keys=True, default=str)
    return hashlib.sha256(blob.encode()).hexdigest()[:16]


def save_policy(doc: Dict, actor: str = "system") -> Dict:
    doc = dict(doc)
    version = policy_hash(doc)
    doc["policy_version"] = version
    doc["created_at"] = doc.get("created_at") or now_iso()
    store_mod.write_json(doc, "policies", f"{version}.json")
    store_mod.write_json({"active": version}, "policies", "_active.json")
    store_mod.audit("policy_saved", None, actor,
                    detail=f"policy {doc.get('policy_name')} version {version}")
    return doc


def get_policy(version: Optional[str] = None) -> Dict:
    if version:
        doc = store_mod.read_json("policies", f"{version}.json")
        if doc is None:
            raise KeyError(f"Unknown policy version {version}")
        return doc
    active = store_mod.read_json("policies", "_active.json")
    if active and active.get("active"):
        doc = store_mod.read_json("policies", f"{active['active']}.json")
        if doc is not None:
            return doc
    return save_policy(DEFAULT_POLICY)


def list_policies() -> List[Dict]:
    out = []
    for name in store_mod.list_dir("policies"):
        if name.startswith("_") or not name.endswith(".json"):
            continue
        doc = store_mod.read_json("policies", name)
        if doc:
            out.append({"policy_version": doc.get("policy_version"),
                        "policy_name": doc.get("policy_name"),
                        "policy_semver": doc.get("policy_semver"),
                        "placeholder_values": doc.get("placeholder_values"),
                        "created_at": doc.get("created_at")})
    return out


# ------------------------------------------------------------------ input schema


class PolicyInput(BaseModel):
    """Typed, deterministic inputs for a policy evaluation."""

    failure_id: str
    # severity inputs
    safety_critical_class: bool = False
    collision_observed: bool = False
    behavioral_evidence: str = "none"    # observed_unsafe|observed_contained|uncertain|none
    downstream_contained: bool = False
    functional_impact: bool = True
    # frequency / statistics (deterministic, from the statistical agent)
    rate: Optional[float] = None
    rate_ci: Optional[List[float]] = None
    denominator: Optional[int] = None
    significant: bool = False
    small_sample: bool = False
    # exposure / novelty / confidence
    exposure_share: Optional[float] = None
    novelty: str = "unknown"             # novel|known_regression|known_stable|unknown
    evidence_confidence: str = "INSUFFICIENT_EVIDENCE"
    downstream_consequence: str = "uncertain"
    # integrity signals
    gt_available: bool = True
    lineage_complete: bool = True
    telemetry_available: bool = True
    fusion_verdict: str = "multi_modal_supported"
    agent_conflict: bool = False
    conflict_details: List[str] = Field(default_factory=list)
    # gates + mitigation + ODD concentration
    gate_violated: bool = False
    violated_gates: List[str] = Field(default_factory=list)
    mitigation_validated: bool = False
    mitigation_description: str = ""
    concentration_dimension: Optional[str] = None
    concentrated: bool = False
    odd_detector_recall: Optional[float] = None
    odd_exclusion_share: Optional[float] = None
    residual_rate_outside_odd: Optional[float] = None


# ------------------------------------------------------------------ helpers


def _sev_ge(a: str, b: str) -> bool:
    return SEVERITY_ORDER.index(a) >= SEVERITY_ORDER.index(b)


def _conf_ge(a: str, b: str) -> bool:
    return CONFIDENCE_ORDER.index(a) >= CONFIDENCE_ORDER.index(b)


def assign_severity(policy: Dict, inp: PolicyInput) -> Dict:
    """Deterministic severity from the policy's ordered exact criteria."""
    facts = {
        "collision_observed": inp.collision_observed,
        "safety_critical_class": inp.safety_critical_class,
        "behavioral_evidence": inp.behavioral_evidence,
        "downstream_contained": inp.downstream_contained,
        "functional_impact": inp.functional_impact,
    }
    for rule in policy["severity_rules"]:
        if all(facts.get(k) == v for k, v in rule["when"].items()):
            return {"severity": rule["severity"], "matched_rule": rule,
                    "facts": facts,
                    "taxonomy_description": policy["severity_taxonomy"][rule["severity"]]}
    return {"severity": "S0", "matched_rule": None, "facts": facts,
            "taxonomy_description": policy["severity_taxonomy"]["S0"]}


def _odd_instrumented(dimension: Optional[str]) -> Dict:
    """Reliably-detectable check against the safety ODD taxonomy's
    instrumented dimensions (sensorflow.safety.odd)."""
    if dimension is None:
        return {"dimension": None, "odd_dimension": None, "instrumented": False,
                "reason": "no concentration dimension supplied"}
    odd_dim = ODD_DIMENSION_MAP.get(dimension, dimension)
    try:
        from sensorflow.safety.odd import taxonomy
        dims = taxonomy()["dimensions"]
        entry = dims.get(odd_dim)
        instrumented = bool(entry and entry.get("instrumented"))
        reason = (f"ODD taxonomy dimension '{odd_dim}' instrumented="
                  f"{instrumented}")
    except Exception as e:  # taxonomy unavailable -> fail safe (not detectable)
        instrumented = False
        reason = f"ODD taxonomy unavailable ({e}); failing safe"
    return {"dimension": dimension, "odd_dimension": odd_dim,
            "instrumented": instrumented, "reason": reason,
            "mapping_note": ("construction_zone is proxied to the 'scenario' "
                             "taxonomy dimension" if dimension == "construction_zone" else None)}


def evaluate_option_c(policy: Dict, inp: PolicyInput) -> Dict:
    """Option C (reduced ODD) feasibility from config parameters only."""
    cfg = policy["option_c"]
    checks = []

    odd = _odd_instrumented(inp.concentration_dimension)
    checks.append({"check": "odd_dimension_instrumented",
                   "actual": odd["instrumented"], "required": True,
                   "passed": odd["instrumented"], "detail": odd["reason"]})
    checks.append({"check": "concentrated", "actual": inp.concentrated,
                   "required": True, "passed": inp.concentrated})

    recall = inp.odd_detector_recall
    checks.append({
        "check": "MIN_REQUIRED_SAFETY_RECALL",
        "actual": recall, "required": cfg["MIN_REQUIRED_SAFETY_RECALL"],
        "passed": recall is not None and recall >= cfg["MIN_REQUIRED_SAFETY_RECALL"]})

    resid = inp.residual_rate_outside_odd
    checks.append({
        "check": "MAX_ACCEPTABLE_RESIDUAL_RISK",
        "actual": resid, "required": cfg["MAX_ACCEPTABLE_RESIDUAL_RISK"],
        "passed": resid is not None and resid <= cfg["MAX_ACCEPTABLE_RESIDUAL_RISK"]})

    checks.append({
        "check": "MIN_REQUIRED_EVIDENCE_CONFIDENCE",
        "actual": inp.evidence_confidence,
        "required": cfg["MIN_REQUIRED_EVIDENCE_CONFIDENCE"],
        "passed": _conf_ge(inp.evidence_confidence,
                           cfg["MIN_REQUIRED_EVIDENCE_CONFIDENCE"])})

    excl = inp.odd_exclusion_share
    checks.append({
        "check": "MAX_ALLOWED_ODD_EXCLUSION",
        "actual": excl, "required": cfg["MAX_ALLOWED_ODD_EXCLUSION"],
        "passed": excl is not None and excl <= cfg["MAX_ALLOWED_ODD_EXCLUSION"]})

    rate_in = inp.rate
    checks.append({
        "check": "MAX_ACCEPTABLE_FAILURE_PROBABILITY (post-exclusion)",
        "actual": resid, "required": cfg["MAX_ACCEPTABLE_FAILURE_PROBABILITY"],
        "passed": resid is not None and resid <= cfg["MAX_ACCEPTABLE_FAILURE_PROBABILITY"],
        "detail": f"in-ODD rate before exclusion: {rate_in}"})

    return {"feasible": all(c["passed"] for c in checks), "checks": checks,
            "config": cfg, "odd_taxonomy_check": odd}


# ------------------------------------------------------------------ expected loss


def expected_loss_table(policy: Dict, inp: PolicyInput, severity: str) -> List[Dict]:
    cfg = policy["expected_loss"]
    hard = policy["hard_constraints"]
    rate = inp.rate or 0.0
    resid_c = inp.residual_rate_outside_odd if inp.residual_rate_outside_odd is not None else rate
    excl = inp.odd_exclusion_share or 0.0
    eff = cfg["mitigation_effectiveness"] if inp.mitigation_validated else 0.0
    exposure = cfg["exposure_events_per_release"]
    safety_scoped = _sev_ge(severity, hard["applies_from_severity"])
    max_resid = hard["MAX_ACCEPTABLE_RESIDUAL_RISK"]

    rows = []
    for option, business, residual_rate in [
        ("STOP_SHIP", cfg["delay_cost"], 0.0),
        ("OPTION_A_DELAY", cfg["delay_cost"], 0.0),
        ("OPTION_B_MITIGATION", cfg["mitigation_cost"], rate * (1.0 - eff)),
        ("OPTION_C_REDUCED_ODD",
         cfg["odd_reduction_cost_per_exposure_share"] * excl, resid_c),
        ("EXPAND_EVALUATION", cfg["expand_evaluation_cost"], rate),
        ("HUMAN_SAFETY_REVIEW", cfg["expand_evaluation_cost"], rate),
        ("PROCEED", 0.0, rate),
    ]:
        risk_cost = residual_rate * exposure * cfg["incident_cost"] / max(exposure, 1)
        # normalized: expected incidents per release * incident cost
        expected_incidents = residual_rate * exposure
        total = business + expected_incidents * cfg["incident_cost"] / 1000.0
        feasible = (not safety_scoped) or (residual_rate <= max_resid)
        rows.append({
            "option": option,
            "business_cost": round(business, 2),
            "residual_failure_rate": residual_rate,
            "expected_incidents_per_release": round(expected_incidents, 4),
            "expected_loss": round(total, 2),
            "risk_cost_component": round(risk_cost, 6),
            "feasible": feasible,
            "infeasible_reason": (None if feasible else
                                  f"residual rate {residual_rate:.2e} exceeds hard "
                                  f"constraint {max_resid:.2e} at severity {severity}"),
        })
    return rows


# ------------------------------------------------------------------ evaluation


def _integrity_failures(inp: PolicyInput) -> List[str]:
    problems = []
    if not inp.gt_available:
        problems.append("ground truth unavailable or disputed")
    if not inp.lineage_complete:
        problems.append("lineage (model/dataset fingerprints) incomplete")
    if not inp.telemetry_available or not inp.denominator:
        problems.append("telemetry denominator unavailable — no valid rate claim")
    if inp.fusion_verdict == "verification_failed":
        problems.append("sensor fusion verification failed")
    if inp.agent_conflict:
        problems.append("agents produced conflicting verdicts: "
                        + "; ".join(inp.conflict_details or ["unspecified"]))
    return problems


def _matrix_condition_holds(condition: str, inp: PolicyInput, severity: str,
                            option_c: Dict, stop_ship_fired: bool) -> bool:
    if condition == "GATE_VIOLATED":
        return inp.gate_violated or stop_ship_fired
    if condition == "HIGH_CONFIDENCE_UNCONTAINED_CRITICAL":
        return (_sev_ge(severity, "S3")
                and _conf_ge(inp.evidence_confidence, "LIKELY")
                and not inp.downstream_contained
                and not inp.mitigation_validated)
    if condition == "VALIDATED_MITIGATION":
        return inp.mitigation_validated
    if condition == "RELIABLY_DETECTABLE_ODD_CONCENTRATION":
        return option_c["feasible"]
    if condition == "INSUFFICIENT_EVIDENCE":
        return (inp.evidence_confidence == "INSUFFICIENT_EVIDENCE"
                or inp.small_sample)
    if condition == "AGENT_OR_MODALITY_CONFLICT":
        return inp.agent_conflict or inp.fusion_verdict == "modality_conflict"
    if condition == "NO_SAFETY_IMPACT":
        return True
    return False


def _outcome_rule_matches(rule: Dict, inp: PolicyInput, severity: str) -> bool:
    if "min_severity" in rule and not _sev_ge(severity, rule["min_severity"]):
        return False
    if "max_severity" in rule and _sev_ge(severity, rule["max_severity"]) \
            and severity != rule["max_severity"]:
        return False
    if rule.get("requires_significant") and not inp.significant:
        return False
    if rule.get("requires_novel") and inp.novelty != "novel":
        return False
    if "min_confidence" in rule and not _conf_ge(inp.evidence_confidence,
                                                 rule["min_confidence"]):
        return False
    if "max_confidence" in rule and _conf_ge(inp.evidence_confidence,
                                             rule["max_confidence"]) \
            and inp.evidence_confidence != rule["max_confidence"]:
        return False
    if "downstream_in" in rule and inp.downstream_consequence not in rule["downstream_in"]:
        return False
    return True


def _stop_ship_condition_matches(cond: Dict, inp: PolicyInput, severity: str) -> bool:
    if "min_severity" in cond and not _sev_ge(severity, cond["min_severity"]):
        return False
    if "min_confidence" in cond and not _conf_ge(inp.evidence_confidence,
                                                 cond["min_confidence"]):
        return False
    if cond.get("requires_significant") and not inp.significant:
        return False
    if cond.get("requires_novel") and inp.novelty != "novel":
        return False
    if "min_rate_lower_bound" in cond:
        lb = (inp.rate_ci or [0.0, 0.0])[0]
        if lb < cond["min_rate_lower_bound"]:
            return False
    return True


def evaluate(inp: PolicyInput, policy: Optional[Dict] = None,
             actor: str = "policy_engine") -> Dict:
    """Deterministic policy evaluation. Returns outcome + fired rules +
    option selection + expected-loss table, and audits itself."""
    doc = policy or get_policy()
    version = doc.get("policy_version") or policy_hash(doc)

    result: Dict[str, Any] = {
        "failure_id": inp.failure_id,
        "policy_version": version,
        "policy_name": doc.get("policy_name"),
        "placeholder_values": doc.get("placeholder_values", False),
        "evaluated_at": now_iso(),
        "input": inp.model_dump(),
        "authority": "deterministic policy engine + human approval; agents are advisory",
    }

    # 1. INDETERMINATE fail-safe (never a pass).
    problems = _integrity_failures(inp)
    severity_info = assign_severity(doc, inp)
    severity = severity_info["severity"]
    result["severity"] = severity
    result["severity_assignment"] = severity_info

    if problems:
        result.update({
            "outcome": "INDETERMINATE",
            "indeterminate_reasons": problems,
            "recommended_option": "HUMAN_SAFETY_REVIEW",
            "matrix_row_fired": {"row": 6,
                                 "condition": "AGENT_OR_MODALITY_CONFLICT"
                                 if inp.agent_conflict else "INTEGRITY_FAILURE",
                                 "option": "HUMAN_SAFETY_REVIEW"},
            "expected_loss_table": expected_loss_table(doc, inp, severity),
            "option_c_evaluation": evaluate_option_c(doc, inp),
            "note": "INDETERMINATE is fail-safe: it can never authorize a "
                    "launch and always escalates to human safety review.",
        })
        store_mod.audit("policy_evaluated", inp.failure_id, actor,
                        detail=f"INDETERMINATE ({'; '.join(problems)})",
                        payload={"policy_version": version})
        return result

    # 2. Pre-authorized automatic stop-ship conditions.
    fired_stop = None
    for cond in doc["automatic_stop_ship"]:
        if _stop_ship_condition_matches(cond, inp, severity):
            fired_stop = cond
            break

    # 3. Four-way outcome.
    if fired_stop:
        outcome = "AUTOMATIC_STOP_SHIP"
        fired_rule = fired_stop
    else:
        outcome, fired_rule = "CONTINUE_INVESTIGATION", None
        for rule in doc["outcome_rules"]:
            if _outcome_rule_matches(rule, inp, severity):
                outcome, fired_rule = rule["outcome"], rule
                break

    # 4. Option matrix + hard-constrained expected loss.
    option_c = evaluate_option_c(doc, inp)
    loss_table = expected_loss_table(doc, inp, severity)
    feasible = {r["option"] for r in loss_table if r["feasible"]}

    matrix_row = None
    for row in doc["option_matrix"]:
        if not _matrix_condition_holds(row["condition"], inp, severity,
                                       option_c, fired_stop is not None):
            continue
        if row["option"] in ("STOP_SHIP", "OPTION_A_DELAY", "EXPAND_EVALUATION",
                             "HUMAN_SAFETY_REVIEW") or row["option"] in feasible:
            matrix_row = row
            break
        # matched but infeasible under the hard safety constraint -> next row
    if matrix_row is None:
        matrix_row = {"row": 6, "condition": "AGENT_OR_MODALITY_CONFLICT",
                      "option": "HUMAN_SAFETY_REVIEW",
                      "description": "no feasible matrix row — fail safe"}

    result.update({
        "outcome": outcome,
        "outcome_rule_fired": fired_rule,
        "automatic_stop_ship_condition": fired_stop,
        "recommended_option": matrix_row["option"],
        "matrix_row_fired": matrix_row,
        "option_c_evaluation": option_c,
        "expected_loss_table": loss_table,
        "hard_constraint": doc["hard_constraints"],
        "note": ("expected loss is reported for every option, but options "
                 "violating the hard safety constraint were excluded from "
                 "selection regardless of cost"),
    })
    store_mod.audit("policy_evaluated", inp.failure_id, actor,
                    detail=f"{outcome} -> {matrix_row['option']} "
                           f"(policy {version})",
                    payload={"policy_version": version, "severity": severity})
    return result