Clean up nested duplicate: v2/v2/skills/discipline_gate.py
Browse files- v2/v2/skills/discipline_gate.py +0 -280
v2/v2/skills/discipline_gate.py
DELETED
|
@@ -1,280 +0,0 @@
|
|
| 1 |
-
"""Discipline gate — a Kintsugi skill, not a regex filter.
|
| 2 |
-
|
| 3 |
-
Last node of every plan. Nothing reaches the user without passing here.
|
| 4 |
-
|
| 5 |
-
Three layers, in order of authority:
|
| 6 |
-
|
| 7 |
-
1. BELIEF CHECKS — the gate holds beliefs (from the BDI store) about
|
| 8 |
-
the shared-db constraint, the auth architecture, and the audit
|
| 9 |
-
findings. The draft is checked against each. A belief with
|
| 10 |
-
confidence 1.0 (operator constraint) produces a BLOCK on violation;
|
| 11 |
-
lower-confidence beliefs produce WARNs citing their source.
|
| 12 |
-
2. PATTERN CHECKS — the code-level regex layer (destructive SQL,
|
| 13 |
-
signature bypass shapes, transaction gaps). Code, so it cannot be
|
| 14 |
-
argued with.
|
| 15 |
-
3. CONFIDENCE GRADING — the gate grades how much of the answer is
|
| 16 |
-
actually evidenced: source files read this session → HIGH; packs/
|
| 17 |
-
architecture map only → MEDIUM; neither → LOW. Failed verification
|
| 18 |
-
caps at LOW; no verification of generated code caps at MEDIUM.
|
| 19 |
-
|
| 20 |
-
The gate's output is the final artifact: the (possibly blocked) answer
|
| 21 |
-
plus machine-readable flags for the API response.
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
import re
|
| 25 |
-
from dataclasses import dataclass, field
|
| 26 |
-
from enum import Enum
|
| 27 |
-
|
| 28 |
-
from kintsugi_core import (
|
| 29 |
-
BaseSkillChip,
|
| 30 |
-
BDIStore,
|
| 31 |
-
EFEWeights,
|
| 32 |
-
SkillContext,
|
| 33 |
-
SkillDomain,
|
| 34 |
-
SkillRequest,
|
| 35 |
-
SkillResponse,
|
| 36 |
-
)
|
| 37 |
-
from skills.migration_safety import classify_sql, extract_sql
|
| 38 |
-
from skills.security_review import (
|
| 39 |
-
findings_relevant_to,
|
| 40 |
-
recurring_pattern_hits,
|
| 41 |
-
touches_auth,
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
class Confidence(str, Enum):
|
| 46 |
-
HIGH = "HIGH" # claims backed by source read this session, verified
|
| 47 |
-
MEDIUM = "MEDIUM" # backed by architecture map / audit / packs
|
| 48 |
-
LOW = "LOW" # reasoning from architecture alone, or verification failed
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
class Severity(str, Enum):
|
| 52 |
-
BLOCK = "BLOCK"
|
| 53 |
-
WARN = "WARN"
|
| 54 |
-
INFO = "INFO"
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
@dataclass
|
| 58 |
-
class GateVerdict:
|
| 59 |
-
passed: bool = True
|
| 60 |
-
confidence: Confidence = Confidence.LOW
|
| 61 |
-
flags: list = field(default_factory=list)
|
| 62 |
-
requires_review: list = field(default_factory=list)
|
| 63 |
-
beliefs_consulted: list = field(default_factory=list)
|
| 64 |
-
|
| 65 |
-
def add(self, severity: Severity, message: str, belief_id: str = ""):
|
| 66 |
-
self.flags.append({
|
| 67 |
-
"severity": severity.value, "message": message,
|
| 68 |
-
"belief": belief_id,
|
| 69 |
-
})
|
| 70 |
-
if severity == Severity.BLOCK:
|
| 71 |
-
self.passed = False
|
| 72 |
-
|
| 73 |
-
def format_annotations(self) -> str:
|
| 74 |
-
lines = []
|
| 75 |
-
if self.flags:
|
| 76 |
-
lines.append("**Discipline Gate:**")
|
| 77 |
-
icons = {"BLOCK": "[BLOCK]", "WARN": "[WARN]", "INFO": "[info]"}
|
| 78 |
-
for f in self.flags:
|
| 79 |
-
src = f" (belief: {f['belief']})" if f["belief"] else ""
|
| 80 |
-
lines.append(f"- {icons[f['severity']]} {f['message']}{src}")
|
| 81 |
-
if self.requires_review:
|
| 82 |
-
lines.append(f"- Review required from: "
|
| 83 |
-
f"{', '.join(sorted(set(self.requires_review)))}")
|
| 84 |
-
lines.append(f"- Confidence: **{self.confidence.value}**")
|
| 85 |
-
return "\n".join(lines)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
class DisciplineGateChip(BaseSkillChip):
|
| 89 |
-
name = "discipline_gate"
|
| 90 |
-
description = "Belief-checked final gate with earned confidence"
|
| 91 |
-
version = "2.0.0"
|
| 92 |
-
domain = SkillDomain.SECURITY
|
| 93 |
-
efe_weights = EFEWeights(
|
| 94 |
-
mission_alignment=0.20, stakeholder_benefit=0.30,
|
| 95 |
-
resource_efficiency=0.05, transparency=0.30, equity=0.15,
|
| 96 |
-
)
|
| 97 |
-
consensus_actions = ["release_blocked_answer"]
|
| 98 |
-
|
| 99 |
-
def __init__(self, bdi: BDIStore):
|
| 100 |
-
super().__init__()
|
| 101 |
-
self.bdi = bdi
|
| 102 |
-
|
| 103 |
-
async def handle(self, request: SkillRequest,
|
| 104 |
-
context: SkillContext) -> SkillResponse:
|
| 105 |
-
question = context.metadata.get("question", request.raw_input)
|
| 106 |
-
session = context.metadata.get("session")
|
| 107 |
-
|
| 108 |
-
draft_artifact = request.parameters.get("draft") or {}
|
| 109 |
-
draft = draft_artifact.get("text", "") if isinstance(draft_artifact, dict) else str(draft_artifact)
|
| 110 |
-
analysis = request.parameters.get("analysis") or {}
|
| 111 |
-
migration_report = request.parameters.get("migration_report") or {}
|
| 112 |
-
security_report = request.parameters.get("security_report") or {}
|
| 113 |
-
verification = request.parameters.get("verification") or {}
|
| 114 |
-
|
| 115 |
-
verdict = GateVerdict()
|
| 116 |
-
|
| 117 |
-
if not draft:
|
| 118 |
-
verdict.add(Severity.BLOCK, "synthesis produced no draft")
|
| 119 |
-
return self._respond(verdict, draft, question)
|
| 120 |
-
|
| 121 |
-
self._check_shared_db(verdict, draft)
|
| 122 |
-
self._check_auth(verdict, draft, question, security_report)
|
| 123 |
-
self._check_audit_patterns(verdict, draft)
|
| 124 |
-
self._grade_confidence(
|
| 125 |
-
verdict, draft, session, analysis, verification, draft_artifact,
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
return self._respond(verdict, draft, question)
|
| 129 |
-
|
| 130 |
-
# ------------------------------------------------------------------
|
| 131 |
-
# Layer 1+2: belief-backed checks (patterns cited to beliefs)
|
| 132 |
-
# ------------------------------------------------------------------
|
| 133 |
-
|
| 134 |
-
def _check_shared_db(self, verdict: GateVerdict, draft: str) -> None:
|
| 135 |
-
belief = self.bdi.get_belief("belief_constraint_shared_db")
|
| 136 |
-
belief_id = belief.id if belief else ""
|
| 137 |
-
if belief:
|
| 138 |
-
verdict.beliefs_consulted.append(belief.id)
|
| 139 |
-
|
| 140 |
-
for sql in extract_sql(draft):
|
| 141 |
-
klass = classify_sql(sql)
|
| 142 |
-
if klass["destructive"]:
|
| 143 |
-
severity = (Severity.BLOCK
|
| 144 |
-
if belief and belief.confidence >= 1.0
|
| 145 |
-
else Severity.WARN)
|
| 146 |
-
verdict.add(
|
| 147 |
-
severity,
|
| 148 |
-
f"Draft contains destructive SQL "
|
| 149 |
-
f"({', '.join(klass['violations'])}). "
|
| 150 |
-
f"{belief.content if belief else 'Shared-db rule.'}",
|
| 151 |
-
belief_id,
|
| 152 |
-
)
|
| 153 |
-
|
| 154 |
-
def _check_auth(self, verdict: GateVerdict, draft: str, question: str,
|
| 155 |
-
security_report: dict) -> None:
|
| 156 |
-
auth_hit = (touches_auth(draft) or touches_auth(question)
|
| 157 |
-
or security_report.get("auth_touched", False))
|
| 158 |
-
if not auth_hit:
|
| 159 |
-
return
|
| 160 |
-
belief = self.bdi.get_belief("belief_arch_auth_flow")
|
| 161 |
-
if belief:
|
| 162 |
-
verdict.beliefs_consulted.append(belief.id)
|
| 163 |
-
verdict.add(
|
| 164 |
-
Severity.WARN,
|
| 165 |
-
"This touches authentication. Review with security before "
|
| 166 |
-
"merging.",
|
| 167 |
-
belief.id if belief else "",
|
| 168 |
-
)
|
| 169 |
-
verdict.requires_review.append("security")
|
| 170 |
-
|
| 171 |
-
def _check_audit_patterns(self, verdict: GateVerdict, draft: str) -> None:
|
| 172 |
-
for hit in findings_relevant_to(draft):
|
| 173 |
-
if not hit["pattern_matched"]:
|
| 174 |
-
continue
|
| 175 |
-
belief_id = f"belief_audit_{hit['id'].lower()}"
|
| 176 |
-
belief = self.bdi.get_belief(belief_id)
|
| 177 |
-
if belief:
|
| 178 |
-
verdict.beliefs_consulted.append(belief_id)
|
| 179 |
-
severity = (Severity.WARN
|
| 180 |
-
if hit["severity"] in ("CRITICAL", "HIGH")
|
| 181 |
-
else Severity.INFO)
|
| 182 |
-
verdict.add(
|
| 183 |
-
severity,
|
| 184 |
-
f"Draft walks into audit finding {hit['id']} "
|
| 185 |
-
f"({hit['title']}). {hit['advice']}",
|
| 186 |
-
belief_id,
|
| 187 |
-
)
|
| 188 |
-
for hit in recurring_pattern_hits(draft):
|
| 189 |
-
verdict.add(Severity.INFO, hit["message"])
|
| 190 |
-
|
| 191 |
-
# ------------------------------------------------------------------
|
| 192 |
-
# Layer 3: earned confidence
|
| 193 |
-
# ------------------------------------------------------------------
|
| 194 |
-
|
| 195 |
-
def _grade_confidence(self, verdict: GateVerdict, draft: str, session,
|
| 196 |
-
analysis: dict, verification: dict,
|
| 197 |
-
draft_artifact: dict) -> None:
|
| 198 |
-
files_read = set(session.files_read) if session else set()
|
| 199 |
-
files_cited = set(re.findall(
|
| 200 |
-
r"\b((?:server|client|shared|design-system)/[\w./-]+\.\w+)\b",
|
| 201 |
-
draft,
|
| 202 |
-
))
|
| 203 |
-
|
| 204 |
-
has_source = bool(files_read)
|
| 205 |
-
cites_unread = files_cited - files_read
|
| 206 |
-
has_knowledge = bool(
|
| 207 |
-
draft_artifact.get("packs_used")
|
| 208 |
-
or analysis.get("repo_available") is False
|
| 209 |
-
or self.bdi.list_beliefs()
|
| 210 |
-
)
|
| 211 |
-
|
| 212 |
-
checks = verification.get("checks", []) if verification else []
|
| 213 |
-
ran = [c for c in checks if c.get("available")]
|
| 214 |
-
failed = [c for c in ran if not c.get("passed")]
|
| 215 |
-
has_code = bool(re.search(r"```\w*\n.{10,}?```", draft, re.DOTALL))
|
| 216 |
-
|
| 217 |
-
if failed:
|
| 218 |
-
confidence = Confidence.LOW
|
| 219 |
-
verdict.add(
|
| 220 |
-
Severity.WARN,
|
| 221 |
-
f"Generated code FAILED verification "
|
| 222 |
-
f"({len(failed)}/{len(ran)} checks): "
|
| 223 |
-
f"{failed[0]['output'][:200]}",
|
| 224 |
-
)
|
| 225 |
-
elif has_source and (not has_code or ran):
|
| 226 |
-
confidence = Confidence.HIGH
|
| 227 |
-
elif has_source or has_knowledge:
|
| 228 |
-
confidence = Confidence.MEDIUM
|
| 229 |
-
if has_code and not ran:
|
| 230 |
-
verdict.add(
|
| 231 |
-
Severity.INFO,
|
| 232 |
-
"Code in this answer was not verified (tsc/tests "
|
| 233 |
-
"unavailable here) — treat as unchecked.",
|
| 234 |
-
)
|
| 235 |
-
else:
|
| 236 |
-
confidence = Confidence.LOW
|
| 237 |
-
|
| 238 |
-
if cites_unread:
|
| 239 |
-
if confidence == Confidence.HIGH:
|
| 240 |
-
confidence = Confidence.MEDIUM
|
| 241 |
-
verdict.add(
|
| 242 |
-
Severity.INFO,
|
| 243 |
-
f"Answer references files not read this session: "
|
| 244 |
-
f"{', '.join(sorted(cites_unread)[:5])} — reasoning from "
|
| 245 |
-
f"architecture for those.",
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
verdict.confidence = confidence
|
| 249 |
-
|
| 250 |
-
# ------------------------------------------------------------------
|
| 251 |
-
|
| 252 |
-
def _respond(self, verdict: GateVerdict, draft: str,
|
| 253 |
-
question: str) -> SkillResponse:
|
| 254 |
-
annotations = verdict.format_annotations()
|
| 255 |
-
if verdict.passed:
|
| 256 |
-
final_text = f"{draft}\n\n---\n{annotations}"
|
| 257 |
-
else:
|
| 258 |
-
final_text = (
|
| 259 |
-
"**BLOCKED by Discipline Gate.**\n\n"
|
| 260 |
-
"The generated answer violated a hard constraint and was "
|
| 261 |
-
"withheld.\n\n" + annotations +
|
| 262 |
-
"\n\nAsk for the safe alternative (e.g. an additive "
|
| 263 |
-
"multi-step migration) and Rivet will produce it."
|
| 264 |
-
)
|
| 265 |
-
return SkillResponse(
|
| 266 |
-
content=final_text,
|
| 267 |
-
success=True,
|
| 268 |
-
data={
|
| 269 |
-
"text": final_text,
|
| 270 |
-
"passed": verdict.passed,
|
| 271 |
-
"confidence": verdict.confidence.value,
|
| 272 |
-
"flags": verdict.flags,
|
| 273 |
-
"requires_review": sorted(set(verdict.requires_review)),
|
| 274 |
-
"beliefs_consulted": sorted(set(verdict.beliefs_consulted)),
|
| 275 |
-
},
|
| 276 |
-
requires_consensus=not verdict.passed,
|
| 277 |
-
consensus_action=(
|
| 278 |
-
"release_blocked_answer" if not verdict.passed else None
|
| 279 |
-
),
|
| 280 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|