Clean up nested duplicate: v2/v2/skills/migration_safety.py
Browse files- v2/v2/skills/migration_safety.py +0 -154
v2/v2/skills/migration_safety.py
DELETED
|
@@ -1,154 +0,0 @@
|
|
| 1 |
-
"""Migration safety chip — the shared-database constraint, enforced.
|
| 2 |
-
|
| 3 |
-
Staging and prod share one PostgreSQL instance (belief
|
| 4 |
-
`belief_constraint_shared_db`, confidence 1.0). This chip classifies
|
| 5 |
-
every piece of SQL in the request as additive or destructive, checks
|
| 6 |
-
named tables against the actual schema (live or snapshot), and produces
|
| 7 |
-
the migration_report the discipline gate blocks on.
|
| 8 |
-
|
| 9 |
-
Classification is code, not model judgment. The model can be talked out
|
| 10 |
-
of a rule; a regex cannot.
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
import re
|
| 14 |
-
|
| 15 |
-
from kintsugi_core import (
|
| 16 |
-
BaseSkillChip,
|
| 17 |
-
EFEWeights,
|
| 18 |
-
SkillCapability,
|
| 19 |
-
SkillContext,
|
| 20 |
-
SkillDomain,
|
| 21 |
-
SkillRequest,
|
| 22 |
-
SkillResponse,
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
DESTRUCTIVE_SQL = [
|
| 26 |
-
(r"\bDROP\s+(TABLE|COLUMN|INDEX|CONSTRAINT|SCHEMA|DATABASE)\b",
|
| 27 |
-
"DROP statement"),
|
| 28 |
-
(r"\bALTER\s+TABLE\s+[\w\".]+\s+DROP\b", "ALTER TABLE ... DROP"),
|
| 29 |
-
(r"\bALTER\s+TABLE\s+[\w\".]+\s+(ALTER|MODIFY)\s+COLUMN\s+\w+\s+(SET\s+DATA\s+)?TYPE\b",
|
| 30 |
-
"in-place column type change (needs multi-step expand/contract)"),
|
| 31 |
-
(r"\bALTER\s+(TABLE\s+[\w\".]+\s+)?RENAME\b",
|
| 32 |
-
"RENAME (old code breaks against the shared DB)"),
|
| 33 |
-
(r"\bTRUNCATE\b", "TRUNCATE"),
|
| 34 |
-
(r"\bDELETE\s+FROM\s+[\w\".]+\s*(;|$)", "unqualified DELETE"),
|
| 35 |
-
(r"\bALTER\s+TABLE\s+[\w\".]+\s+ADD\s+(COLUMN\s+)?\w+[^;]*\bNOT\s+NULL\b(?![^;]*DEFAULT)",
|
| 36 |
-
"NOT NULL column without DEFAULT (breaks old code's inserts)"),
|
| 37 |
-
]
|
| 38 |
-
|
| 39 |
-
ADDITIVE_SQL = [
|
| 40 |
-
r"\bCREATE\s+TABLE\b",
|
| 41 |
-
r"\bCREATE\s+(UNIQUE\s+)?INDEX\s+CONCURRENTLY\b",
|
| 42 |
-
r"\bALTER\s+TABLE\s+[\w\".]+\s+ADD\s+(COLUMN\s+)?",
|
| 43 |
-
r"\bCREATE\s+(OR\s+REPLACE\s+)?(FUNCTION|VIEW)\b",
|
| 44 |
-
]
|
| 45 |
-
|
| 46 |
-
SQL_HINT = re.compile(
|
| 47 |
-
r"\b(CREATE|ALTER|DROP|TRUNCATE|DELETE|INSERT|UPDATE|SELECT)\b", re.I,
|
| 48 |
-
)
|
| 49 |
-
TABLE_REF = re.compile(
|
| 50 |
-
r"\b(?:TABLE|FROM|INTO|UPDATE)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?"
|
| 51 |
-
r"(?:public\.)?([a-z_][\w]*)", re.I,
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def extract_sql(text: str) -> list:
|
| 56 |
-
"""SQL from fenced code blocks, plus bare statements outside them.
|
| 57 |
-
|
| 58 |
-
Prose that *describes* destructive SQL should not block; copy-pastable
|
| 59 |
-
SQL should. Fenced blocks are always inspected; outside fences only
|
| 60 |
-
lines that parse as statements count.
|
| 61 |
-
"""
|
| 62 |
-
chunks = []
|
| 63 |
-
fenced = re.findall(r"```(?:\w*)\n(.*?)```", text, re.DOTALL)
|
| 64 |
-
for block in fenced:
|
| 65 |
-
if SQL_HINT.search(block):
|
| 66 |
-
chunks.append(block)
|
| 67 |
-
remainder = re.sub(r"```(?:\w*)\n.*?```", "", text, flags=re.DOTALL)
|
| 68 |
-
for line in remainder.splitlines():
|
| 69 |
-
stripped = line.strip()
|
| 70 |
-
if re.match(
|
| 71 |
-
r"^(CREATE|ALTER|DROP|TRUNCATE|DELETE|INSERT|UPDATE)\s", stripped, re.I
|
| 72 |
-
) and (stripped.endswith(";") or len(stripped.split()) >= 3):
|
| 73 |
-
chunks.append(stripped)
|
| 74 |
-
return chunks
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def classify_sql(sql: str) -> dict:
|
| 78 |
-
violations = []
|
| 79 |
-
for pattern, label in DESTRUCTIVE_SQL:
|
| 80 |
-
if re.search(pattern, sql, re.IGNORECASE):
|
| 81 |
-
violations.append(label)
|
| 82 |
-
additive = any(re.search(p, sql, re.IGNORECASE) for p in ADDITIVE_SQL)
|
| 83 |
-
return {
|
| 84 |
-
"sql": sql[:500],
|
| 85 |
-
"destructive": bool(violations),
|
| 86 |
-
"violations": violations,
|
| 87 |
-
"additive": additive and not violations,
|
| 88 |
-
"tables": list(dict.fromkeys(
|
| 89 |
-
t.lower() for t in TABLE_REF.findall(sql)
|
| 90 |
-
)),
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
class MigrationSafetyChip(BaseSkillChip):
|
| 95 |
-
name = "migration_safety"
|
| 96 |
-
description = "Classify SQL against the shared-db constraint and schema"
|
| 97 |
-
version = "2.0.0"
|
| 98 |
-
domain = SkillDomain.OPERATIONS
|
| 99 |
-
efe_weights = EFEWeights(
|
| 100 |
-
mission_alignment=0.15, stakeholder_benefit=0.35,
|
| 101 |
-
resource_efficiency=0.10, transparency=0.25, equity=0.15,
|
| 102 |
-
)
|
| 103 |
-
capabilities = [SkillCapability.READ_DATA]
|
| 104 |
-
consensus_actions = ["destructive_migration"]
|
| 105 |
-
|
| 106 |
-
def __init__(self, schema_tools=None):
|
| 107 |
-
super().__init__()
|
| 108 |
-
self.schema_tools = schema_tools
|
| 109 |
-
|
| 110 |
-
async def handle(self, request: SkillRequest,
|
| 111 |
-
context: SkillContext) -> SkillResponse:
|
| 112 |
-
question = context.metadata.get("question", request.raw_input)
|
| 113 |
-
session = context.metadata.get("session")
|
| 114 |
-
|
| 115 |
-
statements = [classify_sql(s) for s in extract_sql(question)]
|
| 116 |
-
destructive = [s for s in statements if s["destructive"]]
|
| 117 |
-
|
| 118 |
-
schema_check = {"source": "none", "known_tables": [],
|
| 119 |
-
"unknown_tables": [], "migration_status": ""}
|
| 120 |
-
if self.schema_tools is not None:
|
| 121 |
-
info = self.schema_tools.inspect()
|
| 122 |
-
schema_check["source"] = info.source
|
| 123 |
-
schema_check["migration_status"] = info.migration_status
|
| 124 |
-
if info.tables:
|
| 125 |
-
mentioned = {t for s in statements for t in s["tables"]}
|
| 126 |
-
known = set(info.tables)
|
| 127 |
-
schema_check["known_tables"] = sorted(mentioned & known)
|
| 128 |
-
schema_check["unknown_tables"] = sorted(mentioned - known)
|
| 129 |
-
if session and info.source != "none":
|
| 130 |
-
session.record_evidence(
|
| 131 |
-
"schema", f"{info.source}:{info.table_count} tables",
|
| 132 |
-
self.name,
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
report = {
|
| 136 |
-
"sql_found": bool(statements),
|
| 137 |
-
"statements": statements,
|
| 138 |
-
"destructive_count": len(destructive),
|
| 139 |
-
"schema_check": schema_check,
|
| 140 |
-
"shared_db_rule": (
|
| 141 |
-
"Staging and prod share one PostgreSQL instance. Migrations "
|
| 142 |
-
"must be additive, backward-compatible, and reversible."
|
| 143 |
-
),
|
| 144 |
-
}
|
| 145 |
-
summary = (
|
| 146 |
-
f"{len(statements)} SQL statement(s), "
|
| 147 |
-
f"{len(destructive)} destructive; schema source: "
|
| 148 |
-
f"{schema_check['source']}"
|
| 149 |
-
)
|
| 150 |
-
return SkillResponse(
|
| 151 |
-
content=summary, success=True, data=report,
|
| 152 |
-
requires_consensus=bool(destructive),
|
| 153 |
-
consensus_action="destructive_migration" if destructive else None,
|
| 154 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|