feat(P03 W2): outcome backfill (REQ-317) + escalation_reason (REQ-318)
REQ-317: core/metrics/outcome_backfill.py backfills fact_decision.outcome pending -> succeeded/failed after run.completed/run.failed; idempotent + terminal (does not overwrite a non-pending outcome); wired into the collector. The Post-Pilot AI Decision Accuracy denominator is now grounded (fact_decision.outcome is not stuck pending). REQ-318: ai.decision.made on a block band carries escalation_reason: 'confidence' (the only value in v1.26 — a block is always confidence- driven; future milestones may add 'policy'). Persisted into fact_run by the collector. The Post-Pilot Human Escalation Frequency denominator is now grounded. ---ci--- project: acdl phase: 3 milestone: v1.26 status: execute wave: W2 ---
This commit is contained in:
@@ -54,7 +54,8 @@ def _init_store(db_path=None):
|
||||
confidence_band TEXT,
|
||||
hitl_block INTEGER,
|
||||
cost_estimate_usd REAL,
|
||||
decision_id TEXT
|
||||
decision_id TEXT,
|
||||
escalation_reason TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fact_capability (
|
||||
@@ -110,7 +111,9 @@ def _init_store(db_path=None):
|
||||
confidence REAL,
|
||||
alternatives TEXT,
|
||||
human_override INTEGER,
|
||||
escalation_reason TEXT,
|
||||
outcome TEXT,
|
||||
backfilled_at TEXT,
|
||||
event_time TEXT,
|
||||
PRIMARY KEY (decision_id)
|
||||
);
|
||||
@@ -217,14 +220,15 @@ def collect_run_manifests(db_path=None, runs_dir=None):
|
||||
INSERT OR REPLACE INTO fact_run
|
||||
(run_id, contract_id, environment, started_at, completed_at,
|
||||
exit_code, outcome, confidence_score, confidence_band,
|
||||
hitl_block, cost_estimate_usd, decision_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
hitl_block, cost_estimate_usd, decision_id, escalation_reason)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (run_id, manifest.get("contract_id", ""), manifest.get("environment", ""),
|
||||
manifest.get("started_at", ""), manifest.get("completed_at", ""),
|
||||
manifest.get("exit_code", 0), manifest.get("outcome", ""),
|
||||
conf.get("score", 0), conf.get("band", ""),
|
||||
1 if hitl.get("block") else 0,
|
||||
manifest.get("cost_estimate_usd", 0), manifest.get("decision_id", "")))
|
||||
manifest.get("cost_estimate_usd", 0), manifest.get("decision_id", ""),
|
||||
manifest.get("escalation_reason")))
|
||||
count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -232,7 +236,16 @@ def collect_run_manifests(db_path=None, runs_dir=None):
|
||||
|
||||
|
||||
def collect_decision_ledger(db_path=None, ledger_db=None):
|
||||
"""Read the Decision Ledger SQLite → fact_decision."""
|
||||
"""Read the Decision Ledger SQLite → fact_decision.
|
||||
|
||||
REQ-317: preserves a backfilled outcome. The ledger is append-only
|
||||
and the `nova.ai.decision.made` event always carries outcome=pending
|
||||
(it is emitted before apply). Once `outcome_backfill.backfill()` has
|
||||
transitioned the `fact_decision` row to succeeded/failed, a re-run of
|
||||
the collector must NOT clobber it back to pending. We therefore
|
||||
coalesce: if the existing row has a non-pending outcome, keep it +
|
||||
its backfilled_at; otherwise write pending (the event default).
|
||||
"""
|
||||
if db_path is None:
|
||||
db_path = _STORE_PATH
|
||||
if ledger_db is None:
|
||||
@@ -251,15 +264,27 @@ def collect_decision_ledger(db_path=None, ledger_db=None):
|
||||
payload = json.loads(payload_json)
|
||||
data = payload.get("data", {})
|
||||
decision_id = data.get("decision_id", run_id)
|
||||
# Preserve a backfilled outcome across collector re-runs (REQ-317).
|
||||
existing = conn.execute(
|
||||
"SELECT outcome, backfilled_at FROM fact_decision WHERE decision_id = ?",
|
||||
(decision_id,),
|
||||
).fetchone()
|
||||
if existing and existing[0] and existing[0] != "pending":
|
||||
outcome = existing[0]
|
||||
backfilled_at = existing[1]
|
||||
else:
|
||||
outcome = data.get("outcome", "pending")
|
||||
backfilled_at = data.get("backfilled_at")
|
||||
conn.execute("""
|
||||
INSERT OR REPLACE INTO fact_decision
|
||||
(decision_id, run_id, chosen_action, confidence, alternatives,
|
||||
human_override, outcome, event_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
human_override, escalation_reason, outcome, backfilled_at, event_time)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (decision_id, run_id, data.get("chosen_action", ""),
|
||||
data.get("confidence", 0), json.dumps(data.get("alternatives", {})),
|
||||
1 if data.get("human_override") else 0,
|
||||
data.get("outcome", "pending"), event_time))
|
||||
data.get("escalation_reason"),
|
||||
outcome, backfilled_at, event_time))
|
||||
count += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user