9421442afd
VERIFY: structural — all user-facing strings Nova; behavioral — 79 tests + CI PASS; security — no creds; quality — new onboarding Nova-header test. REQ-166 complete. Internal ship_phase.sh helper added. ---ci--- project: acdl phase: 2 milestone: v1.16 status: complete phase_role: execution requirements: covered: [REQ-166] partial: [] ---/ci---
175 lines
5.7 KiB
Python
175 lines
5.7 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 sys
|
|
|
|
|
|
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 []
|
|
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:
|
|
return Signal(0.0, "block", per_input,
|
|
reasons + [f"CRITICAL_OVERRIDE:{pcr.get('ruleId','?')}"])
|
|
penalty += p
|
|
|
|
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"
|
|
return Signal(score, band, per_input, reasons)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 3:
|
|
print("usage: confidence_signal.py <inputs.json> <environment>", file=sys.stderr)
|
|
sys.exit(2)
|
|
env = sys.argv[2]
|
|
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
|
inputs = json.load(fh)
|
|
sig = compute("cli", env, inputs)
|
|
print(json.dumps(asdict(sig), indent=2)) |