Spaces:
Running
Running
File size: 12,870 Bytes
a464cc6 027ca33 a464cc6 027ca33 a464cc6 027ca33 a464cc6 | 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 | """Granite Guardian 4.1 BYOC custom-rules audit (Phase 2 Day 5 task 2.14).
Reads a PhysicsViolationLog (engine-agnostic; V1 NumPy or V2 cvxpylayers
both work) + a CoaParseResult, applies a BYOC rule registry, emits a
GuardianAudit discriminated union (approve | flag | reject) whose shape
mirrors the canonical frontend contract at `app/shared/types.ts` L323-345.
BYOC rule schema follows docs/architecture-spec.md L187-204. Each rule:
- matches ViolationRecords by violation_type
- maps to a verdict (approve | flag | reject)
- carries optional templated strings for the audit's reasoning_trace +
flagged_concerns + blocked_recommendations fields
Verdict precedence: reject > flag > approve. If any rule fires with a
reject branch, the top-level verdict is reject (D-022 lexicographic
Tier-0 inviolable contract: COA-derived violations are inviolable).
The actual Granite Guardian 4.1 model integration (BYOC custom prompt +
think-mode trace) lands at task 2.15 + Phase 3 + Phase 4 orchestration.
This module ships the deterministic rule-engine floor that the
Guardian model wraps; the engine-agnostic boundary means Gate G5 can
pass on the rule-engine floor even before the Granite model is wired.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Final
from apex.instruct.coa_parser import CoaParseResult
from apex.shared.contracts import (
GuardianAudit,
GuardianVerdict,
PhysicsViolationLog,
ViolationRecord,
new_audit_id,
)
# ---- BYOC rule registry -------------------------------------------------
@dataclass(frozen=True)
class BYOCRule:
"""A Bring-Your-Own-Classifier rule for the Granite Guardian audit.
Concrete shape per docs/architecture-spec.md L187-204. Each rule
matches a single violation_type + emits a single verdict; multi-
verdict rules from the architecture-spec's verdict_map are
represented as separate BYOCRule instances (one per
severity-trigger).
Templated fields use str.format() placeholders: {step}, {long_g},
{lat_g}, {throttle_pct}, {brake_pa}, {speed_mps}, {severity},
{tier}. Templates that reference a field absent from the
violation's channel_values fall back to the literal placeholder
string (no crash on missing fields).
"""
rule_id: str
violation_type: str
verdict: GuardianVerdict
concern_template: str | None = None # used on verdict="flag"
block_template: str | None = None # used on verdict="reject"
reasoning_template: str | None = None # appended to reasoning_trace on any match
def _format_template(template: str, record: ViolationRecord) -> str:
"""Format a BYOC template against a ViolationRecord.
Substitutes {step}, {type}, {severity}, {tier} from record fields
and every key in record.channel_values. Missing placeholders fall
back to the literal `{placeholder}` string.
"""
fields: dict[str, object] = {
"step": record.step,
"type": record.type,
"severity": f"{record.severity:.4f}",
"tier": record.tier,
}
fields.update({k: f"{v:.4f}" for k, v in record.channel_values.items()})
try:
return template.format(**fields)
except (KeyError, IndexError):
return template
DEFAULT_RULE_REGISTRY: Final[tuple[BYOCRule, ...]] = (
BYOCRule(
rule_id="friction_ellipse_breach",
violation_type="friction_ellipse_exceeded",
verdict="flag",
concern_template=(
"Friction-ellipse breach at step {step}: long_g={long_g}, "
"lat_g={lat_g} exceeds the constant-mu envelope by "
"{severity}g. Constraint tier {tier}."
),
reasoning_template=(
"Rule friction_ellipse_breach fired on step {step} "
"(severity {severity})."
),
),
BYOCRule(
rule_id="forward_euler_inconsistency",
violation_type="forward_euler_inconsistent",
verdict="flag",
concern_template=(
"Forward-Euler kinematic break at step {step}: Delta-v vs "
"long_g residual exceeds the 1 Hz tolerance band by {severity} m/s."
),
reasoning_template=(
"Rule forward_euler_inconsistency fired on step {step}."
),
),
BYOCRule(
rule_id="bicycle_kinematic_break",
violation_type="bicycle_kinematic_break",
verdict="flag",
concern_template=(
"Bicycle-model kinematic break at step {step}: lat_g={lat_g} "
"vs steering_rad={steering_rad} at speed_mps={speed_mps} "
"disagrees by {severity}g."
),
reasoning_template=(
"Rule bicycle_kinematic_break fired on step {step}. V1 "
"small-angle bicycle model is known to false-positive at "
"race-corner speeds; V2 cvxpylayers + 8-tier Pacejka "
"supersedes this check at production fidelity."
),
),
BYOCRule(
rule_id="coa_simultaneity_breach",
violation_type="coa_simultaneity_violation",
verdict="reject",
block_template=(
"Cannot approve coaching recommendation: COA does not "
"permit simultaneous throttle + brake input at step {step} "
"(throttle_pct={throttle_pct}, brake_pa={brake_pa}). "
"This is a Tier-0 inviolable constraint per D-022 "
"lexicographic COA hierarchy."
),
reasoning_template=(
"Rule coa_simultaneity_breach fired on step {step}: "
"Tier-0 COA-derived simultaneity gate (inviolable)."
),
),
)
"""Default BYOC rule registry covering the V1 NumPy validator's four
violation types + the Tier-0 COA gate. Convergence-14 expansion to the
remaining 10 violation types lands at Phase 4 task 4.2."""
# ---- Verdict precedence -------------------------------------------------
_VERDICT_PRECEDENCE: Final[dict[GuardianVerdict, int]] = {
"approve": 0,
"flag": 1,
"reject": 2,
}
def _max_verdict(a: GuardianVerdict, b: GuardianVerdict) -> GuardianVerdict:
return a if _VERDICT_PRECEDENCE[a] >= _VERDICT_PRECEDENCE[b] else b
# ---- Guardian -----------------------------------------------------------
class Guardian:
"""Granite Guardian 4.1 BYOC custom-rules audit driver.
Stateless aside from the rule registry; .audit() can be called many
times on the same instance. Each call generates a fresh audit_id
via shared.contracts.violations.new_audit_id().
The Granite model itself is NOT loaded here; this class ships the
deterministic rule-engine floor that the Guardian model wraps.
Task 2.15 + Phase 4 wire the model in. Gate G5 (task 2.16) passes
on the rule-engine floor because the floor catches all 5
impossibilities + emits the right verdict.
"""
def __init__(self, rules: tuple[BYOCRule, ...] | None = None):
self._rules = rules if rules is not None else DEFAULT_RULE_REGISTRY
def audit(
self,
*,
violation_log: PhysicsViolationLog,
coa: CoaParseResult,
) -> GuardianAudit:
"""Apply the BYOC rule registry to `violation_log` + `coa`.
Returns a GuardianAudit whose verdict is the maximum-precedence
verdict across every rule that fired (approve if none fired).
"""
audit_id = new_audit_id()
reasoning_trace: list[str] = []
flagged_concerns: list[str] = []
blocked_recommendations: list[str] = []
top_verdict: GuardianVerdict = "approve"
# Empty log + safe CoA: approve with a single reasoning line.
if violation_log.is_empty():
reasoning_trace.append(
f"Empty violation log on {violation_log.engine}; "
f"COA driver_id={coa.driver_id} simultaneity_permitted="
f"{coa.simultaneity_permitted}. No rules fired."
)
return GuardianAudit(
verdict="approve",
reasoning_trace=tuple(reasoning_trace),
audit_id=audit_id,
)
rules_by_type: dict[str, list[BYOCRule]] = {}
for rule in self._rules:
rules_by_type.setdefault(rule.violation_type, []).append(rule)
for record in violation_log.records:
for rule in rules_by_type.get(record.type, []):
if rule.reasoning_template:
reasoning_trace.append(
_format_template(rule.reasoning_template, record)
)
if rule.verdict == "flag" and rule.concern_template:
flagged_concerns.append(
_format_template(rule.concern_template, record)
)
elif rule.verdict == "reject" and rule.block_template:
blocked_recommendations.append(
_format_template(rule.block_template, record)
)
top_verdict = _max_verdict(top_verdict, rule.verdict)
# Codex BLOCKER wave-50 close: if violation records exist but
# no BYOC rule matched (V12 8-tier Pacejka + V13 SCP iterate +
# Convergence-14 expansion types not yet in DEFAULT_RULE_REGISTRY),
# default-approve is the wrong floor. Unmatched violation types
# represent records the rule engine doesn't know how to interpret
# safely. Promote unmatched-with-records to "flag" so the
# downstream coaching report surfaces them as concerns rather
# than approving by silence. Empty-log + safe-COA path above
# remains "approve" + early-returns.
if not reasoning_trace:
unmatched_types = sorted({r.type for r in violation_log.records})
reasoning_trace.append(
f"Violation log on {violation_log.engine} carried "
f"{len(violation_log.records)} record(s) of "
f"unmatched-by-registry type(s) {unmatched_types}. "
f"Default verdict: flag (rule-engine floor does not "
f"silently approve unmatched violation records)."
)
top_verdict = _max_verdict(top_verdict, "flag")
flagged_concerns.append(
f"Unmatched violation type(s) {unmatched_types} "
f"surfaced on {violation_log.engine}; review for BYOC "
f"rule expansion + Convergence-14 ladder coverage."
)
return GuardianAudit(
verdict=top_verdict,
reasoning_trace=tuple(reasoning_trace),
audit_id=audit_id,
flagged_concerns=tuple(flagged_concerns),
blocked_recommendations=tuple(blocked_recommendations),
)
# ---- UI text-render helper (task 2.15) ---------------------------------
_RENDER_MODES: Final[tuple[str, ...]] = ("think", "no-think")
def render_audit(audit: GuardianAudit, mode: str = "think") -> str:
"""Render a GuardianAudit as UI-consumable text.
Two modes per the Granite Guardian 4.1 hybrid-thinking surface
documented at docs/architecture-spec.md L440:
- 'think' (default): includes the full reasoning_trace chain so
the UI can show the audit's thinking. Used in the /analyze
Guardian panel + the provenance footer.
- 'no-think': verdict header + concerns/blocks + audit_id only.
Used in low-latency surfaces (coaching-report header banner)
where the reasoning chain would be visually noisy.
Output is plain text; the frontend renderer (GuardianAudit
component, wave-42) handles its own markdown / structure parsing
from the discriminated-union audit object. This helper is for
backend log surfaces (provenance footer, BeMyApp submission
artifacts, paper §4 reproducibility appendix).
"""
if mode not in _RENDER_MODES:
raise ValueError(
f"render_audit mode must be one of {_RENDER_MODES}; got {mode!r}."
)
lines: list[str] = []
lines.append(f"GUARDIAN AUDIT verdict={audit.verdict} audit_id={audit.audit_id}")
if audit.verdict == "flag" and audit.flagged_concerns:
lines.append("")
lines.append("Flagged concerns:")
for concern in audit.flagged_concerns:
lines.append(f" - {concern}")
if audit.verdict == "reject" and audit.blocked_recommendations:
lines.append("")
lines.append("Blocked recommendations:")
for block in audit.blocked_recommendations:
lines.append(f" - {block}")
if mode == "think" and audit.reasoning_trace:
lines.append("")
lines.append("Reasoning trace:")
for step in audit.reasoning_trace:
lines.append(f" - {step}")
return "\n".join(lines) + "\n"
__all__ = [
"BYOCRule",
"DEFAULT_RULE_REGISTRY",
"Guardian",
"render_audit",
]
|