Преглед изворни кода

feat: add polished challenge cup demo

caozheng пре 2 недеља
родитељ
комит
16d100d7fe

+ 2 - 0
README.md

@@ -72,6 +72,8 @@ LongCat 官方文档:[LongCat API 开放平台](https://longcat.chat/platform/
 
 ## 如何使用
 
+挑战杯现场演示可直接打开 <http://127.0.0.1:8010/?demo=1>,点击“一键准备挑战杯 Demo”,按首页六步演示路径操作。完整讲稿、时间分配和断网兜底见 [`docs/demo-runbook.md`](docs/demo-runbook.md)。
+
 - 治理总览:观察模型数量、开放问题、待审断言和 M3–M0 分布。
 - 本体模型:检索业务、软件、数据和治理元素,点击对象执行关系影响分析。
 - 语义断言:接受或驳回智能体推断出的候选关系。

+ 84 - 0
docs/demo-runbook.md

@@ -0,0 +1,84 @@
+# 挑战杯黄金 Demo 演示手册
+
+## 一句话主线
+
+> 为了完成一汽 DCP 26→6 工程收敛,`dcp_user.mobile` 能不能迁移并删除?
+
+不要把演示讲成“平台有六个菜单”。全程只回答这一个问题:系统如何从代码、契约和运行事实出发,经多智能体与本体推理,形成可追溯、可审批的工程决策。
+
+## 演示前 15 分钟
+
+1. 在项目目录启动服务:
+
+   ```powershell
+   .\.venv\Scripts\ontorefactor-governance.exe --host 127.0.0.1 --port 8010
+   ```
+
+2. 打开演示直达地址:<http://127.0.0.1:8010/?demo=1>。
+3. 确认项目为“挑战杯 · DCP 手机号迁移治理”。若不存在,点击“一键准备挑战杯 Demo”。
+4. 进入“智能体工作台”,点击“测试模型连接”。连接成功即可,不要在正式演示时重新跑完整模型。
+5. 浏览器设为 90% 或 100% 缩放,开启全屏;关闭消息通知和无关窗口。
+6. 保留 `data/demo-overview.png` 作为断网时的首页备份图。
+
+当前机器已经预热了一次真实 LongCat-2.0 运行。运行记录包含模型、Prompt 版本、输入/输出 Token、耗时与请求/响应哈希,现场可以直接打开审计轨迹。
+
+## 7 分钟演示脚本
+
+| 时间 | 页面与操作 | 建议讲法 | 屏幕证据 |
+|---|---|---|---|
+| 0:00–0:45 | 治理总览 | “企业重构真正危险的不是改代码,而是不知道一个字段还被谁使用。我们用一个真实工程问题演示:手机号字段能否删除?” | 首页问题卡与 `BLOCKED` 门禁 |
+| 0:45–1:30 | 点击步骤 01“输入证据” | “系统不是让大模型凭空回答,而是先接入 DDL、OpenAPI、SQL AST、生产 Trace、质量报告和专家签字。” | 8 份证据及内容哈希 |
+| 1:30–2:30 | 点击步骤 02“智能体协作” | “资产发现和门禁检查是确定性程序;LongCat 只负责业务语义、分类、Owner 和规则候选。所有步骤在同一个 LambdAgent 运行中可审计。” | 三个 Agent、LongCat-2.0、2752 Token、Prompt v2 |
+| 2:30–3:45 | 返回总览,点击“查看字段级影响半径” | “传统搜索只能找到代码引用;本体图能从字段跨到 API、任务、业务对象、Owner、安全策略和质量门禁。” | 31 个影响对象、56 条已接受关系、三跳影响列 |
+| 3:45–4:45 | 点击步骤 04“候选断言” | “模型结论不是事实。13 条建议都有置信度,全部强制为 pending;只有人工接受后才能进入权威图谱。” | pending 状态、接受/驳回按钮、最高置信度上限 85% |
+| 4:45–5:45 | 点击步骤 05“治理门禁” | “门禁不采用模型的主观判断,而是执行确定性规则:3 个生产消费者未归零、完整率只有 99.87%、两项签字未完成,因此禁止破坏性 DDL。” | 4 条核心开放问题与负责人 |
+| 5:45–6:30 | 点击步骤 06“标准交付” | “业务、软件、治理语义被放进 M3–M0 四层模型,并可导出 JSON-LD;企业换数据源或换模型,不需要推倒治理语言。” | 四层元素、Profile、JSON-LD 导出 |
+| 6:30–7:00 | 回到总览收束 | “AI 的价值不是替专家拍板,而是把几天的证据搜集和影响评审压缩成分钟级、可复核的治理闭环。” | `暂缓删除` 与整改路径 |
+
+## 开场和收尾原话
+
+开场:
+
+> 在大型工业软件重构中,最难的不是生成一段新代码,而是回答“改了以后会不会出事故”。今天我们只演示一个问题:为了把 DCP 的 26 个工程收敛为 6 个工程,用户手机号字段能不能迁移并删除?
+
+收尾:
+
+> 系统没有替企业负责人做决定。它把代码、契约、运行数据和专家责任统一成治理本体,让智能体快速提出候选,让确定性门禁守住生产安全,让每个结论都能回到证据。这才是面向东北工业数字化的可落地智能体。
+
+## 评委常问问题
+
+### 为什么有多个智能体?
+
+它们不是多个聊天机器人,也不是多个独立微服务,而是一条进程内 LambdAgent 流水线的专业职责:资产发现、LongCat 语义分析、治理控制和结果合成。拆分职责是为了单独验证、替换和审计。
+
+### 大模型会不会直接修改生产系统?
+
+不会。模型只能生成白名单关系的 M1 候选,置信度最高 0.85,状态强制为 `pending`;删除、授权、发布等高风险动作由确定性门禁和人工审批控制。
+
+### 怎么证明真的调用了大模型?
+
+打开“智能体工作台”的运行轨迹。系统记录 LongCat-2.0、Prompt v2、输入/输出 Token、模型耗时、请求哈希和响应哈希,同时保存 `ModelInference` 证据。
+
+### 本体不是概念图吗,为什么有工程价值?
+
+这里的关系是可计算断言,携带证据、置信度、生成者、有效期和审核状态。字段变化可以沿图计算到 API、任务、业务对象、责任人和门禁,而不只是画一张静态图。
+
+### 断网或模型超时怎么办?
+
+确定性资产发现、影响分析、规则门禁和已落库审计记录全部可离线运行。现场展示使用预热的真实 LongCat 记录;需要证明连通性时只点击“测试模型连接”,不等待完整重跑。
+
+## 现场不要做
+
+- 不要从空白项目开始手工录入数据。
+- 不要在 7 分钟演示中等待完整 LongCat 重跑;模型服务排队可能需要 30–90 秒。
+- 不要点击“重新校验”,它会根据当前候选模型重新生成辅助问题,改变首页基准数字。
+- 不要现场接受或驳回断言,除非已经准备好重新初始化演示数据库。
+- 不要展示或朗读 API Key。
+
+## 故障兜底
+
+- 页面项目不对:打开 <http://127.0.0.1:8010/?demo=1>。
+- Demo 数据不存在:点击“一键准备挑战杯 Demo”,该操作幂等。
+- LongCat 不可用:展示已完成的运行轨迹和 `ModelInference` 证据,并说明确定性门禁不依赖在线模型。
+- 服务未启动:在项目目录执行启动命令后刷新页面。
+- 网络完全不可用:使用 `data/demo-overview.png` 讲完问题、证据数字、门禁结论和整改路径。

+ 5 - 0
src/ontorefactor_governance/api.py

@@ -165,6 +165,11 @@ def bootstrap(tenant: str = Depends(scope)):
     return call(service.seed_tenant_isolation, get_database(), tenant)
 
 
+@app.post("/api/v1/governance/bootstrap/challenge-cup")
+def bootstrap_challenge_cup(tenant: str = Depends(scope)):
+    return call(service.seed_challenge_cup_demo, get_database(), tenant)
+
+
 @app.get("/api/v1/governance/projects/{project_id}/overview")
 def project_overview(project_id: str, tenant: str = Depends(scope)):
     return call(service.overview, get_database(), tenant, project_id)

+ 235 - 5
src/ontorefactor_governance/service.py

@@ -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}

