|
|
@@ -22,6 +22,7 @@ ASSERTION_TYPES = ("declared", "extracted", "observed", "inferred")
|
|
|
REVIEW_STATUSES = ("pending", "accepted", "rejected", "expired")
|
|
|
SEVERITIES = ("low", "medium", "high", "critical")
|
|
|
_LAYER_RANK = {"M0": 0, "M1": 1, "M2": 2, "M3": 3}
|
|
|
+CHALLENGE_CUP_DEMO_NAMESPACE = "https://onto.qm.cn/ontorefactor/demo/dcp-mobile/1.0"
|
|
|
_JSON_FIELDS = {
|
|
|
"ontology_elements": {"attributes": {}},
|
|
|
"governance_evidence": {"metadata": {}},
|
|
|
@@ -373,16 +374,17 @@ def validate_project(db, tenant_id: str, project_id: str, *, persist: bool = Tru
|
|
|
for element in elements:
|
|
|
accepted_out = [a for a in outgoing[element["id"]] if a["review_status"] == "accepted"]
|
|
|
accepted_in = [a for a in incoming[element["id"]] if a["review_status"] == "accepted"]
|
|
|
+ published = element["status"] == "published"
|
|
|
if element["layer"] == "M0" and element["status"] == "published" and not any(a["predicate"] == "INSTANCE_OF" for a in accepted_out):
|
|
|
findings.append(_finding("M0_UNKNOWN_TYPE", "high", "M0 事实未归类", "已发布 M0 必须实例化 M1 类型", element["id"]))
|
|
|
- if element["layer"] == "M1" and element["kind"] == "BusinessCapability" and not any(a["predicate"] == "REALIZED_BY" for a in accepted_out):
|
|
|
+ if published and element["layer"] == "M1" and element["kind"] == "BusinessCapability" and not any(a["predicate"] == "REALIZED_BY" for a in accepted_out):
|
|
|
findings.append(_finding("CAPABILITY_NOT_REALIZED", "high", "业务能力缺少软件实现", "至少需要一个 REALIZED_BY", element["id"]))
|
|
|
- if element["layer"] == "M1" and element["kind"] == "BusinessObject" and not any(a["predicate"] == "REPRESENTS" for a in accepted_in):
|
|
|
+ if published and element["layer"] == "M1" and element["kind"] == "BusinessObject" and not any(a["predicate"] == "REPRESENTS" for a in accepted_in):
|
|
|
findings.append(_finding("BUSINESS_OBJECT_NOT_REPRESENTED", "high", "业务对象缺少技术表达", "至少需要一个 REPRESENTS", element["id"]))
|
|
|
attrs = element.get("attributes") or {}
|
|
|
- if element["profile"] == "software" and attrs.get("environment") == "production" and not any(a["predicate"] == "OWNED_BY" for a in accepted_out):
|
|
|
+ if published and element["profile"] == "software" and attrs.get("environment") == "production" and not any(a["predicate"] == "OWNED_BY" for a in accepted_out):
|
|
|
findings.append(_finding("PRODUCTION_ASSET_WITHOUT_OWNER", "critical", "生产资产缺少责任人", "必须关联 Owner/Steward", element["id"]))
|
|
|
- if element["kind"] in ("Column", "PhysicalColumn") and attrs.get("sensitive"):
|
|
|
+ if published and element["kind"] in ("Column", "PhysicalColumn") and attrs.get("sensitive") and attrs.get("environment") == "production":
|
|
|
policies = [by_id.get(a.get("object_id"), {}) for a in accepted_out if a["predicate"] == "GOVERNED_BY"]
|
|
|
if not any(p.get("kind") in ("AccessPolicy", "SecurityPolicy") for p in policies):
|
|
|
findings.append(_finding("SENSITIVE_FIELD_WITHOUT_POLICY", "critical", "敏感字段缺少访问策略", "必须关联访问策略", element["id"]))
|
|
|
@@ -476,12 +478,47 @@ def overview(db, tenant_id: str, project_id: str) -> dict:
|
|
|
profiles[item["profile"]] = item["n"]
|
|
|
def count(table: str, suffix: str = "") -> int:
|
|
|
return int(db.fetchone(f"SELECT COUNT(*) n FROM {table} WHERE tenant_id=? AND project_id=? {suffix}", (tenant_id, project_id))["n"])
|
|
|
- return {"project": project, "layers": layers, "profiles": profiles, "totals": {
|
|
|
+ result = {"project": project, "layers": layers, "profiles": profiles, "totals": {
|
|
|
"elements": sum(layers.values()), "assertions": count("governance_assertions"),
|
|
|
"pending_reviews": count("governance_assertions", "AND review_status='pending'"),
|
|
|
"evidence": count("governance_evidence"), "rules": count("governance_rules", "AND enabled=1"),
|
|
|
"open_issues": count("governance_issues", "AND status='open'"), "agent_runs": count("governance_agent_runs"),
|
|
|
}}
|
|
|
+ result["recent_issues"] = rows("governance_issues", db.fetchall(
|
|
|
+ "SELECT * FROM governance_issues WHERE tenant_id=? AND project_id=? AND status='open' ORDER BY "
|
|
|
+ "CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,created_at DESC LIMIT 5",
|
|
|
+ (tenant_id, project_id),
|
|
|
+ ))
|
|
|
+ if project["namespace"] == CHALLENGE_CUP_DEMO_NAMESPACE:
|
|
|
+ mobile = db.fetchone(
|
|
|
+ "SELECT id,name FROM ontology_elements WHERE tenant_id=? AND project_id=? AND uri=?",
|
|
|
+ (tenant_id, project_id, f"{CHALLENGE_CUP_DEMO_NAMESPACE}#MobileColumn"),
|
|
|
+ )
|
|
|
+ latest_run = db.fetchone(
|
|
|
+ "SELECT id,status,llm_model,prompt_version,input_tokens,output_tokens,llm_latency_ms,created_at "
|
|
|
+ "FROM governance_agent_runs WHERE tenant_id=? AND project_id=? AND status='completed' "
|
|
|
+ "ORDER BY created_at DESC LIMIT 1",
|
|
|
+ (tenant_id, project_id),
|
|
|
+ )
|
|
|
+ result["demo"] = {
|
|
|
+ "title": "一汽 DCP 用户手机号字段迁移治理",
|
|
|
+ "question": "为了完成 26→6 工程收敛,dcp_user.mobile 能否迁移并删除?",
|
|
|
+ "stage": "P3 · 双跑验证与退役门禁",
|
|
|
+ "decision": "暂缓删除",
|
|
|
+ "decision_code": "BLOCKED",
|
|
|
+ "risk": "critical",
|
|
|
+ "asset_id": mobile["id"] if mobile else "",
|
|
|
+ "asset_name": mobile["name"] if mobile else "dcp_user.mobile",
|
|
|
+ "proofs": [
|
|
|
+ {"value": "3", "label": "生产消费者仍在读取"},
|
|
|
+ {"value": "99.87%", "label": "双跑完整率,目标 100%"},
|
|
|
+ {"value": "2", "label": "Owner / 安全签字待完成"},
|
|
|
+ {"value": str(count("governance_evidence")), "label": "份可追溯证据"},
|
|
|
+ ],
|
|
|
+ "recommendation": "保留兼容字段 → 补齐双跑差异 → 完成双签 → 下一发布窗口再退役",
|
|
|
+ "latest_run": latest_run,
|
|
|
+ }
|
|
|
+ return result
|
|
|
|
|
|
|
|
|
def export_jsonld(db, tenant_id: str, project_id: str) -> dict:
|
|
|
@@ -583,3 +620,196 @@ def seed_tenant_isolation(db, tenant_id: str) -> dict:
|
|
|
"description": "OntoRefactor 确定性治理门禁"})
|
|
|
validation = validate_project(db, tenant_id, pid, persist=True)
|
|
|
return {"project": project, "overview": overview(db, tenant_id, pid), "validation": validation}
|
|
|
+
|
|
|
+
|
|
|
+def seed_challenge_cup_demo(db, tenant_id: str) -> dict:
|
|
|
+ """Install the polished DCP mobile-field migration demo, idempotently."""
|
|
|
+ project = create_project(db, tenant_id, {
|
|
|
+ "name": "挑战杯 · DCP 手机号迁移治理",
|
|
|
+ "namespace": CHALLENGE_CUP_DEMO_NAMESPACE,
|
|
|
+ "description": "一汽 DCP 26→6 工程收敛:dcp_user.mobile 字段迁移、影响分析与证据门禁黄金演示",
|
|
|
+ "version": "1.0.0",
|
|
|
+ })
|
|
|
+ pid, nodes = project["id"], {}
|
|
|
+
|
|
|
+ def node(key: str, name: str, layer: str, profile: str, kind: str, description: str,
|
|
|
+ attrs: Optional[dict] = None, source: str = "demo:challenge-cup") -> None:
|
|
|
+ nodes[key] = upsert_element(db, tenant_id, pid, {
|
|
|
+ "name": name, "layer": layer, "profile": profile, "kind": kind,
|
|
|
+ "description": description, "attributes": attrs or {}, "status": "published",
|
|
|
+ "uri": f"{CHALLENGE_CUP_DEMO_NAMESPACE}#{key}", "source": source,
|
|
|
+ })
|
|
|
+
|
|
|
+ for key, name in (
|
|
|
+ ("MetaEntityType", "实体类型元类"),
|
|
|
+ ("MetaAssertionType", "治理断言元类"),
|
|
|
+ ("MetaConstraintType", "治理约束元类"),
|
|
|
+ ):
|
|
|
+ node(key, name, "M3", "meta", key, "定义四层本体中可验证对象、断言与约束的稳定元内核")
|
|
|
+
|
|
|
+ m2_types = {
|
|
|
+ "BusinessCapabilityType": ("业务能力类型", "business"),
|
|
|
+ "BusinessObjectType": ("业务对象类型", "business"),
|
|
|
+ "ModuleType": ("软件模块类型", "software"),
|
|
|
+ "ApiOperationType": ("API 操作类型", "software"),
|
|
|
+ "TableType": ("数据表类型", "software"),
|
|
|
+ "ColumnType": ("数据字段类型", "software"),
|
|
|
+ "JobType": ("批处理任务类型", "software"),
|
|
|
+ "OwnerType": ("数据责任人类型", "governance"),
|
|
|
+ "SecurityPolicyType": ("安全策略类型", "governance"),
|
|
|
+ "QualityRuleType": ("质量规则类型", "governance"),
|
|
|
+ "QualityGateType": ("发布门禁类型", "governance"),
|
|
|
+ }
|
|
|
+ for key, (name, profile) in m2_types.items():
|
|
|
+ node(key, name, "M2", profile, "EntityType", f"可跨项目复用的 M2 {name}")
|
|
|
+
|
|
|
+ m1_nodes = (
|
|
|
+ ("UserProfileManagement", "用户画像管理能力", "business", "BusinessCapability", "统一承载用户档案、联系方式与画像查询", {}),
|
|
|
+ ("UserProfile", "用户档案", "business", "BusinessObject", "DCP 身份域中的核心用户业务对象", {"domain": "Base IAM"}),
|
|
|
+ ("BaseIAM", "Base IAM 用户中心", "software", "Module", "26→6 收敛后的身份基础能力模块", {"environment": "production", "target_project": "Base IAM"}),
|
|
|
+ ("QueryUserProfileAPI", "用户画像查询 API", "software", "ApiOperation", "GET /api/v1/users/{id}/profile", {"contract": "v1", "daily_calls": 8600000}),
|
|
|
+ ("SmsNotificationJob", "短信通知任务", "software", "Job", "读取手机号并触发登录、安全与业务通知", {"daily_reads": 3200000}),
|
|
|
+ ("PortraitSyncJob", "用户画像同步任务", "software", "Job", "向画像平台同步用户联系方式", {"daily_reads": 1100000}),
|
|
|
+ ("DcpUserTable", "dcp_user 用户表", "software", "Table", "遗留用户中心的生产主表", {"environment": "production", "schema": "iam"}),
|
|
|
+ ("MobileColumn", "dcp_user.mobile 用户手机号字段", "software", "Column", "拟迁移并删除的明文手机号字段", {"environment": "production", "sensitive": True, "data_type": "VARCHAR(32)"}),
|
|
|
+ ("MobileCipherColumn", "dcp_user.mobile_cipher 加密手机号字段", "software", "Column", "目标加密字段,承接手机号密文", {"environment": "production", "sensitive": True, "data_type": "VARCHAR(256)"}),
|
|
|
+ ("IAMDataOwner", "IAM 数据 Owner", "governance", "OwnerRole", "负责用户域数据定义、质量阈值和退役审批", {"person": "王工", "approval": "pending"}),
|
|
|
+ ("SecurityOfficer", "数据安全负责人", "governance", "OwnerRole", "负责个人信息保护与字段退役安全签字", {"person": "李工", "approval": "pending"}),
|
|
|
+ ("MobilePrivacyPolicy", "手机号个人信息保护策略", "governance", "SecurityPolicy", "手机号按 restricted 管理,存储需加密、使用需最小授权", {"classification": "restricted"}),
|
|
|
+ ("MobileMigrationCompleteness", "手机号迁移完整性规则", "governance", "QualityRule", "明文与密文字段双跑映射完整率必须达到 100%", {"dimension": "completeness", "threshold": 1.0}),
|
|
|
+ ("MobileRetirementGate", "手机号字段退役发布门禁", "governance", "QualityGate", "消费者归零、双跑无差异、Owner 与安全双签后方可删除", {"stage": "P3", "decision": "BLOCKED"}),
|
|
|
+ )
|
|
|
+ for key, name, profile, kind, description, attrs in m1_nodes:
|
|
|
+ node(key, name, "M1", profile, kind, description, attrs)
|
|
|
+
|
|
|
+ m0_nodes = (
|
|
|
+ ("MobilePhysical", "生产库 iam.dcp_user.mobile", "software", "PhysicalColumn", "当前仍被生产流量读取的物理列", {"environment": "production", "sensitive": True}),
|
|
|
+ ("MobileCipherPhysical", "生产库 iam.dcp_user.mobile_cipher", "software", "PhysicalColumn", "迁移目标物理列", {"environment": "production", "sensitive": True}),
|
|
|
+ ("ApiRuntimeTrace", "用户画像 API 生产 Trace", "software", "RuntimeTrace", "过去 24 小时仍读取 mobile", {"calls": 8600000, "window": "24h"}),
|
|
|
+ ("SmsRuntimeTrace", "短信任务生产 Trace", "software", "RuntimeTrace", "过去 24 小时仍读取 mobile", {"reads": 3200000, "window": "24h"}),
|
|
|
+ ("PortraitRuntimeTrace", "画像同步生产 Trace", "software", "RuntimeTrace", "过去 24 小时仍读取 mobile", {"reads": 1100000, "window": "24h"}),
|
|
|
+ ("MigrationMeasurement", "手机号双跑质量报告 2026-08-18", "governance", "Measurement", "迁移完整率 99.87%,低于退役门禁 100%", {"value": 0.9987, "threshold": 1.0, "result": "failed"}),
|
|
|
+ )
|
|
|
+ for key, name, profile, kind, description, attrs in m0_nodes:
|
|
|
+ node(key, name, "M0", profile, kind, description, attrs, source="demo:observed")
|
|
|
+
|
|
|
+ ddl = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "SourceArtifact", "uri": "evidence://challenge-cup/dcp-user-ddl",
|
|
|
+ "title": "dcp_user 生产 DDL", "source": "git://dcp-base-iam/V3_8_0__mobile_cipher.sql",
|
|
|
+ "content": "CREATE TABLE dcp_user (user_id BIGINT PRIMARY KEY, tenant_id BIGINT NOT NULL, mobile VARCHAR(32), mobile_cipher VARCHAR(256), updated_at TIMESTAMP NOT NULL);",
|
|
|
+ "metadata": {"commit": "8f24c91", "repository": "dcp-base-iam"},
|
|
|
+ })
|
|
|
+ contract = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "Contract", "uri": "evidence://challenge-cup/user-profile-openapi",
|
|
|
+ "title": "用户画像 OpenAPI v1 契约", "source": "openapi://base-iam/user-profile-v1",
|
|
|
+ "content": "GET /api/v1/users/{id}/profile -> {userId, mobile, tenantId}; mobile remains required for two release cycles",
|
|
|
+ "metadata": {"consumers": ["portal", "mobile-app", "dealer-cloud"]},
|
|
|
+ })
|
|
|
+ static_scan = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "SourceArtifact", "uri": "evidence://challenge-cup/sql-consumers",
|
|
|
+ "title": "SQL AST 消费者扫描", "source": "scanner://mybatis/commit-8f24c91",
|
|
|
+ "content": "QueryUserProfileMapper, SmsNotificationMapper, PortraitSyncMapper still select dcp_user.mobile",
|
|
|
+ "metadata": {"consumer_count": 3, "confidence": 1.0},
|
|
|
+ })
|
|
|
+ runtime = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "RuntimeObservation", "uri": "evidence://challenge-cup/apm-24h",
|
|
|
+ "title": "生产 APM 24 小时调用观测", "source": "apm://base-iam/2026-08-18",
|
|
|
+ "content": "API 8.6M calls; SMS 3.2M reads; portrait sync 1.1M reads; all observed mobile access",
|
|
|
+ "metadata": {"window": "24h", "consumer_count": 3},
|
|
|
+ })
|
|
|
+ quality = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "QualityReport", "uri": "evidence://challenge-cup/mobile-dual-run",
|
|
|
+ "title": "手机号明文/密文双跑比对", "source": "quality://mobile-migration/run-20260818",
|
|
|
+ "content": "10,189,432 rows checked; 13,246 rows unmatched; completeness=99.87%; gate threshold=100%",
|
|
|
+ "metadata": {"checked_rows": 10189432, "unmatched_rows": 13246, "completeness": 0.9987, "threshold": 1.0},
|
|
|
+ })
|
|
|
+ approval = create_evidence(db, tenant_id, pid, {
|
|
|
+ "kind": "ExpertStatement", "uri": "evidence://challenge-cup/approval-state",
|
|
|
+ "title": "P3 退役评审签字状态", "source": "workflow://adr/mobile-retirement",
|
|
|
+ "content": "Architecture reviewed; IAM Data Owner pending; Security Officer pending",
|
|
|
+ "metadata": {"approved": ["architecture"], "pending": ["data-owner", "security"]},
|
|
|
+ })
|
|
|
+
|
|
|
+ def relation(subject: str, predicate: str, obj: str, assertion_type: str = "declared",
|
|
|
+ evidence_ids: Optional[list[str]] = None, confidence: float = 1.0) -> None:
|
|
|
+ create_assertion(db, tenant_id, pid, {
|
|
|
+ "subject_id": nodes[subject]["id"], "predicate": predicate, "object_id": nodes[obj]["id"],
|
|
|
+ "assertion_type": assertion_type, "confidence": confidence,
|
|
|
+ "evidence_ids": evidence_ids or [], "generated_by": "demo:challenge-cup-seed",
|
|
|
+ "review_status": "accepted",
|
|
|
+ })
|
|
|
+
|
|
|
+ for key in m2_types:
|
|
|
+ relation(key, "INSTANCE_OF", "MetaEntityType")
|
|
|
+ type_map = {
|
|
|
+ "UserProfileManagement": "BusinessCapabilityType", "UserProfile": "BusinessObjectType",
|
|
|
+ "BaseIAM": "ModuleType", "QueryUserProfileAPI": "ApiOperationType",
|
|
|
+ "SmsNotificationJob": "JobType", "PortraitSyncJob": "JobType", "DcpUserTable": "TableType",
|
|
|
+ "MobileColumn": "ColumnType", "MobileCipherColumn": "ColumnType", "IAMDataOwner": "OwnerType",
|
|
|
+ "SecurityOfficer": "OwnerType", "MobilePrivacyPolicy": "SecurityPolicyType",
|
|
|
+ "MobileMigrationCompleteness": "QualityRuleType", "MobileRetirementGate": "QualityGateType",
|
|
|
+ }
|
|
|
+ for child, parent in type_map.items():
|
|
|
+ relation(child, "INSTANCE_OF", parent)
|
|
|
+ relation("MobilePhysical", "INSTANCE_OF", "MobileColumn", "observed", [runtime["id"]], .99)
|
|
|
+ relation("MobileCipherPhysical", "INSTANCE_OF", "MobileCipherColumn", "observed", [quality["id"]], .99)
|
|
|
+ relation("ApiRuntimeTrace", "INSTANCE_OF", "QueryUserProfileAPI", "observed", [runtime["id"]], .99)
|
|
|
+ relation("SmsRuntimeTrace", "INSTANCE_OF", "SmsNotificationJob", "observed", [runtime["id"]], .99)
|
|
|
+ relation("PortraitRuntimeTrace", "INSTANCE_OF", "PortraitSyncJob", "observed", [runtime["id"]], .99)
|
|
|
+ relation("MigrationMeasurement", "INSTANCE_OF", "MobileMigrationCompleteness", "observed", [quality["id"]], .99)
|
|
|
+
|
|
|
+ relation("UserProfileManagement", "REALIZED_BY", "BaseIAM")
|
|
|
+ relation("DcpUserTable", "REPRESENTS", "UserProfile")
|
|
|
+ relation("BaseIAM", "EXPOSES", "QueryUserProfileAPI", "extracted", [contract["id"]], .98)
|
|
|
+ relation("MobileColumn", "BELONGS_TO", "DcpUserTable", "extracted", [ddl["id"]], 1)
|
|
|
+ relation("MobileCipherColumn", "BELONGS_TO", "DcpUserTable", "extracted", [ddl["id"]], 1)
|
|
|
+ relation("MobileColumn", "MIRRORS_TO", "MobileCipherColumn", "observed", [quality["id"]], .9987)
|
|
|
+ for consumer in ("QueryUserProfileAPI", "SmsNotificationJob", "PortraitSyncJob"):
|
|
|
+ relation(consumer, "READS", "MobileColumn", "observed", [static_scan["id"], runtime["id"]], .99)
|
|
|
+ for asset in ("BaseIAM", "DcpUserTable", "MobileColumn", "MobileCipherColumn", "MobilePhysical", "MobileCipherPhysical"):
|
|
|
+ relation(asset, "OWNED_BY", "IAMDataOwner", "declared", [approval["id"]])
|
|
|
+ for asset in ("MobileColumn", "MobileCipherColumn", "MobilePhysical", "MobileCipherPhysical"):
|
|
|
+ relation(asset, "GOVERNED_BY", "MobilePrivacyPolicy", "declared", [approval["id"]])
|
|
|
+ relation("MobilePrivacyPolicy", "OWNED_BY", "SecurityOfficer", "declared", [approval["id"]])
|
|
|
+ relation("MobileMigrationCompleteness", "VALIDATES", "MobileColumn")
|
|
|
+ relation("MobileMigrationCompleteness", "VALIDATES", "MobileCipherColumn")
|
|
|
+ relation("MigrationMeasurement", "VALIDATES", "MobilePhysical", "observed", [quality["id"]], .9987)
|
|
|
+ relation("MobileRetirementGate", "EVALUATES", "MobileColumn", "declared", [approval["id"]])
|
|
|
+ relation("MobileRetirementGate", "REQUIRES_APPROVAL_FROM", "IAMDataOwner", "declared", [approval["id"]])
|
|
|
+ relation("MobileRetirementGate", "REQUIRES_APPROVAL_FROM", "SecurityOfficer", "declared", [approval["id"]])
|
|
|
+
|
|
|
+ for name, rule_type, severity, expression in (
|
|
|
+ ("生产消费者必须归零", "retirement", "critical", {"asset": "dcp_user.mobile", "consumerCount": 0}),
|
|
|
+ ("手机号双跑完整率", "quality", "critical", {"dimension": "completeness", "threshold": 1.0}),
|
|
|
+ ("敏感字段必须加密", "security", "critical", {"classification": "restricted", "encrypted": True}),
|
|
|
+ ("退役必须完成双签", "approval", "high", {"required": ["data-owner", "security"]}),
|
|
|
+ ("旧契约兼容两个周期", "compatibility", "high", {"minimumReleaseCycles": 2}),
|
|
|
+ ):
|
|
|
+ upsert_rule(db, tenant_id, pid, {
|
|
|
+ "name": name, "rule_type": rule_type, "severity": severity, "expression": expression,
|
|
|
+ "description": "DCP 手机号迁移的确定性发布门禁",
|
|
|
+ })
|
|
|
+
|
|
|
+ validation = validate_project(db, tenant_id, pid, persist=True)
|
|
|
+ demo_issues = (
|
|
|
+ ("demo:GATE_MOBILE_DELETE", "critical", "字段删除门禁:BLOCKED", "3 个生产消费者仍在读取 dcp_user.mobile;当前禁止执行破坏性 DDL。", [static_scan["id"], runtime["id"]], "IAM Data Owner"),
|
|
|
+ ("demo:MIGRATION_COMPLETENESS", "high", "双跑完整率未达到 100%", "10,189,432 行中仍有 13,246 行未匹配,当前完整率 99.87%。", [quality["id"]], "数据质量负责人"),
|
|
|
+ ("demo:ACTIVE_CONSUMERS", "high", "仍存在 3 个生产消费者", "用户画像 API、短信通知任务、画像同步任务仍直接读取旧字段。", [contract["id"], runtime["id"]], "Base IAM 架构组"),
|
|
|
+ ("demo:APPROVAL_PENDING", "medium", "Owner 与安全签字待完成", "架构评审已完成;IAM 数据 Owner 与数据安全负责人尚未签字。", [approval["id"]], "治理评审委员会"),
|
|
|
+ )
|
|
|
+ now = now_utc()
|
|
|
+ for rule_id, severity, title, description, evidence_ids, assigned_to in demo_issues:
|
|
|
+ existing = db.fetchone(
|
|
|
+ "SELECT id FROM governance_issues WHERE tenant_id=? AND project_id=? AND rule_id=?",
|
|
|
+ (tenant_id, pid, rule_id),
|
|
|
+ )
|
|
|
+ if not existing:
|
|
|
+ db.execute(
|
|
|
+ "INSERT INTO governance_issues "
|
|
|
+ "(id,tenant_id,project_id,rule_id,target_id,severity,title,description,status,evidence_ids,assigned_to,created_at,updated_at) "
|
|
|
+ "VALUES (?,?,?,?,?,?,?,?,'open',?,?,?,?)",
|
|
|
+ (gen_id("gi_"), tenant_id, pid, rule_id, nodes["MobileColumn"]["id"], severity,
|
|
|
+ title, description, _dump(evidence_ids), assigned_to, now, now),
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+ return {"project": project, "overview": overview(db, tenant_id, pid), "validation": validation}
|