178 lines
5.4 KiB
Python
178 lines
5.4 KiB
Python
"""Import compliance rules from the Sprint 1 Markdown dataset."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.config.database import AgentSessionLocal
|
|
from app.model.entities import ComplianceRule
|
|
|
|
DEFAULT_DATASET = ROOT / "docs" / "开发文档" / "20-Sprint1首批合规规则数据集.md"
|
|
|
|
ALLOWED_RULE_TYPES = {"keyword", "regex", "semantic"}
|
|
ALLOWED_SEVERITIES = {"block", "warn", "info"}
|
|
REQUIRED_HEADERS = [
|
|
"序号",
|
|
"rule_type",
|
|
"pattern",
|
|
"severity",
|
|
"category",
|
|
"suggestion",
|
|
"is_active",
|
|
"priority",
|
|
"审核状态",
|
|
"备注",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SeedComplianceRule:
|
|
rule_code: str
|
|
rule_type: str
|
|
pattern: str
|
|
severity: str
|
|
category: str
|
|
suggestion: str | None
|
|
is_active: bool
|
|
priority: int
|
|
review_status: str
|
|
note: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ImportResult:
|
|
total: int
|
|
created: int
|
|
updated: int
|
|
|
|
|
|
def load_rules_from_markdown(path: Path = DEFAULT_DATASET) -> list[SeedComplianceRule]:
|
|
rows = _read_markdown_table(path)
|
|
rules: list[SeedComplianceRule] = []
|
|
for row in rows:
|
|
sequence = int(row["序号"])
|
|
rule_type = row["rule_type"].lower()
|
|
severity = row["severity"].lower()
|
|
priority = int(row["priority"])
|
|
pattern = row["pattern"].strip()
|
|
category = row["category"].strip()
|
|
suggestion = row["suggestion"].strip() or None
|
|
note = row["备注"].strip()
|
|
|
|
if rule_type not in ALLOWED_RULE_TYPES:
|
|
raise ValueError(f"Unsupported rule_type at row {sequence}: {rule_type}")
|
|
if severity not in ALLOWED_SEVERITIES:
|
|
raise ValueError(f"Unsupported severity at row {sequence}: {severity}")
|
|
if not pattern:
|
|
raise ValueError(f"Missing pattern at row {sequence}")
|
|
if not category:
|
|
raise ValueError(f"Missing category at row {sequence}")
|
|
if priority < 1:
|
|
raise ValueError(f"Priority must be positive at row {sequence}")
|
|
|
|
rules.append(
|
|
SeedComplianceRule(
|
|
rule_code=f"CR-TEST-{sequence:03d}",
|
|
rule_type=rule_type,
|
|
pattern=pattern,
|
|
severity=severity,
|
|
category=category,
|
|
suggestion=suggestion,
|
|
is_active=_parse_bool(row["is_active"]),
|
|
priority=priority,
|
|
review_status=_normalize_review_status(row["审核状态"], note),
|
|
note=note,
|
|
)
|
|
)
|
|
return rules
|
|
|
|
|
|
def import_rules_from_markdown(
|
|
path: Path = DEFAULT_DATASET,
|
|
*,
|
|
actor_id: str = "seed:test_data",
|
|
) -> ImportResult:
|
|
rules = load_rules_from_markdown(path)
|
|
created = 0
|
|
updated = 0
|
|
|
|
with AgentSessionLocal() as session:
|
|
for seed_rule in rules:
|
|
existing = session.query(ComplianceRule).filter(ComplianceRule.rule_code == seed_rule.rule_code).one_or_none()
|
|
values = {
|
|
"rule_type": seed_rule.rule_type,
|
|
"pattern": seed_rule.pattern,
|
|
"severity": seed_rule.severity,
|
|
"category": seed_rule.category,
|
|
"suggestion": seed_rule.suggestion,
|
|
"is_active": seed_rule.is_active,
|
|
"priority": seed_rule.priority,
|
|
"updated_by": actor_id,
|
|
}
|
|
if existing is None:
|
|
session.add(ComplianceRule(rule_code=seed_rule.rule_code, created_by=actor_id, **values))
|
|
created += 1
|
|
else:
|
|
for key, value in values.items():
|
|
setattr(existing, key, value)
|
|
updated += 1
|
|
session.commit()
|
|
|
|
return ImportResult(total=len(rules), created=created, updated=updated)
|
|
|
|
|
|
def _read_markdown_table(path: Path) -> list[dict[str, str]]:
|
|
if not path.exists():
|
|
raise FileNotFoundError(path)
|
|
|
|
table_lines = [
|
|
line.strip()
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip().startswith("|") and line.strip().endswith("|")
|
|
]
|
|
if len(table_lines) < 3:
|
|
raise ValueError("Markdown table is missing or empty")
|
|
|
|
headers = _split_markdown_row(table_lines[0])
|
|
if headers != REQUIRED_HEADERS:
|
|
raise ValueError(f"Unexpected table headers: {headers}")
|
|
|
|
rows: list[dict[str, str]] = []
|
|
for line in table_lines[2:]:
|
|
cells = _split_markdown_row(line)
|
|
if len(cells) != len(headers):
|
|
raise ValueError(f"Column count mismatch: {line}")
|
|
rows.append(dict(zip(headers, cells, strict=True)))
|
|
return rows
|
|
|
|
|
|
def _split_markdown_row(line: str) -> list[str]:
|
|
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
|
|
|
|
def _parse_bool(value: str) -> bool:
|
|
normalized = value.strip().lower()
|
|
if normalized == "true":
|
|
return True
|
|
if normalized == "false":
|
|
return False
|
|
raise ValueError(f"Expected true/false, got: {value}")
|
|
|
|
|
|
def _normalize_review_status(value: str, note: str) -> str:
|
|
if "已审核" in value:
|
|
return "approved"
|
|
if "测试数据" in value or "测试数据" in note:
|
|
return "test_data"
|
|
return "pending"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
result = import_rules_from_markdown()
|
|
print(f"Imported compliance rules: total={result.total}, created={result.created}, updated={result.updated}")
|