Files
acdl/core/confidence_signal.py
T
Jon Chery 2ed2b3ae0f feat(P01): nova subcommands — thin delegates to core/* (CAP-033/034, cli-engineer)
One nova/<name>.py per user-facing core/ module. Each ≤50 lines, ≤3
FunctionDef (add_parser + run [+1 helper]), every user-function call
resolves to a core.* import, no `if` statements except `if __name__`.

Subcommands:
- nova resolve       → core.contract_resolver.resolve
- nova decommission  → core.decommission_transform.decommission_transform
- nova env-transition detect|record → core.env_transition
- nova env-check     → core.environment_check.check
- nova hitl          → core.hitl_gates.attest (+ approver_from_env)
- nova onboard       → core.onboarding.generate_env_file
- nova outbox        → core.outbox_writer.write_event
- nova publish-outputs → core.output_publisher.publish_to_ssm + format_comment
- nova policy        → core.policy_engine.get_engine + get_policy_root (status)
- nova regression    → core.regression_verify.run_regression + write_report
- nova sod           → core.separation_of_duties.check
- nova readiness     → core.submission_readiness.cli_main
- nova attestation-matrix → core.attestation_matrix.cli_main (new thin wrapper)
- nova confidence    → core.confidence_signal.cli_main (new thin wrapper)

core wrappers added (minimal): attestation_matrix.cli_main,
confidence_signal.cli_main — extracted from their __main__ blocks so
the nova subcommands stay thin.

---ci---
project: acdl
phase: 1
milestone: v1.28
status: execute
persona: cli-engineer
---/ci---
2026-08-19 22:25:00 +00:00

235 lines
8.4 KiB
Python

"""Nova Confidence Signal (REQ-19).
The platform's certified answer to "is this safe to proceed?" (vision
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
a measurable, explainable confidence signal; reliance on operator
instinct is not a substitute.
Inputs (weights sum to 1.0, D-040):
1. policy_results (0.30) — list[PolicyCheckResult] (schemas/policy_check_result.schema.json)
2. validation (0.25) — {schema: bool, stack_resolved: bool, tf_validated: bool, tf_planned: bool}
3. freshness (0.10) — {age_days: float, max_age_days: float}
4. source (0.15) — {submitter: str, commit_sha: str, signed: bool}
5. history (0.10) — {prior_rollbacks: int, prior_policy_fails: int}
6. nfrs (0.10) — {declared: list[str], conformance: float|None}
Severity -> penalty (locked, ARCHITECTURE.md §8):
critical -> hard override (score = 0, block)
high -> -0.20
medium -> -0.05
low -> -0.01
info -> 0.00
Per-env thresholds (locked, ARCHITECTURE.md §8): dev 0.50, qa 0.75, prod 0.90, dr 0.95.
Output: {score, band, perInput, reasonCodes}.
Halt with explicit reason on missing input (§8).
Spike cold-start (A-6.2): inputs 3 (freshness), 5 (history), 6 (nfrs) are
'present + neutral 0.5' because the spike is the first submission with no
history and no declared NFRs. The gate is *presence*, not *conformance* —
the 'all six inputs present' dev gate (§5) is satisfied by non-null
per-input scores.
"""
from dataclasses import dataclass, asdict
from typing import List, Literal, Optional, Dict, Any
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.metrics.event_envelope import emit, make_event, append_event
from core.metrics.decision_ledger import append as ledger_append
WEIGHTS = {
"policy": 0.30,
"validation": 0.25,
"freshness": 0.10,
"source": 0.15,
"history": 0.10,
"nfrs": 0.10,
}
PENALTY = {
"critical": None,
"high": 0.20,
"medium": 0.05,
"low": 0.01,
"info": 0.0,
}
THRESHOLDS = {"dev": 0.50, "qa": 0.75, "prod": 0.90, "dr": 0.95}
@dataclass
class Signal:
score: float
band: Literal["pass", "warn", "block"]
perInput: Dict[str, float]
reasonCodes: List[str]
def _per_input_score(name: str, raw: Any) -> tuple:
"""Return (score in [0,1], reasons list). Unknown/missing -> 0.5 + INPUT_MISSING."""
reasons: List[str] = []
if raw is None:
return 0.5, [f"INPUT_MISSING:{name}"]
if name == "policy":
pcrs = raw if isinstance(raw, list) else []
if not pcrs:
return 0.5, []
scores = []
for pcr in pcrs:
r = pcr.get("result", "skipped")
if r == "pass" or r == "skipped":
scores.append(1.0)
else:
scores.append(0.0)
return sum(scores) / len(scores), []
if name == "validation":
keys = ("schema", "stack_resolved", "tf_validated", "tf_planned")
if not isinstance(raw, dict):
return 0.5, []
trues = sum(1 for k in keys if raw.get(k))
return trues / 4.0, []
if name == "freshness":
if not isinstance(raw, dict):
return 0.5, []
age = float(raw.get("age_days", 0))
mx = float(raw.get("max_age_days", 1)) or 1
s = 1.0 - (age / mx)
return max(0.0, min(1.0, s)), []
if name == "source":
if not isinstance(raw, dict):
return 0.5, []
if raw.get("submitter") and raw.get("commit_sha"):
return 1.0, []
return 0.5, []
if name == "history":
if not isinstance(raw, dict):
return 0.5, []
rollbacks = int(raw.get("prior_rollbacks", 0))
fails = int(raw.get("prior_policy_fails", 0))
s = 1.0 - (rollbacks * 0.2 + fails * 0.1)
return max(0.0, min(1.0, s)), []
if name == "nfrs":
if not isinstance(raw, dict):
return 0.5, []
conf = raw.get("conformance")
if conf is None:
return 0.5, []
return float(conf), []
return 0.5, []
def compute(contract_id: str, environment: str,
inputs: Dict[str, Any]) -> Signal:
"""Orchestrate the 6-input weighted sum + severity penalty + band."""
missing = sorted(set(WEIGHTS.keys()) - set(inputs.keys()))
if missing:
return Signal(0.0, "block", {},
[f"INPUT_MISSING:{m}" for m in missing])
per_input: Dict[str, float] = {}
reasons: List[str] = []
base = 0.0
for name, weight in WEIGHTS.items():
raw = inputs.get(name)
s, r = _per_input_score(name, raw)
per_input[name] = s
reasons.extend(r)
base += s * weight
penalty = 0.0
policy_input = inputs.get("policy")
pcrs = policy_input if isinstance(policy_input, list) else []
critical_override = False
for pcr in pcrs:
if not isinstance(pcr, dict):
continue
if pcr.get("result") != "fail":
continue
sev = pcr.get("severity")
p = PENALTY.get(sev, 0.0)
if p is None:
# Critical PCR hard override: score = 0, band = block.
# Do NOT early-return — fall through to the event emission
# block below so the SPEC §5.8 evidence stream
# (confidence.computed -> ai.decision.made -> ...) is complete
# even on a critical override (REQ-318: a critical PCR is a
# confidence-driven escalation and must carry escalation_reason).
reasons.append(f"CRITICAL_OVERRIDE:{pcr.get('ruleId','?')}")
critical_override = True
break
penalty += p
if critical_override:
score = 0.0
band = "block"
else:
score = max(0.0, min(1.0, base - penalty))
threshold = THRESHOLDS[environment]
if score >= threshold:
band = "pass"
elif score < threshold - 0.10:
band = "block"
else:
band = "warn"
if environment == "dev" and band == "warn":
band = "block"
signal = Signal(score, band, per_input, reasons)
# Emit nova.confidence.computed + nova.ai.decision.made events (D-122).
# The "AI decision" is the confidence-gated policy engine, not an LLM.
# decision_id = run_id (or "cli-<ts>" when called from CLI without a run).
try:
run_id = os.environ.get("NOVA_RUN_ID", f"cli-{int(__import__('time').time())}")
conf_data = {"score": score, "band": band, "perInput": per_input, "reasonCodes": reasons}
emit("nova.confidence.computed", run_id, environment, conf_data, contract_id=contract_id)
decision_data = {
"decision_id": run_id,
"chosen_action": band,
"confidence": score,
"alternatives": per_input,
"human_override": band == "block",
"threshold": THRESHOLDS[environment],
}
# REQ-318 (SPEC §5.8): on a `block` band, carry escalation_reason.
# In v1.26 the only value is "confidence" — a block is always
# confidence-driven (the score fell below threshold OR a critical
# PCR fired a hard override). Future milestones may add "policy"
# (a critical PCR that is not confidence-scored); leave the door
# open but only emit "confidence" now. On pass/warn bands the
# field is ABSENT (escalation_reason is only meaningful on a
# block — it is the Post-Pilot Human Escalation Frequency
# denominator).
if band == "block":
decision_data["escalation_reason"] = "confidence"
decision_event = make_event("nova.ai.decision.made", run_id, environment, decision_data,
contract_id=contract_id, actor_type="confidence-gate",
actor_id="confidence_signal")
append_event(decision_event)
ledger_append(decision_event)
except Exception:
pass # metrics emission must never break the confidence gate
return signal
def cli_main(argv) -> int:
"""Thin CLI entry (P1): nova confidence <inputs.json> <environment>."""
if len(argv) < 3:
print("usage: confidence <inputs.json> <environment>", file=sys.stderr)
return 2
env = argv[2]
with open(argv[1], "r", encoding="utf-8") as fh:
inputs = json.load(fh)
sig = compute("cli", env, inputs)
print(json.dumps(asdict(sig), indent=2))
return 0
if __name__ == "__main__":
sys.exit(cli_main(sys.argv))