+ 112 - 22
src/ontorefactor_governance/static/app.js

@@ -1,4 +1,13 @@
 const API = "/api/v1/governance";
+const DEMO_NAMESPACE = "https://onto.qm.cn/ontorefactor/demo/dcp-mobile/1.0";
+const DEMO_DDL = `CREATE TABLE dcp_user (
+  user_id BIGINT PRIMARY KEY,
+  tenant_id BIGINT NOT NULL,
+  mobile VARCHAR(32),
+  mobile_cipher VARCHAR(256),
+  full_name VARCHAR(128),
+  updated_at TIMESTAMP NOT NULL
+);`;
 const state = {
   tenant: localStorage.getItem("or-tenant") || "local",
   apiKey: localStorage.getItem("or-api-key") || "",
@@ -13,6 +22,13 @@ const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
 const esc = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[char]));
 const date = (value) => value ? new Intl.DateTimeFormat("zh-CN", {dateStyle:"medium", timeStyle:"short"}).format(new Date(value)) : "—";
 const badge = (value) => `<span class="badge ${esc(String(value).toLowerCase())}">${esc(value)}</span>`;
+const termLabel = (value) => ({
+  "source-normalizer": "输入规范化",
+  "asset-discovery-agent": "资产发现智能体",
+  "longcat-governance-semantics": "LongCat 业务语义智能体",
+  "governance-controls-agent": "治理控制智能体",
+  "governance-synthesis-agent": "结果合成与门禁",
+}[value] || value || "执行步骤");
 
 async function request(path, options = {}) {
   const headers = {"X-Tenant-ID": state.tenant, ...(options.headers || {})};
@@ -37,6 +53,7 @@ function toast(message, error = false) {
 }
 
 function currentProject() { return state.projects.find((item) => item.id === state.projectId); }
+function isDemoProject() { return currentProject()?.namespace === DEMO_NAMESPACE; }
 
 async function loadProjects(preferredId) {
   state.projects = (await request("/projects")).projects;
@@ -55,13 +72,13 @@ async function loadProjects(preferredId) {
 }
 
 async function bootstrap() {
-  setBusy($("#bootstrap-button"), true, "创建中…");
+  setBusy($("#bootstrap-button"), true, "正在准备黄金 Demo…");
   try {
-    const result = await request("/bootstrap/tenant-isolation", {method: "POST"});
+    const result = await request("/bootstrap/challenge-cup", {method: "POST"});
     await loadProjects(result.project.id);
-    toast(`已准备演示项目:${result.project.name}`);
+    toast("挑战杯黄金 Demo 已就绪:从治理总览开始演示");
   } catch (error) { toast(error.message, true); }
-  finally { setBusy($("#bootstrap-button"), false, "创建演示项目"); }
+  finally { setBusy($("#bootstrap-button"), false, "一键准备挑战杯 Demo"); }
 }
 
 async function createProject(event) {
@@ -130,7 +147,41 @@ async function renderOverview() {
   const layers = data.layers || {};
   const max = Math.max(1, ...Object.values(layers));
   const issues = data.recent_issues || [];
+  const demo = data.demo;
+  const demoSpotlight = demo ? `
+    <section class="demo-spotlight">
+      <div class="demo-copy">
+        <div class="demo-kicker"><span>挑战杯黄金演示</span>${esc(demo.stage)}</div>
+        <h2>${esc(demo.question)}</h2>
+        <p>系统把 DDL、OpenAPI、SQL 静态扫描、生产 Trace、质量报告和专家签字统一为可计算的治理本体,再由智能体给出有证据的决策。</p>
+        <div class="demo-actions">
+          <button class="button demo-primary" data-demo-action="impact" data-demo-asset="${esc(demo.asset_id)}">查看字段级影响半径</button>
+          <button class="button demo-secondary" data-demo-action="agent">查看真实智能体轨迹</button>
+        </div>
+      </div>
+      <aside class="gate-card ${esc(demo.risk)}">
+        <div class="gate-label"><span class="pulse"></span>确定性发布门禁</div>
+        <strong>${esc(demo.decision)}</strong>
+        <span class="gate-code">${esc(demo.decision_code)}</span>
+        <p>${esc(demo.recommendation)}</p>
+      </aside>
+    </section>
+    <section class="demo-proof-grid">
+      ${demo.proofs.map((item) => `<article><strong>${esc(item.value)}</strong><span>${esc(item.label)}</span></article>`).join("")}
+    </section>
+    <section class="demo-tour panel">
+      <div class="demo-tour-head"><div><span>现场演示路径</span><strong>从证据到决策,六步讲清系统价值</strong></div>${demo.latest_run ? `<em>LongCat-2.0 真实运行已审计 · ${Number(demo.latest_run.input_tokens || 0) + Number(demo.latest_run.output_tokens || 0)} tokens</em>` : ""}</div>
+      <div class="demo-steps">
+        <button data-demo-action="evidence"><b>01</b><span>输入证据<small>代码、契约与运行事实</small></span></button>
+        <button data-demo-action="agent"><b>02</b><span>智能体协作<small>确定性抽取 + LongCat</small></span></button>
+        <button data-demo-action="impact" data-demo-asset="${esc(demo.asset_id)}"><b>03</b><span>影响分析<small>字段到业务与责任人</small></span></button>
+        <button data-demo-action="assertions"><b>04</b><span>候选断言<small>置信度与人工复核</small></span></button>
+        <button data-demo-action="issues"><b>05</b><span>治理门禁<small>规则阻断破坏性变更</small></span></button>
+        <button data-demo-action="model"><b>06</b><span>标准交付<small>四层本体与 JSON-LD</small></span></button>
+      </div>
+    </section>` : "";
   $("#content").innerHTML = `
+    ${demoSpotlight}
     <div class="metric-grid">${metrics.map(([label, value, note]) => `<article class="metric panel"><div class="metric-label">${label}</div><div class="metric-value">${value}</div><div class="metric-note">${note}</div></article>`).join("")}</div>
     <div class="dashboard-grid">
       <article class="section-card panel">
@@ -176,8 +227,15 @@ async function renderModel() {
 async function showImpact(id) {
   try {
     const result = await request(`/elements/${id}/impact?max_depth=3`);
+    const affected = (result.nodes || []).filter(x => x.id !== id);
+    const groups = [1, 2, 3].map(distance => ({distance, items: affected.filter(x => x.distance === distance)}));
+    const governanceCount = affected.filter(x => x.profile === "governance").length;
     $("#detail-title").textContent = `影响分析 · ${result.center.name}`;
-    $("#detail-content").innerHTML = `<p class="muted">沿已接受的本体关系向外分析三层,识别变更可能影响的对象。</p><div class="table-wrap"><table><thead><tr><th>距离</th><th>对象</th><th>类型</th><th>层级</th></tr></thead><tbody>${(result.nodes || []).filter(x => x.id !== id).map(x => `<tr><td>${x.distance}</td><td>${esc(x.name)}</td><td>${esc(x.kind)}</td><td>${badge(x.layer)}</td></tr>`).join("") || '<tr><td colspan="4" class="inline-empty">未发现关联影响</td></tr>'}</tbody></table></div>`;
+    $("#detail-content").innerHTML = `
+      <div class="impact-summary"><div><span>影响对象</span><strong>${affected.length}</strong></div><div><span>已验证关系</span><strong>${result.edges?.length || 0}</strong></div><div><span>治理控制</span><strong>${governanceCount}</strong></div><p>沿已接受且有证据的本体关系向外计算三层,不使用大模型猜测影响范围。</p></div>
+      <div class="impact-origin"><span>变更中心</span><strong>${esc(result.center.name)}</strong><small>${esc(result.center.kind)} · ${esc(result.center.layer)} / ${esc(result.center.profile)}</small></div>
+      <div class="impact-columns">${groups.map(group => `<section><h3>${group.distance} 跳影响</h3>${group.items.length ? group.items.map(item => `<article class="impact-node ${esc(item.profile)}"><span>${badge(item.layer)} ${esc(item.kind)}</span><strong>${esc(item.name)}</strong></article>`).join("") : '<p class="muted">无更多对象</p>'}</section>`).join("")}</div>
+      <div class="impact-relations"><h3>关键证据关系</h3>${(result.edges || []).slice(0, 16).map(edge => `<span>${esc(edge.subject_name || "对象")} <b>${esc(edge.predicate)}</b> ${esc(edge.object_name || "对象")}</span>`).join("")}</div>`;
     $("#detail-dialog").showModal();
   } catch (error) { toast(error.message, true); }
 }
@@ -194,10 +252,10 @@ async function downloadJsonLd() {
 
 async function renderAssertions() {
   const rows = (await request(`/projects/${state.projectId}/assertions`)).assertions;
-  $("#content").innerHTML = `<div class="toolbar"><select id="assertion-status"><option value="">全部状态</option><option>pending</option><option>accepted</option><option>rejected</option></select><span class="spacer"></span><span class="muted">共 ${rows.length} 条语义关系</span></div><div class="table-wrap"><table><thead><tr><th>主语</th><th>谓词</th><th>宾语</th><th>状态</th><th>置信度</th><th>操作</th></tr></thead><tbody id="assertion-rows"></tbody></table></div>`;
+  $("#content").innerHTML = `<div class="toolbar"><select id="assertion-status"><option value="">全部状态</option><option ${isDemoProject() ? "selected" : ""}>pending</option><option>accepted</option><option>rejected</option></select><span class="spacer"></span><span class="muted">共 ${rows.length} 条语义关系 · 模型结论必须人工确认后才进入权威图谱</span></div><div class="table-wrap"><table><thead><tr><th>主语</th><th>谓词</th><th>宾语</th><th>生成者</th><th>状态</th><th>置信度</th><th>操作</th></tr></thead><tbody id="assertion-rows"></tbody></table></div>`;
   const draw = () => {
     const status = $("#assertion-status").value;
-    $("#assertion-rows").innerHTML = rows.filter(x => !status || x.review_status === status).map((x) => `<tr><td class="wrap">${esc(x.subject_name || x.subject_id)}</td><td><span class="code">${esc(x.predicate)}</span></td><td class="wrap">${esc(x.object_name || x.object_value || x.object_id)}</td><td>${badge(x.review_status)}</td><td>${Math.round((x.confidence || 0) * 100)}%</td><td>${x.review_status === "pending" ? `<button class="button small secondary" data-review="${esc(x.id)}" data-decision="accepted">接受</button> <button class="button small ghost" data-review="${esc(x.id)}" data-decision="rejected">驳回</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配断言</td></tr>';
+    $("#assertion-rows").innerHTML = rows.filter(x => !status || x.review_status === status).map((x) => `<tr><td class="wrap">${esc(x.subject_name || x.subject_id)}</td><td><span class="code">${esc(x.predicate)}</span></td><td class="wrap">${esc(x.object_name || x.object_value || x.object_id)}</td><td class="wrap"><small class="code">${esc(x.generated_by || "人工声明")}</small></td><td>${badge(x.review_status)}</td><td>${Math.round((x.confidence || 0) * 100)}%</td><td>${x.review_status === "pending" ? `<button class="button small secondary" data-review="${esc(x.id)}" data-decision="accepted">接受</button> <button class="button small ghost" data-review="${esc(x.id)}" data-decision="rejected">驳回</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="7" class="inline-empty">没有匹配断言</td></tr>';
   };
   $("#assertion-status").addEventListener("change", draw); draw();
 }
@@ -211,15 +269,26 @@ async function reviewAssertion(id, decision) {
 
 async function renderEvidence() {
   const rows = (await request(`/projects/${state.projectId}/evidence`)).evidence;
-  $("#content").innerHTML = `<div class="section-head"><div><h2>证据中心</h2><p>每条自动推断都可以追溯到原始输入和处理过程</p></div></div><div class="table-wrap"><table><thead><tr><th>名称</th><th>类型</th><th>来源</th><th>校验值</th><th>创建时间</th></tr></thead><tbody>${rows.map((x) => `<tr><td>${esc(x.title)}</td><td>${badge(x.kind)}</td><td class="wrap">${esc(x.source || x.uri || "—")}</td><td class="code">${esc((x.content_hash || "").slice(0, 16))}${x.content_hash ? "…" : ""}</td><td>${date(x.created_at)}</td></tr>`).join("") || '<tr><td colspan="5" class="inline-empty">暂无证据</td></tr>'}</tbody></table></div>`;
+  $("#content").innerHTML = `<div class="section-head"><div><h2>证据中心</h2><p>每条自动推断都可以追溯到原始输入和处理过程;点击名称查看证据原文</p></div></div><div class="table-wrap"><table><thead><tr><th>名称</th><th>类型</th><th>来源</th><th>校验值</th><th>创建时间</th></tr></thead><tbody>${rows.map((x) => `<tr><td><button class="link-button" data-evidence="${esc(x.id)}">${esc(x.title)}</button></td><td>${badge(x.kind)}</td><td class="wrap">${esc(x.source || x.uri || "—")}</td><td class="code">${esc((x.content_hash || "").slice(0, 16))}${x.content_hash ? "…" : ""}</td><td>${date(x.created_at)}</td></tr>`).join("") || '<tr><td colspan="5" class="inline-empty">暂无证据</td></tr>'}</tbody></table></div>`;
+}
+
+async function showEvidence(id) {
+  try {
+    const rows = (await request(`/projects/${state.projectId}/evidence`)).evidence;
+    const evidence = rows.find(item => item.id === id);
+    if (!evidence) return;
+    $("#detail-title").textContent = `证据详情 · ${evidence.title}`;
+    $("#detail-content").innerHTML = `<dl class="definition-list"><dt>证据类型</dt><dd>${badge(evidence.kind)}</dd><dt>来源</dt><dd class="code">${esc(evidence.source || evidence.uri || "—")}</dd><dt>内容哈希</dt><dd class="code">${esc(evidence.content_hash)}</dd><dt>观测时间</dt><dd>${date(evidence.observed_at)}</dd><dt>元数据</dt><dd class="code">${esc(JSON.stringify(evidence.metadata || {}))}</dd></dl><h3>证据原文</h3><div class="result-box evidence-content">${esc(evidence.content || "无正文")}</div>`;
+    $("#detail-dialog").showModal();
+  } catch (error) { toast(error.message, true); }
 }
 
 async function renderIssues() {
   const rows = (await request(`/projects/${state.projectId}/issues?status=`)).issues;
-  $("#content").innerHTML = `<div class="toolbar"><select id="issue-status"><option value="">全部状态</option><option value="open">开放</option><option value="acknowledged">已确认</option><option value="resolved">已解决</option><option value="waived">已豁免</option></select><span class="spacer"></span><button class="button secondary" data-action="validate">重新校验</button></div><div class="table-wrap"><table><thead><tr><th>严重度</th><th>问题</th><th>规则</th><th>状态</th><th>发现时间</th><th>操作</th></tr></thead><tbody id="issue-rows"></tbody></table></div>`;
+  $("#content").innerHTML = `<div class="toolbar"><select id="issue-status"><option value="">全部状态</option><option value="open" ${isDemoProject() ? "selected" : ""}>开放</option><option value="acknowledged">已确认</option><option value="resolved">已解决</option><option value="waived">已豁免</option></select><span class="spacer"></span><button class="button secondary" data-action="validate">重新校验</button></div><div class="table-wrap"><table><thead><tr><th>严重度</th><th>问题</th><th>规则</th><th>状态</th><th>发现时间</th><th>操作</th></tr></thead><tbody id="issue-rows"></tbody></table></div>`;
   const draw = () => {
     const status = $("#issue-status").value;
-    $("#issue-rows").innerHTML = rows.filter(x => !status || x.status === status).map((x) => `<tr><td>${badge(x.severity)}</td><td class="wrap"><strong>${esc(x.title)}</strong><br><small class="muted">${esc(x.description || "")}</small></td><td class="code">${esc(x.rule_code || "—")}</td><td>${badge(x.status)}</td><td>${date(x.created_at)}</td><td>${x.status === "open" ? `<button class="button small secondary" data-resolve="${esc(x.id)}">标记解决</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配问题</td></tr>';
+    $("#issue-rows").innerHTML = rows.filter(x => !status || x.status === status).map((x) => `<tr><td>${badge(x.severity)}</td><td class="wrap"><strong>${esc(x.title)}</strong><br><small class="muted">${esc(x.description || "")}</small></td><td class="code">${esc(x.rule_code || x.rule_id || "—")}</td><td>${badge(x.status)}</td><td>${date(x.created_at)}</td><td>${x.status === "open" ? `<button class="button small secondary" data-resolve="${esc(x.id)}">标记解决</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配问题</td></tr>';
   };
   $("#issue-status").addEventListener("change", draw); draw();
 }
@@ -237,22 +306,27 @@ async function renderAgent() {
     request("/llm/status"),
   ]);
   const runs = runResult.runs;
+  const demoMode = isDemoProject();
+  const sourceName = demoMode ? "V3_8_0__mobile_cipher.sql" : "console-import";
+  const sourceContent = demoMode ? DEMO_DDL : `CREATE TABLE customer_order (
+  order_id INTEGER PRIMARY KEY,
+  customer_id INTEGER NOT NULL,
+  total_amount DECIMAL(12,2),
+  created_at TIMESTAMP
+);`;
+  const instruction = demoMode ? "判断 dcp_user.mobile 是否可以迁移并删除;识别业务对象、敏感等级、Owner、质量规则和治理风险。" : "发现数据资产,推断业务语义并执行治理检查";
   $("#content").innerHTML = `<div class="agent-grid">
-    <article class="section-card panel"><div class="section-head"><div><h2>资产发现智能体</h2><p>由 LambdAgent 流水线完成规范化、并行分析、合成与治理落库</p></div></div>
+    <article class="section-card panel"><div class="section-head"><div><h2>治理多智能体工作台</h2><p>由 LambdAgent 完成规范化、并行分析、LongCat 语义推理、门禁校验与证据落库</p></div></div>
       <div class="model-strip ${llm.configured ? "ready" : "offline"}">
         <div><span class="dot"></span><strong>${esc(llm.model)}</strong><small>${llm.configured ? "LongCat 大模型已配置" : "未配置密钥,将使用确定性语义分析"}</small></div>
         <button id="test-llm" class="button small ghost" ${llm.configured ? "" : "disabled"}>测试模型连接</button>
       </div>
+      ${demoMode && runs.some(run => run.status === "completed" && run.llm_model) ? '<div class="demo-ready"><strong>✓ 真实模型结果已预热</strong><span>现场可直接查看右侧审计轨迹;需要时也可以再次实时运行。</span></div>' : ""}
       <form id="agent-form" class="form-stack">
-        <div class="form-pair"><label>输入格式<select id="source-type"><option value="ddl">SQL DDL</option><option value="openapi">OpenAPI JSON/YAML</option><option value="inventory">资产清单 JSON/YAML</option></select></label><label>语义分析方式<select id="semantic-mode"><option value="auto">自动选择(推荐)</option><option value="llm">强制 LongCat-2.0</option><option value="deterministic">仅确定性规则</option></select></label></div>
-        <label>来源名称<input id="source-name" value="console-import" required></label>
-        <label>原始内容<textarea id="source-content" spellcheck="false">CREATE TABLE customer_order (
-  order_id INTEGER PRIMARY KEY,
-  customer_id INTEGER NOT NULL,
-  total_amount DECIMAL(12,2),
-  created_at TIMESTAMP
-);</textarea></label>
-        <label>运行说明<input id="agent-instruction" value="发现数据资产,推断业务语义并执行治理检查"></label>
+        <div class="form-pair"><label>输入格式<select id="source-type"><option value="ddl">SQL DDL</option><option value="openapi">OpenAPI JSON/YAML</option><option value="inventory">资产清单 JSON/YAML</option></select></label><label>语义分析方式<select id="semantic-mode"><option value="auto">自动选择(推荐)</option><option value="llm" ${demoMode ? "selected" : ""}>强制 LongCat-2.0</option><option value="deterministic">仅确定性规则</option></select></label></div>
+        <label>来源名称<input id="source-name" value="${esc(sourceName)}" required></label>
+        <label>原始内容<textarea id="source-content" spellcheck="false">${esc(sourceContent)}</textarea></label>
+        <label>运行说明<input id="agent-instruction" value="${esc(instruction)}"></label>
         <div class="button-row"><button id="run-agent" class="button primary" type="submit">运行智能体</button><span class="muted">自动结论以 pending 状态等待人工确认</span></div>
       </form>
       <div id="agent-result"></div>
@@ -292,7 +366,7 @@ async function showRun(id) {
     $("#detail-title").textContent = "智能体执行轨迹";
     const trace = run.trace_json || [];
     const result = run.result_json || {};
-    $("#detail-content").innerHTML = `<dl class="definition-list"><dt>运行标识</dt><dd class="code">${esc(run.id)}</dd><dt>状态</dt><dd>${badge(run.status)}</dd><dt>来源类型</dt><dd>${esc(run.source_type || "—")}</dd><dt>语义方式</dt><dd>${esc(run.semantic_mode || "deterministic")}</dd><dt>大模型</dt><dd>${esc(run.llm_model || "未使用")}</dd><dt>Token</dt><dd>${Number(run.input_tokens || 0)} 输入 / ${Number(run.output_tokens || 0)} 输出</dd><dt>模型耗时</dt><dd>${Number(run.llm_latency_ms || 0)} ms</dd><dt>Prompt 版本</dt><dd class="code">${esc(run.prompt_version || "—")}</dd><dt>落库结果</dt><dd>${esc(JSON.stringify(result.persisted || {}))}</dd></dl><div class="trace">${trace.map((step, index) => `<div class="trace-step"><strong>${index + 1}. ${esc(step.node || step.step || step.name || "步骤")}</strong><small>${esc(step.summary || step.status || JSON.stringify(step))}</small></div>`).join("")}</div>`;
+    $("#detail-content").innerHTML = `<dl class="definition-list"><dt>运行标识</dt><dd class="code">${esc(run.id)}</dd><dt>状态</dt><dd>${badge(run.status)}</dd><dt>来源类型</dt><dd>${esc(run.source_type || "—")}</dd><dt>语义方式</dt><dd>${esc(run.semantic_mode || "deterministic")}</dd><dt>大模型</dt><dd>${esc(run.llm_model || "未使用")}</dd><dt>Token</dt><dd>${Number(run.input_tokens || 0)} 输入 / ${Number(run.output_tokens || 0)} 输出</dd><dt>模型耗时</dt><dd>${Number(run.llm_latency_ms || 0).toFixed(2)} ms</dd><dt>Prompt 版本</dt><dd class="code">${esc(run.prompt_version || "—")}</dd><dt>落库结果</dt><dd>${esc(JSON.stringify(result.persisted || {}))}</dd></dl><div class="trace">${trace.map((step, index) => `<div class="trace-step"><strong>${index + 1}. ${esc(termLabel(step.term_name))}</strong><small>${step.model ? `${esc(step.model)} · ` : "确定性步骤 · "}${Number(step.duration_ms || 0).toFixed(2)} ms</small></div>`).join("")}</div>`;
     $("#detail-dialog").showModal();
   } catch (error) { toast(error.message, true); }
 }
@@ -303,7 +377,11 @@ document.addEventListener("click", (event) => {
   const action = event.target.closest("[data-action]")?.dataset.action;
   if (action === "bootstrap") bootstrap();
   if (action === "validate") validate();
+  const demoAction = event.target.closest("[data-demo-action]")?.dataset.demoAction;
+  if (demoAction === "impact") showImpact(event.target.closest("[data-demo-action]").dataset.demoAsset);
+  if (demoAction && demoAction !== "impact") { state.activeTab = demoAction; activateTab(); }
   const impact = event.target.closest("[data-impact]")?.dataset.impact; if (impact) showImpact(impact);
+  const evidence = event.target.closest("[data-evidence]")?.dataset.evidence; if (evidence) showEvidence(evidence);
   const review = event.target.closest("[data-review]"); if (review) reviewAssertion(review.dataset.review, review.dataset.decision);
   const issue = event.target.closest("[data-resolve]")?.dataset.resolve; if (issue) resolveIssue(issue);
   const run = event.target.closest("[data-run]")?.dataset.run; if (run) showRun(run);
@@ -311,6 +389,12 @@ document.addEventListener("click", (event) => {
 });
 
 async function init() {
+  const demoRequested = new URLSearchParams(window.location.search).get("demo") === "1";
+  if (demoRequested) {
+    state.tenant = "local";
+    state.activeTab = "overview";
+    localStorage.setItem("or-tenant", "local");
+  }
   $("#tenant-input").value = state.tenant;
   $("#api-key-input").value = state.apiKey;
   $("#bootstrap-button").addEventListener("click", bootstrap);
@@ -322,7 +406,13 @@ async function init() {
   $("#settings-button").addEventListener("click", () => $("#settings-dialog").showModal());
   $("#save-settings").addEventListener("click", () => { state.apiKey = $("#api-key-input").value.trim(); localStorage.setItem("or-api-key", state.apiKey); toast("连接设置已保存"); });
   $("#close-detail").addEventListener("click", () => $("#detail-dialog").close());
-  try { await loadProjects(); } catch (error) { toast(error.message, true); $("#content").innerHTML = `<div class="panel inline-empty">无法连接服务:${esc(error.message)}</div>`; }
+  try {
+    await loadProjects();
+    if (demoRequested) {
+      const project = state.projects.find(item => item.namespace === DEMO_NAMESPACE);
+      if (project) await loadProjects(project.id);
+    }
+  } catch (error) { toast(error.message, true); $("#content").innerHTML = `<div class="panel inline-empty">无法连接服务:${esc(error.message)}</div>`; }
 }
 
 init();

+ 4 - 4
src/ontorefactor_governance/static/index.html

@@ -27,7 +27,7 @@
     <main>
       <section class="hero">
         <div>
-          <p class="eyebrow">独立运行 · 四层本体 · 可审计智能体</p>
+          <p class="eyebrow">东北产业数字化 · 四层本体 · 可审计智能体</p>
           <h1>把数据资产,变成<br><span>可验证的治理知识。</span></h1>
           <p class="hero-copy">从 DDL、OpenAPI 或资产清单发现对象,生成带证据的语义断言,并用治理规则持续校验。</p>
         </div>
@@ -36,7 +36,7 @@
           <select id="project-select"><option value="">正在加载…</option></select>
           <div class="button-row">
             <button id="new-project-button" class="button ghost">新建空白项目</button>
-            <button id="bootstrap-button" class="button primary">创建演示项目</button>
+            <button id="bootstrap-button" class="button primary">一键准备挑战杯 Demo</button>
             <button id="validate-button" class="button secondary" disabled>运行治理校验</button>
           </div>
           <p id="project-hint" class="muted">项目完全保存在本地 SQLite 中。</p>
@@ -55,8 +55,8 @@
       <section id="empty-state" class="empty-state panel hidden">
         <div class="empty-icon">◇</div>
         <h2>还没有治理项目</h2>
-        <p>创建演示项目可立即查看完整模型,也可以在智能体工作台导入自己的数据定义。</p>
-        <button class="button primary" data-action="bootstrap">创建演示项目</button>
+        <p>一键准备“一汽 DCP 手机号字段迁移”黄金 Demo,也可以在智能体工作台导入自己的数据定义。</p>
+        <button class="button primary" data-action="bootstrap">一键准备挑战杯 Demo</button>
       </section>
 
       <section id="content" aria-live="polite"></section>

+ 68 - 0
src/ontorefactor_governance/static/styles.css

@@ -139,6 +139,67 @@ td.wrap { min-width: 220px; max-width: 420px; white-space: normal; }
 .trace-step small { color: var(--muted); }
 .result-box { padding: 15px; border-radius: 11px; color: #dbece8; background: #183d38; white-space: pre-wrap; word-break: break-word; font: 12px/1.6 "Cascadia Code", monospace; }
 
+.demo-spotlight {
+  position: relative; display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .55fr); gap: 28px;
+  overflow: hidden; margin-bottom: 14px; padding: 34px; color: #f4f7ef; border-radius: 20px;
+  background: radial-gradient(circle at 88% 12%, rgb(200 235 103 / 19%), transparent 17rem), linear-gradient(135deg, #123b35, #092823);
+  box-shadow: 0 22px 60px rgb(16 57 50 / 20%);
+}
+.demo-spotlight::after { content: "M3 · M2 · M1 · M0"; position: absolute; right: 24px; bottom: -14px; color: rgb(255 255 255 / 4%); font: 800 54px/1 Inter, sans-serif; letter-spacing: -.06em; }
+.demo-copy { position: relative; z-index: 1; }
+.demo-kicker { display: flex; align-items: center; gap: 10px; color: #bdd7d0; font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
+.demo-kicker span { padding: 5px 9px; color: #173d35; background: var(--lime); border-radius: 99px; }
+.demo-copy h2 { max-width: 780px; margin: 20px 0 12px; color: #fff; font-family: Georgia, "Noto Serif SC", serif; font-size: clamp(28px, 3.2vw, 46px); font-weight: 500; line-height: 1.12; letter-spacing: -.035em; }
+.demo-copy p { max-width: 760px; margin: 0; color: #bed2cd; font-size: 14px; }
+.demo-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 24px; }
+.button.demo-primary { color: #123b35; background: var(--lime); }
+.button.demo-secondary { color: #f1f7f4; background: rgb(255 255 255 / 8%); border: 1px solid rgb(255 255 255 / 20%); }
+.gate-card { position: relative; z-index: 1; align-self: stretch; display: flex; flex-direction: column; justify-content: center; min-height: 220px; padding: 24px; border: 1px solid rgb(255 255 255 / 14%); border-radius: 16px; background: rgb(255 255 255 / 7%); backdrop-filter: blur(10px); }
+.gate-label { display: flex; align-items: center; gap: 8px; color: #c6d9d4; font-size: 11px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
+.pulse { width: 9px; height: 9px; border-radius: 50%; background: #ff8d73; box-shadow: 0 0 0 6px rgb(255 141 115 / 14%); }
+.gate-card > strong { margin-top: 18px; color: #fff; font-family: Georgia, serif; font-size: 40px; font-weight: 500; line-height: 1; }
+.gate-code { align-self: flex-start; margin-top: 10px; padding: 4px 9px; color: #57190f; background: #ff9c84; border-radius: 99px; font-size: 11px; font-weight: 900; letter-spacing: .09em; }
+.gate-card p { margin-top: 18px; color: #c3d5d0; font-size: 12px; }
+.demo-proof-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 14px; }
+.demo-proof-grid article { display: flex; align-items: center; gap: 12px; padding: 15px 17px; border: 1px solid var(--line); border-radius: 13px; background: rgb(255 253 249 / 85%); }
+.demo-proof-grid strong { color: var(--green); font-family: Georgia, serif; font-size: 26px; line-height: 1; white-space: nowrap; }
+.demo-proof-grid span { color: var(--muted); font-size: 11px; line-height: 1.35; }
+.demo-tour { margin-bottom: 18px; padding: 18px; }
+.demo-tour-head { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 14px; }
+.demo-tour-head div { display: grid; gap: 3px; }
+.demo-tour-head span { color: var(--green-2); font-size: 10px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
+.demo-tour-head strong { font-size: 16px; }
+.demo-tour-head em { color: var(--green-2); font-size: 11px; font-style: normal; font-weight: 700; }
+.demo-steps { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
+.demo-steps button { display: flex; align-items: center; gap: 9px; padding: 12px; color: var(--ink); border: 1px solid var(--line); border-radius: 11px; background: #f7f4ed; text-align: left; }
+.demo-steps button:hover { border-color: var(--green-2); background: #fff; transform: translateY(-2px); }
+.demo-steps b { color: var(--green-2); font: 700 18px/1 Georgia, serif; }
+.demo-steps span { display: grid; font-size: 12px; font-weight: 800; }
+.demo-steps small { color: var(--muted); font-size: 9px; font-weight: 500; }
+.demo-ready { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: -4px 0 18px; padding: 11px 13px; color: var(--green); border: 1px solid rgb(40 121 106 / 20%); border-radius: 10px; background: rgb(40 121 106 / 8%); font-size: 11px; }
+
+#detail-dialog { width: min(1080px, calc(100vw - 30px)); }
+.impact-summary { display: grid; grid-template-columns: repeat(3, 130px) 1fr; gap: 12px; align-items: center; margin-bottom: 14px; padding: 16px; color: #eaf4f1; border-radius: 13px; background: var(--green); }
+.impact-summary div { display: grid; gap: 2px; padding-right: 12px; border-right: 1px solid rgb(255 255 255 / 17%); }
+.impact-summary span { color: #bcd3cd; font-size: 10px; }
+.impact-summary strong { color: #fff; font-family: Georgia, serif; font-size: 30px; line-height: 1; }
+.impact-summary p { margin: 0; color: #cfe0dc; font-size: 11px; }
+.impact-origin { display: grid; gap: 3px; margin: 14px 0; padding: 15px 18px; border-left: 4px solid var(--lime); border-radius: 0 11px 11px 0; background: #edf5e0; }
+.impact-origin span, .impact-origin small { color: var(--muted); font-size: 10px; }
+.impact-origin strong { color: var(--green); font-size: 16px; }
+.impact-columns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
+.impact-columns > section { min-width: 0; padding: 13px; border: 1px solid var(--line); border-radius: 12px; background: #f7f4ed; }
+.impact-columns h3, .impact-relations h3 { margin: 0 0 10px; color: var(--muted); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; }
+.impact-node { display: grid; gap: 5px; margin-top: 7px; padding: 11px; border-radius: 9px; background: #fff; box-shadow: 0 5px 16px rgb(30 50 43 / 6%); }
+.impact-node span { color: var(--muted); font-size: 9px; }
+.impact-node strong { overflow-wrap: anywhere; font-size: 12px; }
+.impact-node.governance { border-left: 3px solid var(--amber); }
+.impact-node.business { border-left: 3px solid var(--blue); }
+.impact-node.software { border-left: 3px solid var(--green-2); }
+.impact-relations { margin-top: 16px; }
+.impact-relations > span { display: inline-block; margin: 0 6px 7px 0; padding: 6px 9px; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; background: #fff; font-size: 9px; }
+.impact-relations b { color: var(--green-2); }
+
 .toast { position: fixed; right: 28px; bottom: 28px; z-index: 10; max-width: 420px; padding: 13px 17px; color: white; background: var(--green); border-radius: 10px; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transform: translateY(12px); transition: .2s; }
 .toast.show { opacity: 1; transform: none; }
 .toast.error { background: var(--red); }
@@ -156,6 +217,7 @@ dialog::backdrop { background: rgb(12 25 24 / 45%); backdrop-filter: blur(4px);
 @media (max-width: 1080px) {
   .metric-grid { grid-template-columns: repeat(3, 1fr); }
   .hero { grid-template-columns: 1fr 340px; gap: 28px; }
+  .demo-steps { grid-template-columns: repeat(3, 1fr); }
 }
 
 @media (max-width: 760px) {
@@ -171,4 +233,10 @@ dialog::backdrop { background: rgb(12 25 24 / 45%); backdrop-filter: blur(4px);
   .toolbar { align-items: stretch; flex-direction: column; }
   .toolbar input, .toolbar select { max-width: none; }
   .form-pair { grid-template-columns: 1fr; }
+  .demo-spotlight { grid-template-columns: 1fr; padding: 24px; }
+  .demo-proof-grid { grid-template-columns: repeat(2, 1fr); }
+  .demo-tour-head, .demo-ready { align-items: flex-start; flex-direction: column; }
+  .demo-steps, .impact-columns { grid-template-columns: 1fr; }
+  .impact-summary { grid-template-columns: repeat(3, 1fr); }
+  .impact-summary p { grid-column: 1 / -1; }
 }

+ 15 - 0
tests/test_api.py

@@ -97,3 +97,18 @@ def test_llm_connection_failure_is_a_structured_api_error(monkeypatch):
 
     assert response.status_code == 502
     assert response.json()["detail"]["code"] == "LLM_AUTH_FAILED"
+
+
+def test_challenge_cup_demo_bootstrap_endpoint(monkeypatch):
+    db = Database("sqlite:///:memory:")
+    monkeypatch.setattr(api, "get_database", lambda: db)
+    client = TestClient(api.app)
+    headers = {"X-Tenant-ID": "challenge-demo"}
+
+    response = client.post("/api/v1/governance/bootstrap/challenge-cup", headers=headers)
+
+    assert response.status_code == 200
+    payload = response.json()
+    assert payload["overview"]["demo"]["decision"] == "暂缓删除"
+    assert payload["overview"]["totals"]["open_issues"] == 4
+    db.close()

+ 20 - 0
tests/test_service.py

@@ -7,6 +7,7 @@ from ontorefactor_governance.service import (
     impact,
     list_elements,
     require_project,
+    seed_challenge_cup_demo,
     seed_tenant_isolation,
 )
 
@@ -56,3 +57,22 @@ def test_impact_and_jsonld_export(db):
     assert any(node["name"] == "DcpTenantTable" for node in affected["nodes"])
     assert exported["@id"] == result["project"]["namespace"]
     assert len(exported["@graph"]) == 104
+
+
+def test_challenge_cup_demo_is_idempotent_and_has_a_field_impact_story(db):
+    first = seed_challenge_cup_demo(db, "demo")
+    second = seed_challenge_cup_demo(db, "demo")
+
+    assert first["validation"]["valid"] is True
+    assert second["overview"]["demo"]["decision_code"] == "BLOCKED"
+    assert second["overview"]["demo"]["asset_id"]
+    assert second["overview"]["totals"]["evidence"] == 6
+    assert second["overview"]["totals"]["rules"] == 5
+    assert second["overview"]["totals"]["open_issues"] == 4
+
+    affected = impact(db, "demo", second["overview"]["demo"]["asset_id"], 3)
+    names = {item["name"] for item in affected["nodes"]}
+    assert "用户画像查询 API" in names
+    assert "短信通知任务" in names
+    assert "IAM 数据 Owner" in names
+    assert "手机号个人信息保护策略" in names