Browse Source

feat: add ontology-driven data governance platform

caozheng 2 months ago
parent
commit
226148f626
47 changed files with 3433 additions and 18 deletions
  1. 21 0
      README.md
  2. 8 0
      agentexample/agentpacks/governance.ontology-steward/README.md
  3. 17 0
      agentexample/agentpacks/governance.ontology-steward/agents/asset.yml
  4. 17 0
      agentexample/agentpacks/governance.ontology-steward/agents/governance.yml
  5. 17 0
      agentexample/agentpacks/governance.ontology-steward/agents/impact.yml
  6. 75 0
      agentexample/agentpacks/governance.ontology-steward/agents/orchestrator.yml
  7. 17 0
      agentexample/agentpacks/governance.ontology-steward/agents/semantics.yml
  8. 27 0
      agentexample/agentpacks/governance.ontology-steward/manifest.yml
  9. 2 1
      agentpaas/src/agentpaas/api/app.py
  10. 8 0
      agentpaas/src/agentpaas/api/v1/agentpacks.py
  11. 9 1
      agentpaas/src/agentpaas/api/v1/agents.py
  12. 272 0
      agentpaas/src/agentpaas/api/v1/governance.py
  13. 163 0
      agentpaas/src/agentpaas/db/models.py
  14. 2 0
      agentpaas/src/agentpaas/engine/builtin_packs.py
  15. 17 4
      agentpaas/src/agentpaas/engine/mcp_registry.py
  16. 19 0
      agentpaas/src/agentpaas/governance/__init__.py
  17. 470 0
      agentpaas/src/agentpaas/governance/agents.py
  18. 853 0
      agentpaas/src/agentpaas/governance/service.py
  19. 18 1
      lambdagent/src/lambdagent/_shell_compat.py
  20. 56 0
      lambdagent/src/lambdagent/agentpack.py
  21. 18 4
      lambdagent/src/lambdagent/fromconfig/compiler.py
  22. 19 1
      lambdagent/src/lambdagent/providers/claude_code_provider.py
  23. 24 3
      lambdagent/src/lambdagent/tool_gateway.py
  24. 27 0
      ontology/README.md
  25. 8 0
      ontology/m1/dcp/business-model.ttl
  26. 16 0
      ontology/m1/dcp/governance-model.ttl
  27. 10 0
      ontology/m1/dcp/software-model.ttl
  28. 16 0
      ontology/m2/business-profile.ttl
  29. 17 0
      ontology/m2/cross-profile-relations.ttl
  30. 22 0
      ontology/m2/governance-profile.ttl
  31. 19 0
      ontology/m2/software-profile.ttl
  32. 30 0
      ontology/m2/upper.ttl
  33. 23 0
      ontology/m3/meta-core.ttl
  34. 11 0
      ontology/shapes/instance-shapes.ttl
  35. 8 0
      ontology/shapes/meta-shapes.ttl
  36. 12 0
      ontology/shapes/model-shapes.ttl
  37. 7 0
      ontology/views/business-view.yaml
  38. 7 0
      ontology/views/engineering-view.yaml
  39. 7 0
      ontology/views/governance-view.yaml
  40. 40 0
      tests/test_agentpack.py
  41. 42 0
      tests/test_api_endpoints.py
  42. 123 0
      tests/test_governance.py
  43. 2 0
      webui/src/App.tsx
  44. 5 2
      webui/src/api/client.ts
  45. 180 0
      webui/src/api/governance.ts
  46. 2 1
      webui/src/components/Sidebar.tsx
  47. 650 0
      webui/src/pages/Governance.tsx

+ 21 - 0
README.md

@@ -38,6 +38,27 @@ Things v1.0 is **not yet good for:** untrusted multi-tenant SaaS, knowledge-dir-
 | [`docs/`](docs/) | Design docs, architecture, audits, comparison studies, [INTRODUCTION.md](docs/INTRODUCTION.md). | — |
 | [`tests/`](tests/) | 189+ pytest suite across DSL, compiler, runtime, cost vectors, algebraic laws, AgentPack e2e. | `pytest tests/` |
 
+## OntoRefactor data-governance platform
+
+The monorepo now includes an end-to-end governance plane based on UML/MOF
+M3—M0 metamodeling. Business, software, and governance are composable M2
+Profiles; important graph edges are reified assertions with evidence,
+confidence, validity, generator, and human-review state.
+
+- Semantic source: [`ontology/`](ontology/)
+- LambdAgent pack: [`agentexample/agentpacks/governance.ontology-steward/`](agentexample/agentpacks/governance.ontology-steward/)
+- REST API: `/api/v1/governance/*` (15 paths, tenant scoped)
+- Web UI: open **数据治理** in the sidebar, then load the built-in tenant-isolation sample
+- Inputs: MySQL DDL, OpenAPI YAML/JSON, or JSON asset inventory
+- Workflow: deterministic extraction → parallel LambdAgent analysis → M0 facts/M1 candidates → validation → evidence/review → impact graph
+
+After starting AgentPaaS and the Web UI, the fastest demo is:
+
+1. Open **数据治理** and click **加载租户隔离样例**.
+2. Inspect the 48-node M3—M0 reference slice and its cross-Profile relations.
+3. Paste DDL into **智能体分析** to create evidence-backed facts and candidates.
+4. Review inferred assertions, run the governance gates, and inspect impact paths.
+
 ## Live deployments
 
 | Service | URL | Notes |

+ 8 - 0
agentexample/agentpacks/governance.ontology-steward/README.md

@@ -0,0 +1,8 @@
+# OntoRefactor 数据治理协作组
+
+该 AgentPack 用于解释和评审治理材料;DDL/OpenAPI 的确定性入图请使用
+WebUI“数据治理”页中的智能抽取入口。协作组所有结论都必须输出事实类型、
+证据、置信度和审核状态。
+
+角色包括资产抽取、业务语义、治理控制和影响分析。M0 事实及 M1 候选可由
+智能体高频产生,M2/M3 只能提交变更提案并等待专家审批。

+ 17 - 0
agentexample/agentpacks/governance.ontology-steward/agents/asset.yml

@@ -0,0 +1,17 @@
+agentId: governance-asset-agent
+name: 软件与数据资产抽取智能体
+type: simple
+model:
+  provider: ollama
+  name: qwen2.5:7b
+  temperature: 0.0
+  maxTokens: 4096
+systemPrompt: |
+  你只负责识别材料中的代码、DDL、API、事件、表、字段和运行事实。
+  能确定的关系标 extracted/observed 并给出原文位置;不确定的不得写成事实。
+  稳定的项目类型进入 M1 candidate,具体版本、部署和观测进入 M0。
+  输出 JSON:elements、assertions、evidence、unknowns。
+guard:
+  validator: "len(x) > 10"
+  retry: 0
+  fallback: last

+ 17 - 0
agentexample/agentpacks/governance.ontology-steward/agents/governance.yml

@@ -0,0 +1,17 @@
+agentId: governance-control-agent
+name: 数据治理与门禁智能体
+type: simple
+model:
+  provider: ollama
+  name: qwen2.5:7b
+  temperature: 0.0
+  maxTokens: 4096
+systemPrompt: |
+  你负责 Owner/Steward、标准、质量、安全、血缘、风险、审批和证据有效性。
+  检查关键能力的软件实现、核心对象的技术表达、生产资产 Owner、敏感字段策略、
+  正式断言证据,以及 inferred 高风险动作。输出 issues、rules、gateResult、approvals。
+  不执行生产变更,不替代人工签字。
+guard:
+  validator: "len(x) > 10"
+  retry: 0
+  fallback: last

+ 17 - 0
agentexample/agentpacks/governance.ontology-steward/agents/impact.yml

@@ -0,0 +1,17 @@
+agentId: governance-impact-agent
+name: 跨域影响分析智能体
+type: simple
+model:
+  provider: ollama
+  name: qwen2.5:7b
+  temperature: 0.0
+  maxTokens: 4096
+systemPrompt: |
+  你只读取已发布 M1 与有效 M0 事实,沿 INSTANCE_OF、CONTAINS、REPRESENTS、
+  REALIZED_BY、GOVERNED_BY、OWNED_BY、VALIDATES、EVALUATES 和 AFFECTS 分析影响。
+  每条路径要列出节点、方向、依据和缺口;不得修改权威模型。
+  输出 JSON:paths、affectedCapabilities、affectedControls、owners、evidenceGaps。
+guard:
+  validator: "len(x) > 10"
+  retry: 0
+  fallback: last

+ 75 - 0
agentexample/agentpacks/governance.ontology-steward/agents/orchestrator.yml

@@ -0,0 +1,75 @@
+agentId: governance-ontology-steward
+name: OntoRefactor 数据治理协作组
+description: 协调四类专业智能体,生成可验证、可追溯、可审批的数据治理结论。
+type: react
+model:
+  provider: ollama
+  name: qwen2.5:7b
+  temperature: 0.1
+  maxTokens: 8192
+systemPrompt: |
+  你是 OntoRefactor 数据治理主编排器。平台采用 UML/MOF 的 M3-M0 四层元建模:
+  M3 是稳定的本体建模内核;M2 是通用上层本体及 business/software/governance
+  三个 Profile;M1 是项目模型和候选区;M0 是真实资产、运行事实与证据。
+
+  工作规则:
+  1. 先识别用户材料和问题类型,再分别调用资产、语义、治理和影响智能体;
+  2. 解析器可确定的内容标为 extracted/observed,模型推断标为 inferred;
+  3. 每个重要关系输出 subject/predicate/object、confidence、evidence、generatedBy、
+     validFrom/validTo 和 reviewStatus;
+  4. M0 事实和 M1 候选可以建议写入,M2/M3 只能形成变更提案;
+  5. inferred 结论不得直接触发删除、授权、降级、退役或正式发布;
+  6. 不得把业务、软件、治理三个 Profile 错当成三个元层。
+
+  必须依次调用 call_asset、call_semantics、call_governance、call_impact,综合分歧后
+  写出 `{workspace}/governance_report.md` 和 `{workspace}/assertions.json`,最后 terminate。
+  assertions.json 必须是合法 JSON,报告必须包含“结论、层级、证据、影响、风险、待审批项”。
+react:
+  maxSteps: 30
+  observationEnabled: true
+  progressDiscipline: true
+  enforceLoop:
+    sequence: [call_asset, call_semantics, call_governance, call_impact]
+    minCycles: 1
+memory:
+  enabled: true
+  strategy: local
+  size: 30
+guard:
+  validator: "'证据' in x and ('review' in x.lower() or '审核' in x)"
+  retry: 1
+  fallback: last
+  dangerousCommandBlock: true
+  highRiskConfirmation: true
+subAgents:
+  asset:
+    config: ./asset.yml
+    tool: call_asset
+    description: 确定性资产与技术关系抽取
+  semantics:
+    config: ./semantics.yml
+    tool: call_semantics
+    description: 业务语义、术语与跨 Profile 映射
+  governance:
+    config: ./governance.yml
+    tool: call_governance
+    description: 责任、质量、安全、规则和证据门禁
+  impact:
+    config: ./impact.yml
+    tool: call_impact
+    description: 跨业务、软件、治理的影响路径分析
+mcp:
+  localTools:
+    - call_asset
+    - call_semantics
+    - call_governance
+    - call_impact
+    - ReadFile
+    - ListFiles
+    - SearchContent
+    - KBSearch
+    - WriteFile
+    - terminate
+  policy:
+    mode: auto
+    maxConcurrent: 1

+ 17 - 0
agentexample/agentpacks/governance.ontology-steward/agents/semantics.yml

@@ -0,0 +1,17 @@
+agentId: governance-semantics-agent
+name: 业务语义与实体对齐智能体
+type: simple
+model:
+  provider: ollama
+  name: qwen2.5:7b
+  temperature: 0.1
+  maxTokens: 4096
+systemPrompt: |
+  你负责识别业务能力、业务对象、规则、事件和术语,并提出 REALIZED_BY、
+  REPRESENTS、OPERATES_ON 等 M1 跨 Profile 候选映射。所有映射默认 inferred、
+  reviewStatus=pending,必须说明命名、需求或专家材料证据,不得自行发布 M2/M3。
+  输出 JSON:businessCandidates、mappingAssertions、conflicts、evidence。
+guard:
+  validator: "len(x) > 10"
+  retry: 0
+  fallback: last

+ 27 - 0
agentexample/agentpacks/governance.ontology-steward/manifest.yml

@@ -0,0 +1,27 @@
+id: governance.ontology-steward
+name: OntoRefactor 数据治理协作组
+version: 1.0.0
+domain: governance
+entrypoint: agents/orchestrator.yml
+description: >
+  面向 M3-M0 四层元建模的数据治理协作智能体。协调资产抽取、业务语义、
+  治理控制和影响分析四个角色,输出带证据、置信度和审核状态的候选断言,
+  不允许智能体自动发布 M2/M3 标准或执行生产高风险动作。
+audience:
+  - data_owner
+  - data_steward
+  - architect
+  - developer
+permissions:
+  network: false
+  shell: false
+  file_write: workspace
+  read_knowledge: true
+  read_filesystem: true
+model:
+  recommended:
+    - ollama/qwen2.5:7b
+    - dashscope/qwen-max
+    - anthropic/claude-sonnet
+author: OntoRefactor Team
+license: BUSL-1.1

+ 2 - 1
agentpaas/src/agentpaas/api/app.py

@@ -17,7 +17,7 @@ from agentpaas.config import settings
 # (默认 no-op)。providers 导入也会调一次,二者幂等。
 from lambdagent._ssl_bootstrap import configure_ssl as _configure_ssl
 _configure_ssl()
-from agentpaas.api.v1 import agents, auth, admin, billing, traces, jobs, discovery, metrics, status, analyze, setup as setup_router, providers_api, templates_api, knowledge as knowledge_router, agentpacks, assistant, mcp as mcp_router, skills as skills_router
+from agentpaas.api.v1 import agents, auth, admin, billing, traces, jobs, discovery, metrics, status, analyze, setup as setup_router, providers_api, templates_api, knowledge as knowledge_router, agentpacks, assistant, mcp as mcp_router, skills as skills_router, governance as governance_router
 from agentpaas.observability.logging import logger
 
 # 改进⑦ (AUDIT_2026-06-11): on_event("startup") 在 FastAPI 中已弃用,
@@ -226,6 +226,7 @@ app.include_router(agentpacks.router, prefix="/api/v1")
 app.include_router(assistant.router, prefix="/api/v1")
 app.include_router(mcp_router.router, prefix="/api/v1")
 app.include_router(skills_router.router, prefix="/api/v1")
+app.include_router(governance_router.router, prefix="/api/v1")
 
 
 async def _on_startup():

+ 8 - 0
agentpaas/src/agentpaas/api/v1/agentpacks.py

@@ -397,6 +397,13 @@ async def create_agent_from_pack(
     if req.system_prompt_override is not None:
         config["systemPrompt"] = req.system_prompt_override
 
+    # Manifest permissions are runtime capabilities, not merely install-time
+    # metadata.  Persist the effective boundary so lazy sub-agents inherit it.
+    from lambdagent.agentpack import enforce_config_permissions
+    config, permission_report = enforce_config_permissions(
+        config, pack.manifest.permissions
+    )
+
     config_json = json.dumps(config, ensure_ascii=False)
     config_hash = hashlib.sha256(config_json.encode()).hexdigest()
 
@@ -439,6 +446,7 @@ async def create_agent_from_pack(
         "agent_id": agent_id,
         "name": req.name,
         "pack": {"id": pack.id, "version": pack.version, "name": pack.name},
+        "permission_enforcement": permission_report,
         "work_dir": work_dir,
         "source_dir": source_dir,
         "run_dir": run_dir,

+ 9 - 1
agentpaas/src/agentpaas/api/v1/agents.py

@@ -2192,7 +2192,15 @@ def _execute_agent(config: dict, input_text: str, on_step=None,
     # 写各自 run 工作区,root=None 不受影响。finally(下方)清空,防泄漏到下个 run。
     try:
         from lambdagent.builtin_tools._sandbox import set_sandbox_root
-        set_sandbox_root(inplace_dir if (inplace_dir and os.path.isdir(inplace_dir)) else None)
+        pack_permissions = config.get("_agentpack_permissions") or {}
+        pack_write_scope = pack_permissions.get("file_write")
+        if inplace_dir and os.path.isdir(inplace_dir):
+            write_root = inplace_dir
+        elif pack_write_scope == "workspace" and workspace_path:
+            write_root = workspace_path
+        else:
+            write_root = None
+        set_sandbox_root(write_root)
     except Exception as _sbe:
         logger.warning("set sandbox root failed: %s", _sbe)
 

+ 272 - 0
agentpaas/src/agentpaas/api/v1/governance.py

@@ -0,0 +1,272 @@
+"""OntoRefactor four-level ontology and data-governance REST API."""
+from __future__ import annotations
+
+from typing import Any, Dict, List, Optional
+
+from fastapi import APIRouter, Depends, Query
+from pydantic import BaseModel, Field
+
+from agentpaas.api.deps import get_tenant
+from agentpaas.api.errors import api_error
+from agentpaas.api.middleware.auth import TenantContext
+from agentpaas.db.session import get_db
+from agentpaas.governance import service
+from agentpaas.governance.agents import execute_governance_agent, list_agent_runs
+
+
+router = APIRouter(prefix="/governance", tags=["governance"])
+
+
+def _call(fn, *args, **kwargs):
+    try:
+        return fn(*args, **kwargs)
+    except service.GovernanceError as exc:
+        api_error(exc.status, exc.code, str(exc))
+
+
+class ProjectCreate(BaseModel):
+    name: str = Field(..., min_length=1, max_length=256)
+    description: str = Field(default="", max_length=4096)
+    namespace: str = Field(..., min_length=3, max_length=1024)
+    version: str = Field(default="1.0.0", max_length=64)
+    status: str = Field(default="active", max_length=32)
+
+
+class ElementUpsert(BaseModel):
+    uri: str = Field(default="", max_length=2048)
+    layer: str
+    profile: str
+    kind: str = Field(..., min_length=1, max_length=128)
+    name: str = Field(..., min_length=1, max_length=256)
+    description: str = Field(default="", max_length=8192)
+    status: str = "candidate"
+    version: str = "1.0.0"
+    attributes: Dict[str, Any] = Field(default_factory=dict)
+    source: str = Field(default="", max_length=2048)
+
+
+class EvidenceCreate(BaseModel):
+    kind: str = Field(default="SourceArtifact", max_length=128)
+    uri: str = Field(default="", max_length=2048)
+    title: str = Field(..., min_length=1, max_length=512)
+    content: str = Field(default="", max_length=524288)
+    content_hash: str = Field(default="", max_length=128)
+    source: str = Field(default="", max_length=2048)
+    observed_at: Optional[str] = None
+    valid_from: Optional[str] = None
+    valid_to: Optional[str] = None
+    metadata: Dict[str, Any] = Field(default_factory=dict)
+
+
+class AssertionCreate(BaseModel):
+    subject_id: str
+    predicate: str = Field(..., min_length=1, max_length=128)
+    object_id: Optional[str] = None
+    object_value: str = Field(default="", max_length=8192)
+    assertion_type: str = "inferred"
+    confidence: float = Field(default=0.0, ge=0.0, le=1.0)
+    evidence_ids: List[str] = Field(default_factory=list)
+    generated_by: str = Field(default="", max_length=1024)
+    valid_from: Optional[str] = None
+    valid_to: Optional[str] = None
+    review_status: str = "pending"
+    reviewer: str = Field(default="", max_length=256)
+    review_note: str = Field(default="", max_length=4096)
+
+
+class AssertionReview(BaseModel):
+    status: str
+    reviewer: str = Field(..., min_length=1, max_length=256)
+    note: str = Field(default="", max_length=4096)
+
+
+class RuleUpsert(BaseModel):
+    name: str = Field(..., min_length=1, max_length=256)
+    description: str = Field(default="", max_length=4096)
+    rule_type: str = Field(default="integrity", max_length=128)
+    target_layer: str = ""
+    target_profile: str = ""
+    target_kind: str = ""
+    expression: Dict[str, Any] = Field(default_factory=dict)
+    severity: str = "medium"
+    enabled: bool = True
+    version: str = "1.0.0"
+
+
+class IssueUpdate(BaseModel):
+    status: str
+    assigned_to: str = Field(default="", max_length=256)
+
+
+class AgentAnalyzeRequest(BaseModel):
+    source_type: str = Field(..., description="ddl | openapi | inventory")
+    content: str = Field(..., min_length=1, max_length=524288)
+    source_name: str = Field(default="", max_length=1024)
+    environment: str = Field(default="development", max_length=128)
+    persist: bool = True
+
+
+@router.get("/projects")
+async def projects(tenant: TenantContext = Depends(get_tenant)):
+    return {"projects": service.list_projects(get_db(), tenant.tenant_id)}
+
+
+@router.post("/projects", status_code=201)
+async def create_project(body: ProjectCreate, tenant: TenantContext = Depends(get_tenant)):
+    return _call(service.create_project, get_db(), tenant.tenant_id, body.model_dump())
+
+
+@router.post("/bootstrap/tenant-isolation")
+async def bootstrap_tenant_isolation(tenant: TenantContext = Depends(get_tenant)):
+    """Install the idempotent M3->M0 tenant-isolation reference slice."""
+    return _call(service.seed_tenant_isolation, get_db(), tenant.tenant_id)
+
+
+@router.get("/projects/{project_id}/overview")
+async def project_overview(project_id: str, tenant: TenantContext = Depends(get_tenant)):
+    return _call(service.overview, get_db(), tenant.tenant_id, project_id)
+
+
+@router.get("/projects/{project_id}/elements")
+async def elements(
+    project_id: str,
+    layer: str = "",
+    profile: str = "",
+    status: str = "",
+    q: str = "",
+    limit: int = Query(default=200, ge=1, le=500),
+    offset: int = Query(default=0, ge=0),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(
+        service.list_elements, get_db(), tenant.tenant_id, project_id,
+        layer=layer, profile=profile, status=status, query=q, limit=limit, offset=offset,
+    )
+
+
+@router.post("/projects/{project_id}/elements", status_code=201)
+async def upsert_element(
+    project_id: str, body: ElementUpsert, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(service.upsert_element, get_db(), tenant.tenant_id, project_id, body.model_dump())
+
+
+@router.get("/elements/{element_id}/impact")
+async def element_impact(
+    element_id: str, max_depth: int = Query(default=4, ge=1, le=8),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(service.impact, get_db(), tenant.tenant_id, element_id, max_depth)
+
+
+@router.get("/projects/{project_id}/graph")
+async def project_graph(
+    project_id: str, include_pending: bool = True,
+    limit: int = Query(default=500, ge=1, le=500),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(
+        service.graph, get_db(), tenant.tenant_id, project_id,
+        include_pending=include_pending, limit=limit,
+    )
+
+
+@router.get("/projects/{project_id}/evidence")
+async def evidence(
+    project_id: str, limit: int = Query(default=200, ge=1, le=500),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    return {"evidence": _call(service.list_evidence, get_db(), tenant.tenant_id, project_id, limit)}
+
+
+@router.post("/projects/{project_id}/evidence", status_code=201)
+async def add_evidence(
+    project_id: str, body: EvidenceCreate, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(service.create_evidence, get_db(), tenant.tenant_id, project_id, body.model_dump())
+
+
+@router.get("/projects/{project_id}/assertions")
+async def assertions(
+    project_id: str, review_status: str = "", assertion_type: str = "",
+    limit: int = Query(default=300, ge=1, le=500),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    return {"assertions": _call(
+        service.list_assertions, get_db(), tenant.tenant_id, project_id,
+        review_status=review_status, assertion_type=assertion_type, limit=limit,
+    )}
+
+
+@router.post("/projects/{project_id}/assertions", status_code=201)
+async def add_assertion(
+    project_id: str, body: AssertionCreate, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(service.create_assertion, get_db(), tenant.tenant_id, project_id, body.model_dump())
+
+
+@router.post("/assertions/{assertion_id}/review")
+async def review_assertion(
+    assertion_id: str, body: AssertionReview, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(
+        service.review_assertion, get_db(), tenant.tenant_id, assertion_id,
+        body.status, body.reviewer, body.note,
+    )
+
+
+@router.get("/projects/{project_id}/rules")
+async def rules(project_id: str, tenant: TenantContext = Depends(get_tenant)):
+    return {"rules": _call(service.list_rules, get_db(), tenant.tenant_id, project_id)}
+
+
+@router.post("/projects/{project_id}/rules", status_code=201)
+async def add_rule(
+    project_id: str, body: RuleUpsert, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(service.upsert_rule, get_db(), tenant.tenant_id, project_id, body.model_dump())
+
+
+@router.post("/projects/{project_id}/validate")
+async def validate(project_id: str, tenant: TenantContext = Depends(get_tenant)):
+    return _call(service.validate_project, get_db(), tenant.tenant_id, project_id, persist=True)
+
+
+@router.get("/projects/{project_id}/issues")
+async def issues(
+    project_id: str, status: str = "open", tenant: TenantContext = Depends(get_tenant),
+):
+    return {"issues": _call(service.list_issues, get_db(), tenant.tenant_id, project_id, status)}
+
+
+@router.put("/issues/{issue_id}")
+async def change_issue(
+    issue_id: str, body: IssueUpdate, tenant: TenantContext = Depends(get_tenant),
+):
+    return _call(
+        service.update_issue, get_db(), tenant.tenant_id, issue_id,
+        body.status, body.assigned_to,
+    )
+
+
+@router.post("/projects/{project_id}/agent-runs/analyze")
+async def analyze_with_agents(
+    project_id: str, body: AgentAnalyzeRequest, tenant: TenantContext = Depends(get_tenant),
+):
+    _call(service.require_project, get_db(), tenant.tenant_id, project_id)
+    try:
+        return execute_governance_agent(
+            get_db(), tenant.tenant_id, project_id, body.model_dump(), persist=body.persist,
+        )
+    except service.GovernanceError as exc:
+        api_error(exc.status, exc.code, str(exc))
+
+
+@router.get("/projects/{project_id}/agent-runs")
+async def agent_runs(
+    project_id: str, limit: int = Query(default=50, ge=1, le=200),
+    tenant: TenantContext = Depends(get_tenant),
+):
+    _call(service.require_project, get_db(), tenant.tenant_id, project_id)
+    return {"runs": list_agent_runs(get_db(), tenant.tenant_id, project_id, limit)}
+

+ 163 - 0
agentpaas/src/agentpaas/db/models.py

@@ -356,6 +356,140 @@ class Database:
             )
         """)
 
+        # OntoRefactor governance plane (2026-08-17).
+        #
+        # The model follows the UML/MOF four-level architecture:
+        # M3 meta-kernel -> M2 upper ontology + profiles -> M1 project model
+        # -> M0 runtime facts.  Important relations are reified as assertions
+        # so evidence, provenance, validity and human review stay queryable.
+        self._ensure_table("governance_projects", """
+            CREATE TABLE IF NOT EXISTS governance_projects (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                name TEXT NOT NULL,
+                description TEXT DEFAULT '',
+                namespace TEXT NOT NULL,
+                version TEXT DEFAULT '1.0.0',
+                status TEXT DEFAULT 'active',
+                created_at TEXT,
+                updated_at TEXT,
+                UNIQUE(tenant_id, namespace)
+            )
+        """)
+        self._ensure_table("ontology_elements", """
+            CREATE TABLE IF NOT EXISTS ontology_elements (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                uri TEXT NOT NULL,
+                layer TEXT NOT NULL,
+                profile TEXT NOT NULL,
+                kind TEXT NOT NULL,
+                name TEXT NOT NULL,
+                description TEXT DEFAULT '',
+                status TEXT DEFAULT 'candidate',
+                version TEXT DEFAULT '1.0.0',
+                attributes TEXT DEFAULT '{}',
+                source TEXT DEFAULT '',
+                created_at TEXT,
+                updated_at TEXT,
+                UNIQUE(tenant_id, project_id, uri)
+            )
+        """)
+        self._ensure_table("governance_evidence", """
+            CREATE TABLE IF NOT EXISTS governance_evidence (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                kind TEXT NOT NULL,
+                uri TEXT DEFAULT '',
+                title TEXT NOT NULL,
+                content TEXT DEFAULT '',
+                content_hash TEXT DEFAULT '',
+                source TEXT DEFAULT '',
+                observed_at TEXT,
+                valid_from TEXT,
+                valid_to TEXT,
+                metadata TEXT DEFAULT '{}',
+                created_at TEXT
+            )
+        """)
+        self._ensure_table("governance_assertions", """
+            CREATE TABLE IF NOT EXISTS governance_assertions (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                subject_id TEXT REFERENCES ontology_elements(id),
+                predicate TEXT NOT NULL,
+                object_id TEXT REFERENCES ontology_elements(id),
+                object_value TEXT DEFAULT '',
+                assertion_type TEXT DEFAULT 'inferred',
+                confidence REAL DEFAULT 0.0,
+                evidence_ids TEXT DEFAULT '[]',
+                generated_by TEXT DEFAULT '',
+                valid_from TEXT,
+                valid_to TEXT,
+                review_status TEXT DEFAULT 'pending',
+                reviewer TEXT DEFAULT '',
+                review_note TEXT DEFAULT '',
+                created_at TEXT,
+                updated_at TEXT
+            )
+        """)
+        self._ensure_table("governance_rules", """
+            CREATE TABLE IF NOT EXISTS governance_rules (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                name TEXT NOT NULL,
+                description TEXT DEFAULT '',
+                rule_type TEXT NOT NULL,
+                target_layer TEXT DEFAULT '',
+                target_profile TEXT DEFAULT '',
+                target_kind TEXT DEFAULT '',
+                expression TEXT DEFAULT '{}',
+                severity TEXT DEFAULT 'medium',
+                enabled INTEGER DEFAULT 1,
+                version TEXT DEFAULT '1.0.0',
+                created_at TEXT,
+                updated_at TEXT
+            )
+        """)
+        self._ensure_table("governance_issues", """
+            CREATE TABLE IF NOT EXISTS governance_issues (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                rule_id TEXT DEFAULT '',
+                target_id TEXT DEFAULT '',
+                assertion_id TEXT DEFAULT '',
+                severity TEXT DEFAULT 'medium',
+                title TEXT NOT NULL,
+                description TEXT DEFAULT '',
+                status TEXT DEFAULT 'open',
+                evidence_ids TEXT DEFAULT '[]',
+                assigned_to TEXT DEFAULT '',
+                created_at TEXT,
+                updated_at TEXT,
+                resolved_at TEXT
+            )
+        """)
+        self._ensure_table("governance_agent_runs", """
+            CREATE TABLE IF NOT EXISTS governance_agent_runs (
+                id TEXT PRIMARY KEY,
+                tenant_id TEXT REFERENCES tenants(id),
+                project_id TEXT REFERENCES governance_projects(id),
+                source_type TEXT NOT NULL,
+                input_hash TEXT DEFAULT '',
+                status TEXT DEFAULT 'running',
+                result_json TEXT DEFAULT '{}',
+                trace_json TEXT DEFAULT '[]',
+                error TEXT DEFAULT '',
+                created_at TEXT,
+                completed_at TEXT
+            )
+        """)
+
         # Indexes for KB tables (FIX-06 extension)
         try:
             self.conn.executescript("""
@@ -369,6 +503,35 @@ class Database:
         except Exception:
             pass
 
+        # Governance indexes are deliberately tenant-first: every API query
+        # is scoped by tenant_id before project/filter columns.
+        try:
+            self.conn.executescript("""
+                CREATE INDEX IF NOT EXISTS idx_gov_projects_tenant
+                    ON governance_projects(tenant_id, updated_at DESC);
+                CREATE INDEX IF NOT EXISTS idx_ontology_elements_scope
+                    ON ontology_elements(tenant_id, project_id, layer, profile, status);
+                CREATE INDEX IF NOT EXISTS idx_ontology_elements_name
+                    ON ontology_elements(tenant_id, project_id, name);
+                CREATE INDEX IF NOT EXISTS idx_gov_assertions_scope
+                    ON governance_assertions(tenant_id, project_id, review_status);
+                CREATE INDEX IF NOT EXISTS idx_gov_assertions_subject
+                    ON governance_assertions(subject_id, predicate);
+                CREATE INDEX IF NOT EXISTS idx_gov_assertions_object
+                    ON governance_assertions(object_id, predicate);
+                CREATE INDEX IF NOT EXISTS idx_gov_evidence_scope
+                    ON governance_evidence(tenant_id, project_id, observed_at);
+                CREATE INDEX IF NOT EXISTS idx_gov_rules_scope
+                    ON governance_rules(tenant_id, project_id, enabled);
+                CREATE INDEX IF NOT EXISTS idx_gov_issues_scope
+                    ON governance_issues(tenant_id, project_id, status, severity);
+                CREATE INDEX IF NOT EXISTS idx_gov_agent_runs_scope
+                    ON governance_agent_runs(tenant_id, project_id, created_at DESC);
+            """)
+            self.conn.commit()
+        except Exception:
+            pass
+
     def execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
         return self.conn.execute(sql, params)
 

+ 2 - 0
agentpaas/src/agentpaas/engine/builtin_packs.py

@@ -35,6 +35,8 @@ BUILTIN_PACK_IDS: list[str] = [
     "service.academic-letters",
     # 通用
     "workspace.assistant",
+    # OntoRefactor 数据治理
+    "governance.ontology-steward",
 ]
 
 

+ 17 - 4
agentpaas/src/agentpaas/engine/mcp_registry.py

@@ -51,12 +51,21 @@ def _load() -> Dict[str, dict]:
 
 def _atomic_update(mutate) -> Dict[str, dict]:
     """带文件锁的读-改-写。mutate(data) 原地改 data;返回最终 data。"""
-    import fcntl
     os.makedirs(os.path.dirname(MCP_FILE), exist_ok=True)
     # 用单独的锁文件,避免 truncate 与读冲突
     lock_path = MCP_FILE + ".lock"
-    with open(lock_path, "w") as lock:
-        fcntl.flock(lock, fcntl.LOCK_EX)
+    with open(lock_path, "a+b") as lock:
+        if os.name == "nt":
+            import msvcrt
+            lock.seek(0, os.SEEK_END)
+            if lock.tell() == 0:
+                lock.write(b"\0")
+                lock.flush()
+            lock.seek(0)
+            msvcrt.locking(lock.fileno(), msvcrt.LK_LOCK, 1)
+        else:
+            import fcntl
+            fcntl.flock(lock, fcntl.LOCK_EX)
         try:
             data = _load()
             mutate(data)
@@ -66,7 +75,11 @@ def _atomic_update(mutate) -> Dict[str, dict]:
             os.replace(tmp, MCP_FILE)
             return data
         finally:
-            fcntl.flock(lock, fcntl.LOCK_UN)
+            if os.name == "nt":
+                lock.seek(0)
+                msvcrt.locking(lock.fileno(), msvcrt.LK_UNLCK, 1)
+            else:
+                fcntl.flock(lock, fcntl.LOCK_UN)
 
 
 # ── 工具风险预分级(评审#15)─────────────────────────────────────────────────

+ 19 - 0
agentpaas/src/agentpaas/governance/__init__.py

@@ -0,0 +1,19 @@
+"""OntoRefactor data-governance domain services."""
+
+from .service import (
+    ASSERTION_TYPES,
+    ELEMENT_STATUSES,
+    LAYERS,
+    PROFILES,
+    REVIEW_STATUSES,
+    GovernanceError,
+)
+
+__all__ = [
+    "ASSERTION_TYPES",
+    "ELEMENT_STATUSES",
+    "LAYERS",
+    "PROFILES",
+    "REVIEW_STATUSES",
+    "GovernanceError",
+]

+ 470 - 0
agentpaas/src/agentpaas/governance/agents.py

@@ -0,0 +1,470 @@
+"""Deterministic governance agents composed with the LambdAgent runtime.
+
+These agents deliberately use parsers and rules for facts that can be known
+from DDL/OpenAPI/inventory input.  Semantic mappings are emitted as pending
+inferences.  The distinction is preserved when results are persisted.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+from dataclasses import asdict
+from typing import Any, Dict, List
+
+import yaml
+
+from lambdagent import Context, Guard, Par, Tool
+
+from agentpaas.db.models import gen_id, now_utc
+from .service import (
+    GovernanceError,
+    create_assertion,
+    create_evidence,
+    overview,
+    upsert_element,
+    validate_project,
+)
+
+
+SOURCE_TYPES = ("ddl", "openapi", "inventory")
+_SENSITIVE_TOKENS = (
+    "mobile", "phone", "email", "id_card", "idcard", "identity", "password",
+    "secret", "token", "bank", "address", "姓名", "手机号", "身份证", "邮箱",
+)
+
+
+def _safe_name(value: str) -> str:
+    value = re.sub(r"[^A-Za-z0-9_\-]+", "-", value.strip())
+    return value.strip("-") or "unnamed"
+
+
+def _pascal(value: str) -> str:
+    words = re.split(r"[^A-Za-z0-9\u4e00-\u9fff]+|_+", value)
+    return "".join(word[:1].upper() + word[1:] for word in words if word) or "Unnamed"
+
+
+def _is_sensitive(name: str) -> bool:
+    lowered = name.lower()
+    return any(token in lowered for token in _SENSITIVE_TOKENS)
+
+
+def _split_sql_columns(body: str) -> List[str]:
+    chunks: List[str] = []
+    current: List[str] = []
+    depth = 0
+    quote = ""
+    for char in body:
+        if quote:
+            current.append(char)
+            if char == quote:
+                quote = ""
+            continue
+        if char in ("'", '"', "`"):
+            quote = char
+            current.append(char)
+        elif char == "(":
+            depth += 1
+            current.append(char)
+        elif char == ")":
+            depth = max(depth - 1, 0)
+            current.append(char)
+        elif char == "," and depth == 0:
+            chunks.append("".join(current).strip())
+            current = []
+        else:
+            current.append(char)
+    if current:
+        chunks.append("".join(current).strip())
+    return chunks
+
+
+def _parse_ddl(content: str) -> dict:
+    tables = []
+    pattern = re.compile(
+        r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:[`\"]?([\w-]+)[`\"]?\.)?"
+        r"[`\"]?([\w-]+)[`\"]?\s*\((.*?)\)\s*(?:ENGINE\b[^;]*)?;",
+        re.IGNORECASE | re.DOTALL,
+    )
+    for match in pattern.finditer(content):
+        schema, table_name, body = match.groups()
+        columns = []
+        for raw in _split_sql_columns(body):
+            line = raw.strip()
+            if not line or re.match(
+                r"^(PRIMARY|FOREIGN|UNIQUE|CONSTRAINT|KEY|INDEX|CHECK)\b", line, re.IGNORECASE
+            ):
+                continue
+            col = re.match(r"^[`\"]?([\w-]+)[`\"]?\s+([A-Za-z]+(?:\s*\([^)]*\))?)", line)
+            if not col:
+                continue
+            name, data_type = col.groups()
+            columns.append({
+                "name": name,
+                "data_type": re.sub(r"\s+", "", data_type).upper(),
+                "nullable": not bool(re.search(r"\bNOT\s+NULL\b", line, re.IGNORECASE)),
+                "primary_key": bool(re.search(r"\bPRIMARY\s+KEY\b", line, re.IGNORECASE)),
+                "sensitive": _is_sensitive(name),
+            })
+        tables.append({"schema": schema or "default", "name": table_name, "columns": columns})
+    if not tables:
+        raise GovernanceError("未从 DDL 中识别到 CREATE TABLE 语句", code="NO_DDL_ASSETS")
+    return {"tables": tables, "apis": [], "schemas": []}
+
+
+def _parse_openapi(content: str) -> dict:
+    try:
+        doc = yaml.safe_load(content) or {}
+    except yaml.YAMLError as exc:
+        raise GovernanceError(f"OpenAPI 解析失败: {exc}", code="INVALID_OPENAPI") from exc
+    if not isinstance(doc, dict) or not isinstance(doc.get("paths"), dict):
+        raise GovernanceError("输入不是有效的 OpenAPI 文档:缺少 paths", code="INVALID_OPENAPI")
+    apis = []
+    for path, methods in doc["paths"].items():
+        if not isinstance(methods, dict):
+            continue
+        for method, operation in methods.items():
+            if method.lower() not in {"get", "post", "put", "patch", "delete", "options", "head"}:
+                continue
+            operation = operation if isinstance(operation, dict) else {}
+            apis.append({
+                "path": str(path), "method": method.upper(),
+                "operation_id": operation.get("operationId") or f"{method}-{path}",
+                "summary": operation.get("summary", ""),
+            })
+    schemas = []
+    components = doc.get("components") if isinstance(doc.get("components"), dict) else {}
+    for name, schema in (components.get("schemas") or {}).items():
+        props = schema.get("properties", {}) if isinstance(schema, dict) else {}
+        schemas.append({
+            "name": name,
+            "properties": [
+                {"name": prop_name, "data_type": str((prop or {}).get("type", "object")), "sensitive": _is_sensitive(prop_name)}
+                for prop_name, prop in props.items()
+            ],
+        })
+    return {"tables": [], "apis": apis, "schemas": schemas}
+
+
+def _parse_inventory(content: str) -> dict:
+    try:
+        doc = json.loads(content)
+    except ValueError as exc:
+        raise GovernanceError("inventory 必须是 JSON", code="INVALID_INVENTORY") from exc
+    if not isinstance(doc, dict):
+        raise GovernanceError("inventory 顶层必须是对象", code="INVALID_INVENTORY")
+    return {
+        "tables": doc.get("tables") or [],
+        "apis": doc.get("apis") or [],
+        "schemas": doc.get("schemas") or [],
+    }
+
+
+def normalize_source(payload: Dict[str, Any]) -> dict:
+    source_type = str(payload.get("source_type", "")).lower()
+    content = str(payload.get("content", ""))
+    if source_type not in SOURCE_TYPES:
+        raise GovernanceError(f"source_type 必须是 {', '.join(SOURCE_TYPES)}")
+    if not content.strip():
+        raise GovernanceError("content 不能为空")
+    if len(content.encode("utf-8")) > 512 * 1024:
+        raise GovernanceError("输入超过 512KB", code="SOURCE_TOO_LARGE", status=413)
+    if source_type == "ddl":
+        parsed = _parse_ddl(content)
+    elif source_type == "openapi":
+        parsed = _parse_openapi(content)
+    else:
+        parsed = _parse_inventory(content)
+    return {
+        "source_type": source_type,
+        "source_name": str(payload.get("source_name") or f"inline.{source_type}"),
+        "environment": str(payload.get("environment") or "development"),
+        "content": content,
+        "parsed": parsed,
+    }
+
+
+def discover_assets(normalized: dict) -> dict:
+    elements: List[dict] = []
+    assertions: List[dict] = []
+    env = normalized["environment"]
+    source = normalized["source_name"]
+
+    for table in normalized["parsed"]["tables"]:
+        table_name = str(table.get("name", "unnamed"))
+        schema = str(table.get("schema") or "default")
+        table_key = f"m1:table:{schema}:{table_name}"
+        physical_key = f"m0:table:{schema}:{table_name}"
+        elements.extend((
+            {"local_id": table_key, "layer": "M1", "profile": "software", "kind": "Table",
+             "name": f"{_pascal(table_name)}Table", "status": "candidate",
+             "uri": f"urn:ontorefactor:model:table:{_safe_name(schema)}:{_safe_name(table_name)}",
+             "attributes": {"schema": schema, "table": table_name}},
+            {"local_id": physical_key, "layer": "M0", "profile": "software", "kind": "PhysicalTable",
+             "name": f"{env}.{schema}.{table_name}", "status": "published",
+             "uri": f"db://{_safe_name(env)}/{_safe_name(schema)}/{_safe_name(table_name)}",
+             "attributes": {"environment": env, "schema": schema, "table": table_name}},
+        ))
+        assertions.append({"subject": physical_key, "predicate": "INSTANCE_OF", "object": table_key,
+                           "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"})
+        for column in table.get("columns") or []:
+            column_name = str(column.get("name", "unnamed"))
+            column_key = f"m1:column:{schema}:{table_name}:{column_name}"
+            physical_column_key = f"m0:column:{schema}:{table_name}:{column_name}"
+            attrs = {
+                "environment": env, "data_type": column.get("data_type", ""),
+                "nullable": bool(column.get("nullable", True)),
+                "primary_key": bool(column.get("primary_key", False)),
+                "sensitive": bool(column.get("sensitive", _is_sensitive(column_name))),
+            }
+            elements.extend((
+                {"local_id": column_key, "layer": "M1", "profile": "software", "kind": "Column",
+                 "name": f"{_pascal(table_name)}{_pascal(column_name)}Column", "status": "candidate",
+                 "uri": f"urn:ontorefactor:model:column:{_safe_name(schema)}:{_safe_name(table_name)}:{_safe_name(column_name)}",
+                 "attributes": attrs},
+                {"local_id": physical_column_key, "layer": "M0", "profile": "software", "kind": "PhysicalColumn",
+                 "name": f"{env}.{schema}.{table_name}.{column_name}", "status": "published",
+                 "uri": f"db://{_safe_name(env)}/{_safe_name(schema)}/{_safe_name(table_name)}/{_safe_name(column_name)}",
+                 "attributes": attrs},
+            ))
+            assertions.extend((
+                {"subject": table_key, "predicate": "CONTAINS", "object": column_key,
+                 "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"},
+                {"subject": physical_key, "predicate": "CONTAINS", "object": physical_column_key,
+                 "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"},
+                {"subject": physical_column_key, "predicate": "INSTANCE_OF", "object": column_key,
+                 "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"},
+            ))
+
+    for api in normalized["parsed"]["apis"]:
+        operation_id = str(api.get("operation_id") or "operation")
+        m1_key = f"m1:api:{operation_id}"
+        m0_key = f"m0:api:{operation_id}"
+        elements.extend((
+            {"local_id": m1_key, "layer": "M1", "profile": "software", "kind": "ApiOperation",
+             "name": _pascal(operation_id), "status": "candidate",
+             "uri": f"urn:ontorefactor:model:api:{_safe_name(operation_id)}",
+             "attributes": {"path": api.get("path"), "method": api.get("method")}},
+            {"local_id": m0_key, "layer": "M0", "profile": "software", "kind": "ApiObservation",
+             "name": f"{api.get('method')} {api.get('path')}", "status": "published",
+             "uri": f"openapi://{_safe_name(source)}/{_safe_name(operation_id)}",
+             "attributes": {"environment": env, "path": api.get("path"), "method": api.get("method")}},
+        ))
+        assertions.append({"subject": m0_key, "predicate": "INSTANCE_OF", "object": m1_key,
+                           "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"})
+
+    for schema in normalized["parsed"]["schemas"]:
+        schema_name = str(schema.get("name", "Schema"))
+        schema_key = f"m1:schema:{schema_name}"
+        elements.append({"local_id": schema_key, "layer": "M1", "profile": "software", "kind": "Schema",
+                         "name": _pascal(schema_name), "status": "candidate",
+                         "uri": f"urn:ontorefactor:model:schema:{_safe_name(schema_name)}",
+                         "attributes": {"properties": len(schema.get("properties") or [])}})
+        for prop in schema.get("properties") or []:
+            prop_name = str(prop.get("name", "property"))
+            prop_key = f"m1:schema-field:{schema_name}:{prop_name}"
+            elements.append({"local_id": prop_key, "layer": "M1", "profile": "software", "kind": "Column",
+                             "name": f"{_pascal(schema_name)}{_pascal(prop_name)}Field", "status": "candidate",
+                             "uri": f"urn:ontorefactor:model:schema-field:{_safe_name(schema_name)}:{_safe_name(prop_name)}",
+                             "attributes": {"data_type": prop.get("data_type"), "sensitive": prop.get("sensitive", False)}})
+            assertions.append({"subject": schema_key, "predicate": "CONTAINS", "object": prop_key,
+                               "assertion_type": "extracted", "confidence": 1.0, "review_status": "accepted"})
+    return {"agent": "asset-discovery", "elements": elements, "assertions": assertions, "issues": []}
+
+
+def infer_semantics(normalized: dict) -> dict:
+    elements: List[dict] = []
+    assertions: List[dict] = []
+    for table in normalized["parsed"]["tables"]:
+        table_name = str(table.get("name", "object"))
+        stem = re.sub(r"^(t_|tbl_|dcp_)", "", table_name, flags=re.IGNORECASE)
+        business_name = _pascal(stem)
+        object_key = f"m1:business-object:{stem}"
+        table_key = f"m1:table:{table.get('schema') or 'default'}:{table_name}"
+        elements.append({
+            "local_id": object_key, "layer": "M1", "profile": "business", "kind": "BusinessObject",
+            "name": business_name, "description": f"由表名 {table_name} 推断的候选业务对象",
+            "status": "candidate", "uri": f"urn:ontorefactor:model:business-object:{_safe_name(stem)}",
+            "attributes": {"inference": "table-name", "source_table": table_name},
+        })
+        assertions.append({
+            "subject": table_key, "predicate": "REPRESENTS", "object": object_key,
+            "assertion_type": "inferred", "confidence": 0.72, "review_status": "pending",
+        })
+    return {"agent": "business-semantics", "elements": elements, "assertions": assertions, "issues": []}
+
+
+def analyze_controls(normalized: dict) -> dict:
+    issues: List[dict] = []
+    for table in normalized["parsed"]["tables"]:
+        table_name = str(table.get("name", "unnamed"))
+        schema = str(table.get("schema") or "default")
+        column_names = {str(column.get("name", "")).lower() for column in table.get("columns") or []}
+        if ("tenant" in table_name.lower() or table_name.lower().startswith("dcp_")) and "tenant_id" not in column_names:
+            issues.append({
+                "code": "TENANT_ID_MISSING", "severity": "critical",
+                "title": f"{table_name} 缺少 tenant_id",
+                "description": "租户域表缺少租户隔离字段,必须由 Owner 确认或整改",
+                "target_local_id": f"m1:table:{schema}:{table_name}",
+            })
+        for column in table.get("columns") or []:
+            if column.get("sensitive") or _is_sensitive(str(column.get("name", ""))):
+                issues.append({
+                    "code": "SENSITIVE_FIELD_REVIEW", "severity": "high",
+                    "title": f"敏感字段待策略确认:{table_name}.{column.get('name')}",
+                    "description": "需要补充分类分级、访问策略、脱敏方式和责任人",
+                    "target_local_id": f"m1:column:{schema}:{table_name}:{column.get('name')}",
+                })
+    for schema in normalized["parsed"]["schemas"]:
+        for prop in schema.get("properties") or []:
+            if prop.get("sensitive"):
+                issues.append({
+                    "code": "SENSITIVE_API_FIELD", "severity": "high",
+                    "title": f"API Schema 敏感字段:{schema.get('name')}.{prop.get('name')}",
+                    "description": "需要核对 API 消费者权限和响应脱敏策略",
+                    "target_local_id": f"m1:schema-field:{schema.get('name')}:{prop.get('name')}",
+                })
+    return {"agent": "governance-controls", "elements": [], "assertions": [], "issues": issues}
+
+
+def synthesize(branches: tuple) -> dict:
+    elements: Dict[str, dict] = {}
+    assertions: Dict[tuple, dict] = {}
+    issues: Dict[tuple, dict] = {}
+    agents = []
+    for branch in branches:
+        agents.append(branch["agent"])
+        for element in branch["elements"]:
+            elements[element["local_id"]] = element
+        for assertion in branch["assertions"]:
+            key = (assertion["subject"], assertion["predicate"], assertion["object"])
+            assertions[key] = assertion
+        for issue in branch["issues"]:
+            issues[(issue["code"], issue["target_local_id"])] = issue
+    return {
+        "agents": agents,
+        "elements": list(elements.values()),
+        "assertions": list(assertions.values()),
+        "issues": list(issues.values()),
+        "summary": {
+            "elements": len(elements), "assertions": len(assertions), "issues": len(issues),
+        },
+    }
+
+
+def _valid_result(result: Any) -> bool:
+    return isinstance(result, dict) and all(key in result for key in ("elements", "assertions", "issues", "summary"))
+
+
+def build_governance_agent():
+    """Build the typed execution graph used by the governance ingest API."""
+    normalize = Tool("governance-source-normalizer", normalize_source)
+    parallel_analysis = Par(
+        Tool("asset-discovery-agent", discover_assets),
+        Tool("business-semantics-agent", infer_semantics),
+        Tool("governance-controls-agent", analyze_controls),
+    )
+    merge = Tool("governance-synthesis-agent", synthesize)
+    return Guard(normalize >> parallel_analysis >> merge, validator=_valid_result, retry=0)
+
+
+def execute_governance_agent(
+    db, tenant_id: str, project_id: str, payload: Dict[str, Any], *, persist: bool = True,
+) -> dict:
+    """Execute the LambdAgent graph and optionally persist its proposals/facts."""
+    run_id = gen_id("gar_")
+    now = now_utc()
+    raw = str(payload.get("content", ""))
+    source_type = str(payload.get("source_type", ""))
+    db.execute(
+        "INSERT INTO governance_agent_runs "
+        "(id,tenant_id,project_id,source_type,input_hash,status,result_json,trace_json,error,created_at) "
+        "VALUES (?,?,?,?,?,'running','{}','[]','',?)",
+        (run_id, tenant_id, project_id, source_type, hashlib.sha256(raw.encode("utf-8")).hexdigest(), now),
+    )
+    db.commit()
+    ctx = Context(run_id=run_id)
+    try:
+        result = build_governance_agent().apply(payload, ctx)
+        persisted = {"elements": 0, "assertions": 0, "issues": 0}
+        if persist:
+            evidence = create_evidence(db, tenant_id, project_id, {
+                "kind": "SourceArtifact", "title": str(payload.get("source_name") or f"智能体输入 {source_type}"),
+                "uri": f"evidence://agent-run/{run_id}", "content": raw,
+                "source": str(payload.get("source_name") or "inline"),
+                "metadata": {"agent_run_id": run_id, "source_type": source_type},
+            })
+            id_map: Dict[str, str] = {}
+            for element in result["elements"]:
+                saved = upsert_element(db, tenant_id, project_id, {
+                    **element, "source": f"agent-run:{run_id}",
+                })
+                id_map[element["local_id"]] = saved["id"]
+                persisted["elements"] += 1
+            for assertion in result["assertions"]:
+                if assertion["subject"] not in id_map or assertion["object"] not in id_map:
+                    continue
+                create_assertion(db, tenant_id, project_id, {
+                    "subject_id": id_map[assertion["subject"]],
+                    "predicate": assertion["predicate"],
+                    "object_id": id_map[assertion["object"]],
+                    "assertion_type": assertion["assertion_type"],
+                    "confidence": assertion["confidence"],
+                    "review_status": assertion["review_status"],
+                    "evidence_ids": [evidence["id"]],
+                    "generated_by": f"lambdagent:governance-ingest@1.0#{run_id}",
+                })
+                persisted["assertions"] += 1
+            issue_now = now_utc()
+            for issue in result["issues"]:
+                target_id = id_map.get(issue["target_local_id"], "")
+                db.execute(
+                    "INSERT INTO governance_issues "
+                    "(id,tenant_id,project_id,rule_id,target_id,assertion_id,severity,title,description,"
+                    "status,evidence_ids,assigned_to,created_at,updated_at) "
+                    "VALUES (?,?,?,?,?,'',?,?,?,'open',?,'',?,?)",
+                    (
+                        gen_id("gi_"), tenant_id, project_id, f"agent:{issue['code']}", target_id,
+                        issue["severity"], issue["title"], issue["description"],
+                        json.dumps([evidence["id"]]), issue_now, issue_now,
+                    ),
+                )
+                persisted["issues"] += 1
+            db.commit()
+            validation = validate_project(db, tenant_id, project_id, persist=True)
+        else:
+            validation = None
+        traces = [asdict(item) for item in ctx.trace]
+        response = {
+            "run_id": run_id, "status": "completed", "result": result,
+            "persisted": persisted, "validation": validation,
+            "trace": traces, "overview": overview(db, tenant_id, project_id) if persist else None,
+        }
+        db.execute(
+            "UPDATE governance_agent_runs SET status='completed',result_json=?,trace_json=?,completed_at=? WHERE id=?",
+            (json.dumps(response, ensure_ascii=False, default=str), json.dumps(traces, ensure_ascii=False, default=str), now_utc(), run_id),
+        )
+        db.commit()
+        return response
+    except Exception as exc:
+        db.execute(
+            "UPDATE governance_agent_runs SET status='failed',error=?,trace_json=?,completed_at=? WHERE id=?",
+            (str(exc)[:1000], json.dumps([asdict(item) for item in ctx.trace], ensure_ascii=False, default=str), now_utc(), run_id),
+        )
+        db.commit()
+        raise
+
+
+def list_agent_runs(db, tenant_id: str, project_id: str, limit: int = 50) -> List[dict]:
+    values = db.fetchall(
+        "SELECT * FROM governance_agent_runs WHERE tenant_id=? AND project_id=? "
+        "ORDER BY created_at DESC LIMIT ?",
+        (tenant_id, project_id, min(max(limit, 1), 200)),
+    )
+    result = []
+    for value in values:
+        value["result_json"] = json.loads(value.get("result_json") or "{}")
+        value["trace_json"] = json.loads(value.get("trace_json") or "[]")
+        result.append(value)
+    return result
+

+ 853 - 0
agentpaas/src/agentpaas/governance/service.py

@@ -0,0 +1,853 @@
+"""Four-level ontology and data-governance application service.
+
+The storage model is intentionally graph-shaped but uses the existing SQLite
+abstraction so a desktop deployment works without an external graph database.
+Elements are graph nodes; important relations are reified assertions carrying
+provenance, evidence, validity and review state.  This keeps the API portable
+to RDF/Neo4j later without making either one a prerequisite for the MVP.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+from collections import defaultdict, deque
+from typing import Any, Dict, Iterable, List, Optional
+
+from agentpaas.db.models import gen_id, now_utc
+
+
+LAYERS = ("M3", "M2", "M1", "M0")
+PROFILES = ("meta", "upper", "business", "software", "governance", "cross")
+ELEMENT_STATUSES = ("candidate", "published", "deprecated", "retired")
+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}
+_JSON_FIELDS = {
+    "ontology_elements": ("attributes",),
+    "governance_evidence": ("metadata",),
+    "governance_assertions": ("evidence_ids",),
+    "governance_rules": ("expression",),
+    "governance_issues": ("evidence_ids",),
+    "governance_agent_runs": ("result_json", "trace_json"),
+}
+
+
+class GovernanceError(ValueError):
+    """A domain validation or tenant-scope failure."""
+
+    def __init__(self, message: str, *, code: str = "INVALID_GOVERNANCE_DATA", status: int = 400):
+        super().__init__(message)
+        self.code = code
+        self.status = status
+
+
+def _json_load(value: Any, default: Any) -> Any:
+    if value in (None, ""):
+        return default
+    if isinstance(value, (dict, list)):
+        return value
+    try:
+        return json.loads(value)
+    except (TypeError, ValueError):
+        return default
+
+
+def _row(table: str, value: Optional[dict]) -> Optional[dict]:
+    if value is None:
+        return None
+    out = dict(value)
+    for field in _JSON_FIELDS.get(table, ()):
+        default = [] if field.endswith("_ids") or field == "trace_json" else {}
+        out[field] = _json_load(out.get(field), default)
+    if table == "governance_rules":
+        out["enabled"] = bool(out.get("enabled"))
+    return out
+
+
+def _rows(table: str, values: Iterable[dict]) -> List[dict]:
+    return [_row(table, value) for value in values]  # type: ignore[list-item]
+
+
+def _dump(value: Any) -> str:
+    return json.dumps(value, ensure_ascii=False, sort_keys=True)
+
+
+def require_project(db, tenant_id: str, project_id: str) -> dict:
+    project = db.fetchone(
+        "SELECT * FROM governance_projects WHERE id=? AND tenant_id=?",
+        (project_id, tenant_id),
+    )
+    if not project:
+        raise GovernanceError("本体项目不存在", code="PROJECT_NOT_FOUND", status=404)
+    return project
+
+
+def create_project(db, tenant_id: str, data: Dict[str, Any]) -> dict:
+    name = str(data.get("name", "")).strip()
+    namespace = str(data.get("namespace", "")).strip().rstrip("#/")
+    if not name or not namespace:
+        raise GovernanceError("name 和 namespace 不能为空")
+    existing = db.fetchone(
+        "SELECT * FROM governance_projects WHERE tenant_id=? AND namespace=?",
+        (tenant_id, namespace),
+    )
+    if existing:
+        return existing
+    project_id = gen_id("gp_")
+    now = now_utc()
+    db.execute(
+        "INSERT INTO governance_projects "
+        "(id,tenant_id,name,description,namespace,version,status,created_at,updated_at) "
+        "VALUES (?,?,?,?,?,?,?,?,?)",
+        (
+            project_id, tenant_id, name, str(data.get("description", "")), namespace,
+            str(data.get("version", "1.0.0")), str(data.get("status", "active")), now, now,
+        ),
+    )
+    db.commit()
+    return require_project(db, tenant_id, project_id)
+
+
+def list_projects(db, tenant_id: str) -> List[dict]:
+    return db.fetchall(
+        "SELECT * FROM governance_projects WHERE tenant_id=? ORDER BY updated_at DESC",
+        (tenant_id,),
+    )
+
+
+def upsert_element(db, tenant_id: str, project_id: str, data: Dict[str, Any]) -> dict:
+    project = require_project(db, tenant_id, project_id)
+    layer = str(data.get("layer", "")).upper()
+    profile = str(data.get("profile", "")).lower()
+    status = str(data.get("status", "candidate")).lower()
+    name = str(data.get("name", "")).strip()
+    kind = str(data.get("kind", "")).strip()
+    if layer not in LAYERS:
+        raise GovernanceError(f"未知元建模层级: {layer}")
+    if profile not in PROFILES:
+        raise GovernanceError(f"未知 Profile: {profile}")
+    if status not in ELEMENT_STATUSES:
+        raise GovernanceError(f"未知元素状态: {status}")
+    if not name or not kind:
+        raise GovernanceError("元素 name 和 kind 不能为空")
+    if layer == "M3" and profile != "meta":
+        raise GovernanceError("M3 元素必须属于 meta Profile")
+    if layer == "M2" and profile == "meta":
+        raise GovernanceError("M2 元素应属于 upper 或领域 Profile")
+
+    uri = str(data.get("uri", "")).strip()
+    if not uri:
+        uri = f"{project['namespace']}/{layer.lower()}/{profile}/{name}"
+    existing = db.fetchone(
+        "SELECT id FROM ontology_elements WHERE tenant_id=? AND project_id=? AND uri=?",
+        (tenant_id, project_id, uri),
+    )
+    now = now_utc()
+    attrs = _dump(data.get("attributes") or {})
+    if existing:
+        element_id = existing["id"]
+        db.execute(
+            "UPDATE ontology_elements SET layer=?,profile=?,kind=?,name=?,description=?,"
+            "status=?,version=?,attributes=?,source=?,updated_at=? "
+            "WHERE id=? AND tenant_id=?",
+            (
+                layer, profile, kind, name, str(data.get("description", "")), status,
+                str(data.get("version", "1.0.0")), attrs, str(data.get("source", "")),
+                now, element_id, tenant_id,
+            ),
+        )
+    else:
+        element_id = str(data.get("id") or gen_id("oe_"))
+        db.execute(
+            "INSERT INTO ontology_elements "
+            "(id,tenant_id,project_id,uri,layer,profile,kind,name,description,status,"
+            "version,attributes,source,created_at,updated_at) "
+            "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+            (
+                element_id, tenant_id, project_id, uri, layer, profile, kind, name,
+                str(data.get("description", "")), status,
+                str(data.get("version", "1.0.0")), attrs,
+                str(data.get("source", "")), now, now,
+            ),
+        )
+    db.execute("UPDATE governance_projects SET updated_at=? WHERE id=?", (now, project_id))
+    db.commit()
+    return get_element(db, tenant_id, element_id)
+
+
+def get_element(db, tenant_id: str, element_id: str) -> dict:
+    value = db.fetchone(
+        "SELECT * FROM ontology_elements WHERE id=? AND tenant_id=?", (element_id, tenant_id)
+    )
+    if not value:
+        raise GovernanceError("本体元素不存在", code="ELEMENT_NOT_FOUND", status=404)
+    return _row("ontology_elements", value)  # type: ignore[return-value]
+
+
+def list_elements(
+    db, tenant_id: str, project_id: str, *, layer: str = "", profile: str = "",
+    status: str = "", query: str = "", limit: int = 200, offset: int = 0,
+) -> Dict[str, Any]:
+    require_project(db, tenant_id, project_id)
+    where = ["tenant_id=?", "project_id=?"]
+    params: List[Any] = [tenant_id, project_id]
+    if layer:
+        where.append("layer=?")
+        params.append(layer.upper())
+    if profile:
+        where.append("profile=?")
+        params.append(profile.lower())
+    if status:
+        where.append("status=?")
+        params.append(status.lower())
+    if query:
+        where.append("(name LIKE ? OR uri LIKE ? OR description LIKE ?)")
+        like = f"%{query}%"
+        params.extend((like, like, like))
+    clause = " AND ".join(where)
+    total = db.fetchone(f"SELECT COUNT(*) AS n FROM ontology_elements WHERE {clause}", tuple(params))["n"]
+    bounded = min(max(int(limit), 1), 500)
+    values = db.fetchall(
+        f"SELECT * FROM ontology_elements WHERE {clause} "
+        "ORDER BY CASE layer WHEN 'M3' THEN 0 WHEN 'M2' THEN 1 WHEN 'M1' THEN 2 ELSE 3 END, "
+        "profile,name LIMIT ? OFFSET ?",
+        tuple(params + [bounded, max(int(offset), 0)]),
+    )
+    return {"items": _rows("ontology_elements", values), "total": total}
+
+
+def create_evidence(db, tenant_id: str, project_id: str, data: Dict[str, Any]) -> dict:
+    require_project(db, tenant_id, project_id)
+    title = str(data.get("title", "")).strip()
+    kind = str(data.get("kind", "SourceArtifact")).strip()
+    if not title:
+        raise GovernanceError("证据 title 不能为空")
+    uri = str(data.get("uri", "")).strip()
+    if uri:
+        existing = db.fetchone(
+            "SELECT * FROM governance_evidence WHERE tenant_id=? AND project_id=? AND uri=?",
+            (tenant_id, project_id, uri),
+        )
+        if existing:
+            return _row("governance_evidence", existing)  # type: ignore[return-value]
+    content = str(data.get("content", ""))
+    evidence_id = gen_id("ev_")
+    now = now_utc()
+    db.execute(
+        "INSERT INTO governance_evidence "
+        "(id,tenant_id,project_id,kind,uri,title,content,content_hash,source,observed_at,"
+        "valid_from,valid_to,metadata,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+        (
+            evidence_id, tenant_id, project_id, kind, uri, title, content,
+            str(data.get("content_hash") or hashlib.sha256(content.encode("utf-8")).hexdigest()),
+            str(data.get("source", "")), str(data.get("observed_at") or now),
+            data.get("valid_from") or now, data.get("valid_to"),
+            _dump(data.get("metadata") or {}), now,
+        ),
+    )
+    db.commit()
+    return _row(
+        "governance_evidence",
+        db.fetchone("SELECT * FROM governance_evidence WHERE id=?", (evidence_id,)),
+    )  # type: ignore[return-value]
+
+
+def list_evidence(db, tenant_id: str, project_id: str, limit: int = 200) -> List[dict]:
+    require_project(db, tenant_id, project_id)
+    return _rows(
+        "governance_evidence",
+        db.fetchall(
+            "SELECT * FROM governance_evidence WHERE tenant_id=? AND project_id=? "
+            "ORDER BY observed_at DESC LIMIT ?",
+            (tenant_id, project_id, min(max(limit, 1), 500)),
+        ),
+    )
+
+
+def create_assertion(db, tenant_id: str, project_id: str, data: Dict[str, Any]) -> dict:
+    require_project(db, tenant_id, project_id)
+    subject = get_element(db, tenant_id, str(data.get("subject_id", "")))
+    if subject["project_id"] != project_id:
+        raise GovernanceError("断言 subject 不属于当前项目")
+    object_id = str(data.get("object_id") or "")
+    if object_id:
+        obj = get_element(db, tenant_id, object_id)
+        if obj["project_id"] != project_id:
+            raise GovernanceError("断言 object 不属于当前项目")
+    predicate = str(data.get("predicate", "")).strip().upper()
+    assertion_type = str(data.get("assertion_type", "inferred")).lower()
+    review_status = str(data.get("review_status", "pending")).lower()
+    confidence = float(data.get("confidence", 0.0))
+    if not predicate:
+        raise GovernanceError("predicate 不能为空")
+    if assertion_type not in ASSERTION_TYPES:
+        raise GovernanceError(f"未知断言类型: {assertion_type}")
+    if review_status not in REVIEW_STATUSES:
+        raise GovernanceError(f"未知审核状态: {review_status}")
+    if not 0.0 <= confidence <= 1.0:
+        raise GovernanceError("confidence 必须在 0 到 1 之间")
+    evidence_ids = list(dict.fromkeys(data.get("evidence_ids") or []))
+    for evidence_id in evidence_ids:
+        if not db.fetchone(
+            "SELECT id FROM governance_evidence WHERE id=? AND tenant_id=? AND project_id=?",
+            (evidence_id, tenant_id, project_id),
+        ):
+            raise GovernanceError(f"证据不存在: {evidence_id}")
+
+    existing = db.fetchone(
+        "SELECT * FROM governance_assertions WHERE tenant_id=? AND project_id=? "
+        "AND subject_id=? AND predicate=? AND COALESCE(object_id,'')=? "
+        "AND COALESCE(object_value,'')=?",
+        (
+            tenant_id, project_id, subject["id"], predicate, object_id,
+            str(data.get("object_value", "")),
+        ),
+    )
+    if existing:
+        return _row("governance_assertions", existing)  # type: ignore[return-value]
+    assertion_id = gen_id("as_")
+    now = now_utc()
+    db.execute(
+        "INSERT INTO governance_assertions "
+        "(id,tenant_id,project_id,subject_id,predicate,object_id,object_value,"
+        "assertion_type,confidence,evidence_ids,generated_by,valid_from,valid_to,"
+        "review_status,reviewer,review_note,created_at,updated_at) "
+        "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+        (
+            assertion_id, tenant_id, project_id, subject["id"], predicate,
+            object_id or None, str(data.get("object_value", "")), assertion_type,
+            confidence, _dump(evidence_ids), str(data.get("generated_by", "")),
+            data.get("valid_from") or now, data.get("valid_to"), review_status,
+            str(data.get("reviewer", "")), str(data.get("review_note", "")), now, now,
+        ),
+    )
+    db.commit()
+    return get_assertion(db, tenant_id, assertion_id)
+
+
+def get_assertion(db, tenant_id: str, assertion_id: str) -> dict:
+    value = db.fetchone(
+        "SELECT * FROM governance_assertions WHERE id=? AND tenant_id=?",
+        (assertion_id, tenant_id),
+    )
+    if not value:
+        raise GovernanceError("断言不存在", code="ASSERTION_NOT_FOUND", status=404)
+    return _row("governance_assertions", value)  # type: ignore[return-value]
+
+
+def list_assertions(
+    db, tenant_id: str, project_id: str, *, review_status: str = "",
+    assertion_type: str = "", limit: int = 300,
+) -> List[dict]:
+    require_project(db, tenant_id, project_id)
+    where = ["a.tenant_id=?", "a.project_id=?"]
+    params: List[Any] = [tenant_id, project_id]
+    if review_status:
+        where.append("a.review_status=?")
+        params.append(review_status)
+    if assertion_type:
+        where.append("a.assertion_type=?")
+        params.append(assertion_type)
+    params.append(min(max(limit, 1), 500))
+    values = db.fetchall(
+        "SELECT a.*, s.name AS subject_name, o.name AS object_name "
+        "FROM governance_assertions a "
+        "JOIN ontology_elements s ON s.id=a.subject_id "
+        "LEFT JOIN ontology_elements o ON o.id=a.object_id "
+        f"WHERE {' AND '.join(where)} ORDER BY a.updated_at DESC LIMIT ?",
+        tuple(params),
+    )
+    return _rows("governance_assertions", values)
+
+
+def review_assertion(
+    db, tenant_id: str, assertion_id: str, status: str, reviewer: str, note: str = "",
+) -> dict:
+    assertion = get_assertion(db, tenant_id, assertion_id)
+    status = status.lower()
+    if status not in ("accepted", "rejected", "expired"):
+        raise GovernanceError("审核结果必须是 accepted、rejected 或 expired")
+    if not reviewer.strip():
+        raise GovernanceError("reviewer 不能为空")
+    now = now_utc()
+    db.execute(
+        "UPDATE governance_assertions SET review_status=?,reviewer=?,review_note=?,updated_at=? "
+        "WHERE id=? AND tenant_id=?",
+        (status, reviewer.strip(), note, now, assertion_id, tenant_id),
+    )
+    db.commit()
+    return get_assertion(db, tenant_id, assertion_id)
+
+
+def upsert_rule(db, tenant_id: str, project_id: str, data: Dict[str, Any]) -> dict:
+    require_project(db, tenant_id, project_id)
+    name = str(data.get("name", "")).strip()
+    rule_type = str(data.get("rule_type", "integrity")).strip()
+    severity = str(data.get("severity", "medium")).lower()
+    if not name:
+        raise GovernanceError("规则 name 不能为空")
+    if severity not in SEVERITIES:
+        raise GovernanceError(f"未知严重级别: {severity}")
+    existing = db.fetchone(
+        "SELECT id FROM governance_rules WHERE tenant_id=? AND project_id=? AND name=?",
+        (tenant_id, project_id, name),
+    )
+    now = now_utc()
+    if existing:
+        rule_id = existing["id"]
+        db.execute(
+            "UPDATE governance_rules SET description=?,rule_type=?,target_layer=?,"
+            "target_profile=?,target_kind=?,expression=?,severity=?,enabled=?,version=?,updated_at=? "
+            "WHERE id=? AND tenant_id=?",
+            (
+                str(data.get("description", "")), rule_type,
+                str(data.get("target_layer", "")), str(data.get("target_profile", "")),
+                str(data.get("target_kind", "")), _dump(data.get("expression") or {}),
+                severity, 1 if data.get("enabled", True) else 0,
+                str(data.get("version", "1.0.0")), now, rule_id, tenant_id,
+            ),
+        )
+    else:
+        rule_id = gen_id("gr_")
+        db.execute(
+            "INSERT INTO governance_rules "
+            "(id,tenant_id,project_id,name,description,rule_type,target_layer,target_profile,"
+            "target_kind,expression,severity,enabled,version,created_at,updated_at) "
+            "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
+            (
+                rule_id, tenant_id, project_id, name, str(data.get("description", "")),
+                rule_type, str(data.get("target_layer", "")),
+                str(data.get("target_profile", "")), str(data.get("target_kind", "")),
+                _dump(data.get("expression") or {}), severity,
+                1 if data.get("enabled", True) else 0,
+                str(data.get("version", "1.0.0")), now, now,
+            ),
+        )
+    db.commit()
+    return _row(
+        "governance_rules", db.fetchone("SELECT * FROM governance_rules WHERE id=?", (rule_id,))
+    )  # type: ignore[return-value]
+
+
+def list_rules(db, tenant_id: str, project_id: str) -> List[dict]:
+    require_project(db, tenant_id, project_id)
+    return _rows(
+        "governance_rules",
+        db.fetchall(
+            "SELECT * FROM governance_rules WHERE tenant_id=? AND project_id=? ORDER BY severity DESC,name",
+            (tenant_id, project_id),
+        ),
+    )
+
+
+def _finding(code: str, severity: str, title: str, description: str, target_id: str = "", assertion_id: str = "") -> dict:
+    return {
+        "code": code,
+        "severity": severity,
+        "title": title,
+        "description": description,
+        "target_id": target_id,
+        "assertion_id": assertion_id,
+    }
+
+
+def validate_project(db, tenant_id: str, project_id: str, *, persist: bool = True) -> dict:
+    """Evaluate the first deterministic SHACL-equivalent governance gates."""
+    require_project(db, tenant_id, project_id)
+    elements = _rows(
+        "ontology_elements",
+        db.fetchall(
+            "SELECT * FROM ontology_elements WHERE tenant_id=? AND project_id=?",
+            (tenant_id, project_id),
+        ),
+    )
+    assertions = _rows(
+        "governance_assertions",
+        db.fetchall(
+            "SELECT * FROM governance_assertions WHERE tenant_id=? AND project_id=?",
+            (tenant_id, project_id),
+        ),
+    )
+    by_id = {item["id"]: item for item in elements}
+    outgoing: Dict[str, List[dict]] = defaultdict(list)
+    incoming: Dict[str, List[dict]] = defaultdict(list)
+    findings: List[dict] = []
+    for assertion in assertions:
+        outgoing[assertion["subject_id"]].append(assertion)
+        if assertion.get("object_id"):
+            incoming[assertion["object_id"]].append(assertion)
+
+        subject = by_id.get(assertion["subject_id"])
+        obj = by_id.get(assertion.get("object_id"))
+        predicate = assertion["predicate"]
+        if predicate == "INSTANCE_OF" and subject and obj:
+            if _LAYER_RANK[obj["layer"]] != _LAYER_RANK[subject["layer"]] + 1:
+                findings.append(_finding(
+                    "LAYER_INSTANCE_MISMATCH", "high", "instanceOf 层级不连续",
+                    f"{subject['name']}({subject['layer']}) 必须实例化相邻上层,当前指向 {obj['name']}({obj['layer']})",
+                    subject["id"], assertion["id"],
+                ))
+        if predicate in ("SUB_TYPE_OF", "SUBCLASS_OF") and subject and obj and subject["layer"] != obj["layer"]:
+            findings.append(_finding(
+                "SUBTYPE_CROSSES_LAYER", "high", "subTypeOf 不能跨元层",
+                f"{subject['name']} 与 {obj['name']} 不在同一层", subject["id"], assertion["id"],
+            ))
+        if assertion["assertion_type"] == "inferred" and predicate in {
+            "DELETE", "GRANT", "AUTHORIZE", "DOWNGRADE", "PUBLISH", "RETIRE",
+        }:
+            findings.append(_finding(
+                "INFERRED_HIGH_RISK_ACTION", "critical", "推断断言不能触发高风险动作",
+                f"{predicate} 必须经过确定性证据与人工审批", assertion["subject_id"], assertion["id"],
+            ))
+        if assertion["review_status"] == "accepted" and assertion["assertion_type"] != "declared":
+            if not assertion.get("evidence_ids") or not assertion.get("generated_by"):
+                findings.append(_finding(
+                    "ACCEPTED_ASSERTION_WITHOUT_PROVENANCE", "high", "正式断言缺少证据或生成者",
+                    "accepted 的 extracted/observed/inferred 断言必须关联证据并记录 generated_by",
+                    assertion["subject_id"], assertion["id"],
+                ))
+
+    for element in elements:
+        if element["layer"] == "M3" and element["profile"] != "meta":
+            findings.append(_finding(
+                "M3_DOMAIN_LEAK", "critical", "M3 混入领域概念",
+                "M3 只能包含稳定的 meta 建模内核", element["id"],
+            ))
+        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"]
+        if element["layer"] == "M0" and element["status"] == "published":
+            if 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["profile"] == "business":
+            if element["kind"] == "BusinessCapability" and not any(a["predicate"] == "REALIZED_BY" for a in accepted_out):
+                findings.append(_finding(
+                    "CAPABILITY_NOT_REALIZED", "high", "业务能力缺少软件实现",
+                    f"{element['name']} 至少需要一个 REALIZED_BY 关系", element["id"],
+                ))
+            if element["kind"] == "BusinessObject" and not any(a["predicate"] == "REPRESENTS" for a in accepted_in):
+                findings.append(_finding(
+                    "BUSINESS_OBJECT_NOT_REPRESENTED", "high", "业务对象缺少技术表达",
+                    f"{element['name']} 至少应由类、表、API Schema 或事件契约表达", element["id"],
+                ))
+        attrs = element.get("attributes") or {}
+        if element["profile"] == "software" and attrs.get("environment") == "production":
+            if not any(a["predicate"] == "OWNED_BY" for a in accepted_out):
+                findings.append(_finding(
+                    "PRODUCTION_ASSET_WITHOUT_OWNER", "critical", "生产资产缺少责任人",
+                    f"{element['name']} 必须 OWNED_BY 某个 Owner/Steward", element["id"],
+                ))
+        if element["kind"] in ("Column", "PhysicalColumn") and attrs.get("sensitive"):
+            policy_links = [a for a in accepted_out if a["predicate"] == "GOVERNED_BY"]
+            if not any(by_id.get(a.get("object_id"), {}).get("kind") in ("AccessPolicy", "SecurityPolicy") for a in policy_links):
+                findings.append(_finding(
+                    "SENSITIVE_FIELD_WITHOUT_POLICY", "critical", "敏感字段缺少访问策略",
+                    f"{element['name']} 必须关联分类分级和访问策略", element["id"],
+                ))
+
+    if persist:
+        db.execute(
+            "DELETE FROM governance_issues WHERE tenant_id=? AND project_id=? AND rule_id LIKE 'validation:%'",
+            (tenant_id, project_id),
+        )
+        now = now_utc()
+        for finding in findings:
+            db.execute(
+                "INSERT INTO governance_issues "
+                "(id,tenant_id,project_id,rule_id,target_id,assertion_id,severity,title,description,"
+                "status,evidence_ids,assigned_to,created_at,updated_at) "
+                "VALUES (?,?,?,?,?,?,?,?,?,'open','[]','',?,?)",
+                (
+                    gen_id("gi_"), tenant_id, project_id, f"validation:{finding['code']}",
+                    finding["target_id"], finding["assertion_id"], finding["severity"],
+                    finding["title"], finding["description"], now, now,
+                ),
+            )
+        db.commit()
+    severity_counts = {severity: 0 for severity in SEVERITIES}
+    for finding in findings:
+        severity_counts[finding["severity"]] += 1
+    return {
+        "valid": not any(item["severity"] in ("critical", "high") for item in findings),
+        "findings": findings,
+        "counts": severity_counts,
+        "checked": {"elements": len(elements), "assertions": len(assertions)},
+    }
+
+
+def list_issues(db, tenant_id: str, project_id: str, status: str = "open") -> List[dict]:
+    require_project(db, tenant_id, project_id)
+    where = "tenant_id=? AND project_id=?"
+    params: List[Any] = [tenant_id, project_id]
+    if status:
+        where += " AND status=?"
+        params.append(status)
+    return _rows(
+        "governance_issues",
+        db.fetchall(
+            f"SELECT * FROM governance_issues WHERE {where} "
+            "ORDER BY CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 "
+            "WHEN 'medium' THEN 2 ELSE 3 END, updated_at DESC",
+            tuple(params),
+        ),
+    )
+
+
+def update_issue(db, tenant_id: str, issue_id: str, status: str, assigned_to: str = "") -> dict:
+    if status not in ("open", "acknowledged", "resolved", "waived"):
+        raise GovernanceError("未知问题状态")
+    existing = db.fetchone(
+        "SELECT * FROM governance_issues WHERE id=? AND tenant_id=?", (issue_id, tenant_id)
+    )
+    if not existing:
+        raise GovernanceError("治理问题不存在", code="ISSUE_NOT_FOUND", status=404)
+    now = now_utc()
+    db.execute(
+        "UPDATE governance_issues SET status=?,assigned_to=?,updated_at=?,resolved_at=? "
+        "WHERE id=? AND tenant_id=?",
+        (status, assigned_to, now, now if status in ("resolved", "waived") else None, issue_id, tenant_id),
+    )
+    db.commit()
+    return _row(
+        "governance_issues",
+        db.fetchone("SELECT * FROM governance_issues WHERE id=?", (issue_id,)),
+    )  # type: ignore[return-value]
+
+
+def graph(db, tenant_id: str, project_id: str, *, include_pending: bool = True, limit: int = 500) -> dict:
+    elements = list_elements(db, tenant_id, project_id, limit=limit)["items"]
+    ids = {element["id"] for element in elements}
+    statuses = ("accepted", "pending") if include_pending else ("accepted",)
+    placeholders = ",".join("?" for _ in statuses)
+    values = db.fetchall(
+        "SELECT a.*, s.name AS subject_name, o.name AS object_name "
+        "FROM governance_assertions a "
+        "JOIN ontology_elements s ON s.id=a.subject_id AND s.tenant_id=a.tenant_id "
+        "LEFT JOIN ontology_elements o ON o.id=a.object_id AND o.tenant_id=a.tenant_id "
+        "WHERE a.tenant_id=? AND a.project_id=? "
+        f"AND a.review_status IN ({placeholders}) ORDER BY a.created_at LIMIT ?",
+        (tenant_id, project_id, *statuses, min(max(limit * 2, 1), 1000)),
+    )
+    edges = [
+        item for item in _rows("governance_assertions", values)
+        if item["subject_id"] in ids and (not item.get("object_id") or item["object_id"] in ids)
+    ]
+    return {"nodes": elements, "edges": edges}
+
+
+def impact(db, tenant_id: str, element_id: str, max_depth: int = 4) -> dict:
+    center = get_element(db, tenant_id, element_id)
+    project_id = center["project_id"]
+    elements = list_elements(db, tenant_id, project_id, limit=500)["items"]
+    by_id = {element["id"]: element for element in elements}
+    assertions = list_assertions(db, tenant_id, project_id, review_status="accepted", limit=500)
+    adjacency: Dict[str, List[tuple[str, dict, str]]] = defaultdict(list)
+    for edge in assertions:
+        if not edge.get("object_id"):
+            continue
+        adjacency[edge["subject_id"]].append((edge["object_id"], edge, "out"))
+        adjacency[edge["object_id"]].append((edge["subject_id"], edge, "in"))
+    depth_limit = min(max(int(max_depth), 1), 8)
+    queue = deque([(element_id, 0)])
+    visited = {element_id: 0}
+    used_edges: Dict[str, dict] = {}
+    while queue:
+        current, depth = queue.popleft()
+        if depth >= depth_limit:
+            continue
+        for neighbour, edge, direction in adjacency[current]:
+            used_edges[edge["id"]] = {**edge, "traversed_from": current, "direction": direction}
+            if neighbour not in visited:
+                visited[neighbour] = depth + 1
+                queue.append((neighbour, depth + 1))
+    nodes = [{**by_id[node_id], "distance": distance} for node_id, distance in visited.items() if node_id in by_id]
+    nodes.sort(key=lambda item: (item["distance"], item["layer"], item["name"]))
+    return {"center": center, "nodes": nodes, "edges": list(used_edges.values()), "max_depth": depth_limit}
+
+
+def overview(db, tenant_id: str, project_id: str) -> dict:
+    project = require_project(db, tenant_id, project_id)
+    layer_counts = {layer: 0 for layer in LAYERS}
+    for row in db.fetchall(
+        "SELECT layer,COUNT(*) AS n FROM ontology_elements WHERE tenant_id=? AND project_id=? GROUP BY layer",
+        (tenant_id, project_id),
+    ):
+        layer_counts[row["layer"]] = row["n"]
+    profile_counts = {profile: 0 for profile in PROFILES}
+    for row in db.fetchall(
+        "SELECT profile,COUNT(*) AS n FROM ontology_elements WHERE tenant_id=? AND project_id=? GROUP BY profile",
+        (tenant_id, project_id),
+    ):
+        profile_counts[row["profile"]] = row["n"]
+    def count(table: str, extra: str = "", params: tuple = ()) -> int:
+        row = db.fetchone(
+            f"SELECT COUNT(*) AS n FROM {table} WHERE tenant_id=? AND project_id=? {extra}",
+            (tenant_id, project_id, *params),
+        )
+        return int(row["n"])
+    return {
+        "project": project,
+        "layers": layer_counts,
+        "profiles": profile_counts,
+        "totals": {
+            "elements": sum(layer_counts.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"),
+        },
+    }
+
+
+def seed_tenant_isolation(db, tenant_id: str) -> dict:
+    """Idempotently install the complete M3->M0 tenant-isolation slice."""
+    project = create_project(db, tenant_id, {
+        "name": "DCP 租户与组织域",
+        "description": "OntoRefactor 四层元建模纵向样例:业务、软件、治理与运行事实统一建模",
+        "namespace": "https://onto.qm.cn/ontorefactor/model/dcp/1.0",
+        "version": "1.0.0",
+    })
+    pid = project["id"]
+    nodes: Dict[str, dict] = {}
+
+    def node(key: str, layer: str, profile: str, kind: str, name: str, description: str = "", attributes: Optional[dict] = None, uri: str = "") -> dict:
+        value = upsert_element(db, tenant_id, pid, {
+            "layer": layer, "profile": profile, "kind": kind, "name": name,
+            "description": description, "attributes": attributes or {},
+            "status": "published", "uri": uri,
+            "source": "builtin:tenant-isolation",
+        })
+        nodes[key] = value
+        return value
+
+    # M3: minimal stable modeling language.
+    for name in (
+        "MetaElement", "MetaEntityType", "MetaRelationType", "MetaAttributeType",
+        "MetaConstraintType", "MetaAssertionType", "MetaProfile", "MetaView",
+    ):
+        node(name, "M3", "meta", name, name, f"M3 本体建模内核:{name}", uri=f"https://onto.qm.cn/ontorefactor/meta/1.0#{name}")
+
+    # M2 upper ontology.
+    for name in ("Object", "Subject", "Activity", "Rule", "Result", "Evidence", "Assertion", "Term", "State", "TimeExtent"):
+        node(name, "M2", "upper", "EntityType", name, f"M2 通用上层本体概念:{name}", uri=f"https://onto.qm.cn/ontorefactor/upper/1.0#{name}")
+
+    profile_types = {
+        "business": ("BusinessCapabilityType", "BusinessObjectType", "BusinessRuleType", "BusinessEventType"),
+        "software": ("ModuleType", "ClassType", "ApiOperationType", "TableType", "ColumnType", "EventContractType"),
+        "governance": ("OwnerType", "AccessPolicyType", "QualityRuleType", "QualityGateType"),
+    }
+    for profile, names in profile_types.items():
+        for name in names:
+            node(name, "M2", profile, "EntityType", name, f"M2 {profile} Profile 类型:{name}", uri=f"https://onto.qm.cn/ontorefactor/{profile}/1.0#{name}")
+
+    # M1 unified DCP model.
+    m1 = (
+        ("TenantManagement", "business", "BusinessCapability", "租户管理业务能力", {}),
+        ("Tenant", "business", "BusinessObject", "租户业务对象", {"core": True}),
+        ("TenantIsolation", "business", "BusinessRule", "租户隔离业务不变量", {}),
+        ("BaseIAM", "software", "Module", "身份与租户基础模块", {"environment": "production"}),
+        ("TenantController", "software", "Class", "租户接口控制器", {}),
+        ("CreateTenantAPI", "software", "ApiOperation", "创建租户 API", {}),
+        ("DcpTenantTable", "software", "Table", "租户主表", {"environment": "production"}),
+        ("TenantIdColumn", "software", "Column", "租户标识字段", {"environment": "production", "sensitive": True, "classification": "internal"}),
+        ("TenantCreatedTopic", "software", "EventContract", "租户创建事件契约", {}),
+        ("IAMDataOwner", "governance", "OwnerRole", "IAM 数据责任人", {}),
+        ("TenantIsolationPolicy", "governance", "AccessPolicy", "租户隔离访问策略", {}),
+        ("TenantIdCompleteness", "governance", "QualityRule", "租户标识完整性规则", {"threshold": 1.0}),
+        ("ReleaseSecurityGate", "governance", "QualityGate", "发布安全门禁", {}),
+    )
+    for name, profile, kind, description, attrs in m1:
+        node(name, "M1", profile, kind, name, description, attrs, f"https://onto.qm.cn/ontorefactor/model/dcp/1.0#{name}")
+
+    # M0 concrete facts and observations.
+    node("TenantControllerCommit", "M0", "software", "SourceArtifact", "TenantController@abc123", "Git 提交中的实际源文件", {"commit": "abc123"}, "git://dcp-base-tenant/commit/abc123/src/TenantController.java")
+    node("TenantIdPhysical", "M0", "software", "PhysicalColumn", "prod.dcp_tenant.tenant_id", "生产库中的实际字段", {"environment": "production", "sensitive": True}, "db://prod/dcp_base_tenant/dcp_tenant/tenant_id")
+    node("CompletenessRun", "M0", "governance", "Measurement", "tenant-id-completeness@20260817", "一次质量测量", {"value": 0.9987, "threshold": 1.0, "result": "failed"}, "measurement:tenant-id-completeness:20260817")
+
+    evidence = create_evidence(db, tenant_id, pid, {
+        "kind": "SourceArtifact", "uri": "evidence://dcp/tenant-isolation-design",
+        "title": "租户隔离设计与 DDL", "content": "tenant_id NOT NULL; all tenant queries must filter tenant_id",
+        "source": "builtin:tenant-isolation", "metadata": {"deterministic": True},
+    })
+    runtime_evidence = create_evidence(db, tenant_id, pid, {
+        "kind": "RuntimeObservation", "uri": "evidence://trace/runtime-98271",
+        "title": "生产调用 Trace 98271", "content": "observed tenant-scoped request and database access",
+        "source": "apm://trace/98271", "metadata": {"environment": "production"},
+    })
+
+    type_map = {
+        "TenantManagement": "BusinessCapabilityType", "Tenant": "BusinessObjectType",
+        "TenantIsolation": "BusinessRuleType", "BaseIAM": "ModuleType",
+        "TenantController": "ClassType", "CreateTenantAPI": "ApiOperationType",
+        "DcpTenantTable": "TableType", "TenantIdColumn": "ColumnType",
+        "TenantCreatedTopic": "EventContractType", "IAMDataOwner": "OwnerType",
+        "TenantIsolationPolicy": "AccessPolicyType", "TenantIdCompleteness": "QualityRuleType",
+        "ReleaseSecurityGate": "QualityGateType",
+    }
+    for child, parent in type_map.items():
+        create_assertion(db, tenant_id, pid, {
+            "subject_id": nodes[child]["id"], "predicate": "INSTANCE_OF", "object_id": nodes[parent]["id"],
+            "assertion_type": "declared", "confidence": 1.0, "review_status": "accepted", "generated_by": "builtin:ontology-bootstrap",
+        })
+    for profile_names in profile_types.values():
+        for child in profile_names:
+            create_assertion(db, tenant_id, pid, {
+                "subject_id": nodes[child]["id"], "predicate": "INSTANCE_OF", "object_id": nodes["MetaEntityType"]["id"],
+                "assertion_type": "declared", "confidence": 1.0, "review_status": "accepted", "generated_by": "builtin:ontology-bootstrap",
+            })
+    for child in ("Object", "Subject", "Activity", "Rule", "Result", "Evidence", "Assertion", "Term", "State", "TimeExtent"):
+        create_assertion(db, tenant_id, pid, {
+            "subject_id": nodes[child]["id"], "predicate": "INSTANCE_OF", "object_id": nodes["MetaEntityType"]["id"],
+            "assertion_type": "declared", "confidence": 1.0, "review_status": "accepted", "generated_by": "builtin:ontology-bootstrap",
+        })
+
+    relations = (
+        ("TenantManagement", "REALIZED_BY", "BaseIAM", "declared"),
+        ("BaseIAM", "OWNED_BY", "IAMDataOwner", "declared"),
+        ("TenantController", "BELONGS_TO", "BaseIAM", "extracted"),
+        ("BaseIAM", "EXPOSES", "CreateTenantAPI", "extracted"),
+        ("DcpTenantTable", "REPRESENTS", "Tenant", "declared"),
+        ("CreateTenantAPI", "OPERATES_ON", "Tenant", "declared"),
+        ("TenantCreatedTopic", "REPRESENTS_EVENT", "Tenant", "declared"),
+        ("Tenant", "GOVERNED_BY", "TenantIsolation", "declared"),
+        ("DcpTenantTable", "OWNED_BY", "IAMDataOwner", "declared"),
+        ("TenantIdColumn", "GOVERNED_BY", "TenantIsolationPolicy", "declared"),
+        ("TenantIdColumn", "OWNED_BY", "IAMDataOwner", "declared"),
+        ("TenantIdCompleteness", "VALIDATES", "TenantIdColumn", "declared"),
+        ("ReleaseSecurityGate", "EVALUATES", "BaseIAM", "declared"),
+        ("TenantControllerCommit", "INSTANCE_OF", "TenantController", "extracted"),
+        ("TenantIdPhysical", "INSTANCE_OF", "TenantIdColumn", "observed"),
+        ("TenantIdPhysical", "OWNED_BY", "IAMDataOwner", "observed"),
+        ("TenantIdPhysical", "GOVERNED_BY", "TenantIsolationPolicy", "observed"),
+        ("CompletenessRun", "INSTANCE_OF", "TenantIdCompleteness", "observed"),
+        ("CompletenessRun", "VALIDATES", "TenantIdPhysical", "observed"),
+    )
+    for subject, predicate, obj, assertion_type in relations:
+        create_assertion(db, tenant_id, pid, {
+            "subject_id": nodes[subject]["id"], "predicate": predicate, "object_id": nodes[obj]["id"],
+            "assertion_type": assertion_type, "confidence": 1.0 if assertion_type == "declared" else 0.98,
+            "evidence_ids": [runtime_evidence["id"] if assertion_type == "observed" else evidence["id"]],
+            "generated_by": "builtin:tenant-isolation", "review_status": "accepted",
+        })
+
+    builtin_rules = (
+        ("相邻层实例化", "metamodel", "critical", {"predicate": "INSTANCE_OF", "layerDelta": 1}),
+        ("关键业务能力必须有软件实现", "integrity", "high", {"kind": "BusinessCapability", "requires": "REALIZED_BY"}),
+        ("生产数据资产必须有 Owner", "ownership", "critical", {"environment": "production", "requires": "OWNED_BY"}),
+        ("敏感字段必须受访问策略约束", "security", "critical", {"sensitive": True, "requires": "GOVERNED_BY"}),
+        ("正式非声明断言必须可追溯", "provenance", "high", {"reviewStatus": "accepted", "requires": ["evidence", "generatedBy"]}),
+    )
+    for name, rule_type, severity, expression in builtin_rules:
+        upsert_rule(db, tenant_id, pid, {
+            "name": name, "rule_type": rule_type, "severity": severity,
+            "description": "OntoRefactor 首期确定性治理门禁", "expression": expression,
+        })
+
+    validation = validate_project(db, tenant_id, pid, persist=True)
+    return {"project": project, "overview": overview(db, tenant_id, pid), "validation": validation}

+ 18 - 1
lambdagent/src/lambdagent/_shell_compat.py

@@ -48,12 +48,23 @@ def resolve_bash() -> Optional[str]:
     if not _IS_WINDOWS:
         return None
     if _CACHED_BASH is None:
+        def _is_real_bash(path: str) -> bool:
+            """Reject Windows' WSL launcher (also named bash.exe)."""
+            try:
+                probe = subprocess.run(
+                    [path, "--version"], capture_output=True, timeout=3,
+                )
+                output = (probe.stdout or b"") + (probe.stderr or b"")
+                return probe.returncode == 0 and b"GNU bash" in output
+            except (OSError, subprocess.SubprocessError):
+                return False
+
         for candidate in [
             shutil.which("bash"),
             r"C:\Program Files\Git\bin\bash.exe",
             r"C:\Program Files (x86)\Git\bin\bash.exe",
         ]:
-            if candidate and os.path.isfile(candidate):
+            if candidate and os.path.isfile(candidate) and _is_real_bash(candidate):
                 _CACHED_BASH = candidate
                 break
         else:
@@ -69,7 +80,10 @@ def run_shell(command: str, **kwargs: Any) -> subprocess.CompletedProcess:
     when Git-Bash is available; else ``shell=True`` (cmd.exe).
     """
     bash = resolve_bash()
+    if kwargs.get("text") and "errors" not in kwargs:
+        kwargs["errors"] = "replace"
     if bash:
+        kwargs.setdefault("encoding", "utf-8")
         return subprocess.run([bash, "-c", command], shell=False, **kwargs)
     return subprocess.run(command, shell=True, **kwargs)
 
@@ -77,6 +91,9 @@ def run_shell(command: str, **kwargs: Any) -> subprocess.CompletedProcess:
 def popen_shell(command: str, **kwargs: Any) -> subprocess.Popen:
     """Background variant of :func:`run_shell` — returns the live ``Popen``."""
     bash = resolve_bash()
+    if kwargs.get("text") and "errors" not in kwargs:
+        kwargs["errors"] = "replace"
     if bash:
+        kwargs.setdefault("encoding", "utf-8")
         return subprocess.Popen([bash, "-c", command], shell=False, **kwargs)
     return subprocess.Popen(command, shell=True, **kwargs)

+ 56 - 0
lambdagent/src/lambdagent/agentpack.py

@@ -16,6 +16,7 @@ from __future__ import annotations
 
 import os
 import re
+import copy
 from dataclasses import dataclass, field
 from typing import Any, Dict, List
 
@@ -85,6 +86,61 @@ class PackPermissions:
         return "此智能体包将" + ",".join(parts) + "。"
 
 
+_NETWORK_TOOLS = {"WebSearch", "WebFetch"}
+_SHELL_TOOLS = {
+    "Bash", "RunTests", "GitStatus", "GitDiff", "GitLog", "GitCommit", "GitBranch",
+}
+_FILESYSTEM_READ_TOOLS = {
+    "ReadFile", "ListFiles", "SearchContent", "CodeSearch", "ProjectMap",
+    "GitStatus", "GitDiff", "GitLog", "GitBranch", "OCR",
+}
+_FILESYSTEM_WRITE_TOOLS = {"WriteFile", "EditFile", "NotebookEdit", "DocGen", "GitCommit"}
+_KNOWLEDGE_READ_TOOLS = {
+    "KBSearch", "KBList", "QueryKnowledge", "ListKnowledge", "DeepAnalysis",
+    "WikiQuery", "WikiSearch", "WikiStatus", "MemoryRecall", "MemoryList",
+}
+_KNOWLEDGE_WRITE_TOOLS = {
+    "KBCreate", "KBAdd", "IngestFiles", "RemoveKnowledge", "WikiIngest", "WikiGrow",
+}
+
+
+def enforce_config_permissions(config: Dict[str, Any], permissions: PackPermissions | Dict[str, Any]) -> tuple[Dict[str, Any], Dict[str, Any]]:
+    """Return a config whose callable tools cannot exceed AgentPack permissions.
+
+    The permission declaration is persisted in the config so lazily compiled
+    sub-agents inherit the same boundary.  The host additionally uses the
+    declaration to scope file writes to the per-run workspace.
+    """
+    perms = permissions if isinstance(permissions, PackPermissions) else PackPermissions.from_dict(permissions)
+    cfg = copy.deepcopy(config)
+    mcp = dict(cfg.get("mcp") or {})
+    original = list(mcp.get("localTools") or [])
+    denied = set()
+    if not perms.network:
+        denied.update(_NETWORK_TOOLS)
+        mcp.pop("onlineTool", None)
+    if not perms.shell:
+        denied.update(_SHELL_TOOLS)
+    if not perms.read_filesystem:
+        denied.update(_FILESYSTEM_READ_TOOLS)
+    if perms.file_write == "none":
+        denied.update(_FILESYSTEM_WRITE_TOOLS)
+    if not perms.read_knowledge:
+        denied.update(_KNOWLEDGE_READ_TOOLS)
+    if perms.file_write != "knowledge":
+        denied.update(_KNOWLEDGE_WRITE_TOOLS)
+
+    kept = [name for name in original if name not in denied]
+    mcp["localTools"] = kept
+    cfg["mcp"] = mcp
+    cfg["_agentpack_permissions"] = perms.to_dict()
+    return cfg, {
+        "removed_tools": [name for name in original if name not in kept],
+        "network_tools_removed": not perms.network,
+        "permissions": perms.to_dict(),
+    }
+
+
 # ── Manifest ──────────────────────────────────────────────────────────────
 
 _ID_RE = re.compile(r"^[a-z0-9]([a-z0-9.\-]*[a-z0-9])?$")

+ 18 - 4
lambdagent/src/lambdagent/fromconfig/compiler.py

@@ -272,6 +272,11 @@ def build_agent(cfg: Dict[str, Any], overrides: Dict = None, _depth: int = 0) ->
 
 def _build_agent_inner(cfg: Dict[str, Any], overrides: Dict = None) -> Term:
     overrides = overrides or {}
+    pack_permissions = cfg.get("_agentpack_permissions") or overrides.get("_agentpack_permissions")
+    if pack_permissions:
+        from lambdagent.agentpack import enforce_config_permissions
+        cfg, _ = enforce_config_permissions(cfg, pack_permissions)
+        overrides = {**overrides, "_agentpack_permissions": pack_permissions}
     agent_type = cfg.get("type", "simple")
 
     # An explicit cfg._config_dir TRUMPS whatever from_config inferred from
@@ -413,10 +418,16 @@ def _compile_sub_agents(cfg: Dict, sub_agents_cfg: Dict, overrides: Dict) -> Dic
                     try:
                         if _inline:
                             # Inline mode: compile from dict directly
-                            _compiled_cache[_name] = build_agent(_inline, {})
+                            child_overrides = {}
+                            if overrides.get("_agentpack_permissions"):
+                                child_overrides["_agentpack_permissions"] = overrides["_agentpack_permissions"]
+                            _compiled_cache[_name] = build_agent(_inline, child_overrides)
                         else:
                             # File mode: compile from YAML path
-                            _compiled_cache[_name] = from_config(_path)
+                            child_overrides = {}
+                            if overrides.get("_agentpack_permissions"):
+                                child_overrides["_agentpack_permissions"] = overrides["_agentpack_permissions"]
+                            _compiled_cache[_name] = from_config(_path, **child_overrides)
                     except Exception as e:
                         return f"[SubAgent {_name} compile error: {e}]"
 
@@ -471,10 +482,13 @@ def _compile_sub_agents(cfg: Dict, sub_agents_cfg: Dict, overrides: Dict) -> Dic
 
                 def apply(self, input_val, ctx=None):
                     if self._inner is None:
+                        child_overrides = {}
+                        if overrides.get("_agentpack_permissions"):
+                            child_overrides["_agentpack_permissions"] = overrides["_agentpack_permissions"]
                         if self._inline:
-                            self._inner = build_agent(self._inline, {})
+                            self._inner = build_agent(self._inline, child_overrides)
                         else:
-                            self._inner = from_config(self._path)
+                            self._inner = from_config(self._path, **child_overrides)
                     ctx = ctx or Context()
                     return self._inner.apply(str(input_val), ctx)
 

+ 19 - 1
lambdagent/src/lambdagent/providers/claude_code_provider.py

@@ -11,7 +11,10 @@ Optimizations:
 """
 from __future__ import annotations
 
-import fcntl
+try:  # POSIX-only; keep the provider module importable on Windows.
+    import fcntl
+except ImportError:  # pragma: no cover - exercised on Windows hosts
+    fcntl = None
 import json
 import logging
 import os
@@ -122,6 +125,21 @@ def _run_with_idle_timeout(
     until poll() returns or a watchdog fires. macOS/Linux only — Windows
     has no select-on-pipe, but agentpaas doesn't target Windows anyway.
     """
+    if fcntl is None:
+        # Windows cannot select() non-blocking anonymous pipes.  Preserve the
+        # hard timeout and cancellation-safe process cleanup; idle/first-byte
+        # watchdogs remain the POSIX fast path.
+        started = time.time()
+        try:
+            return subprocess.run(
+                cmd, stdin=subprocess.DEVNULL, capture_output=True,
+                timeout=hard_timeout, text=True,
+            )
+        except subprocess.TimeoutExpired as exc:
+            stdout = exc.stdout.decode(errors="replace") if isinstance(exc.stdout, bytes) else (exc.stdout or "")
+            stderr = exc.stderr.decode(errors="replace") if isinstance(exc.stderr, bytes) else (exc.stderr or "")
+            raise _StallError("hard", time.time() - started, stdout, stderr) from exc
+
     proc = subprocess.Popen(
         cmd,
         stdin=subprocess.DEVNULL,

+ 24 - 3
lambdagent/src/lambdagent/tool_gateway.py

@@ -177,6 +177,9 @@ def classify_tool_call(tool_name: str, tool_input: Any) -> tuple[RiskLevel, str]
     Returns:
         (risk_level, reason)
     """
+    # Built-in tools use display names such as ``Bash`` and ``WriteFile``.
+    # Classification must not depend on the caller's capitalisation.
+    tool_name = tool_name.lower()
     input_str = str(tool_input) if tool_input else ""
 
     # shell 工具 → 命令级分类
@@ -461,7 +464,7 @@ class ToolGateway:
 
         return Action.ALLOW, reason
 
-    def wrap(self, tool: Term) -> "GatedTool":
+    def wrap(self, tool: Term) -> Term:
         """将普通 Tool 包裹为 GatedTool。"""
         from .primitives import Tool as BaseTool
         if isinstance(tool, GatedTool):
@@ -472,6 +475,17 @@ class ToolGateway:
                 fn=tool.fn,
                 gateway=self,
             )
+        # Built-in filesystem/shell tools are ValidatedTool instances rather
+        # than primitive Tool instances.  Keeping those terms unwrapped would
+        # bypass the gateway completely.  Delegate to their ``apply`` method so
+        # schema validation is preserved inside the security boundary.
+        if isinstance(tool, Term):
+            return GatedTool(
+                name=tool._name,
+                fn=None,
+                gateway=self,
+                wrapped=tool,
+            )
         return tool
 
     def truncate_output(self, output: str) -> str:
@@ -537,9 +551,11 @@ class GatedTool(Term):
     与 Tool 的 apply 接口完全一致,但在调用 fn 前先过 Gateway。
     """
 
-    def __init__(self, name: str, fn: Callable, gateway: ToolGateway):
+    def __init__(self, name: str, fn: Optional[Callable], gateway: ToolGateway,
+                 wrapped: Optional[Term] = None):
         super().__init__(name)
         self.fn = fn
+        self.wrapped = wrapped
         self.gateway = gateway
 
     def apply(self, input: Any, ctx: Optional[Context] = None) -> Any:
@@ -585,7 +601,12 @@ class GatedTool(Term):
 
         # ── Step 2: 执行工具 ──
         try:
-            result = self.fn(input)
+            if self.wrapped is not None:
+                result = self.wrapped.apply(input, ctx)
+            elif self.fn is not None:
+                result = self.fn(input)
+            else:  # defensive: construction always supplies one executor
+                raise RuntimeError(f"GatedTool '{self._name}' has no executor")
         except Exception as e:
             duration = (time.time() - t0) * 1000
             error_msg = f"[ERROR] {type(e).__name__}: {e}"

+ 27 - 0
ontology/README.md

@@ -0,0 +1,27 @@
+# OntoRefactor ontology
+
+This directory is the version-controlled semantic source for the governance
+plane. The runtime mirrors these concepts into the tenant-scoped SQLite graph
+through `/api/v1/governance/bootstrap/tenant-isolation`.
+
+- `m3/`: the stable ontology modeling kernel.
+- `m2/`: upper ontology, three domain profiles, and cross-profile relations.
+- `m1/dcp/`: the DCP tenant-domain reference model.
+- `shapes/`: deterministic SHACL governance gates.
+- `views/`: presentation rules; views are orthogonal to M0-M3 semantics.
+
+M0 observations are deliberately not checked into source control. They are
+created by parsers and LambdAgent runs and retain evidence, validity and review
+state in the governance database.
+
+The reference slice can be installed idempotently with:
+
+```http
+POST /api/v1/governance/bootstrap/tenant-isolation
+Authorization: Bearer <agentpaas-api-key>
+```
+
+The API persists the graph in tenant-scoped SQLite tables for Desktop and
+single-node deployments. The TTL files remain the portable semantic source and
+were syntax-checked as one 275-triple corpus; they can later be loaded into an
+RDF store without changing the REST/UI contracts.

+ 8 - 0
ontology/m1/dcp/business-model.ttl

@@ -0,0 +1,8 @@
+@prefix dcp: <https://onto.qm.cn/ontorefactor/model/dcp/1.0#> .
+@prefix business: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+
+dcp:TenantManagement a owl:Class, business:BusinessCapabilityType .
+dcp:Tenant a owl:Class, business:BusinessObjectType .
+dcp:TenantIsolation a owl:Class, business:BusinessRuleType .
+dcp:TenantCreated a owl:Class, business:BusinessEventType .

+ 16 - 0
ontology/m1/dcp/governance-model.ttl

@@ -0,0 +1,16 @@
+@prefix dcp: <https://onto.qm.cn/ontorefactor/model/dcp/1.0#> .
+@prefix governance: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+
+dcp:IAMDataOwner a governance:OwnerType .
+dcp:TenantIsolationPolicy a governance:AccessPolicyType .
+dcp:TenantIdCompleteness a governance:QualityRuleType .
+dcp:ReleaseSecurityGate a governance:QualityGateType .
+
+dcp:TenantManagement cross:REALIZED_BY dcp:BaseIAM .
+dcp:DcpTenantTable cross:REPRESENTS dcp:Tenant ; cross:OWNED_BY dcp:IAMDataOwner .
+dcp:CreateTenantAPI cross:OPERATES_ON dcp:Tenant .
+dcp:TenantIdColumn cross:GOVERNED_BY dcp:TenantIsolationPolicy ; cross:OWNED_BY dcp:IAMDataOwner .
+dcp:TenantIdCompleteness cross:VALIDATES dcp:TenantIdColumn .
+dcp:ReleaseSecurityGate cross:EVALUATES dcp:BaseIAM .

+ 10 - 0
ontology/m1/dcp/software-model.ttl

@@ -0,0 +1,10 @@
+@prefix dcp: <https://onto.qm.cn/ontorefactor/model/dcp/1.0#> .
+@prefix software: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+
+dcp:BaseIAM a owl:Class, software:ModuleType .
+dcp:TenantController a owl:Class, software:ClassType .
+dcp:CreateTenantAPI a owl:Class, software:ApiOperationType .
+dcp:DcpTenantTable a owl:Class, software:TableType .
+dcp:TenantIdColumn a owl:Class, software:ColumnType .
+dcp:TenantCreatedTopic a owl:Class, software:EventContractType .

+ 16 - 0
ontology/m2/business-profile.ttl

@@ -0,0 +1,16 @@
+@prefix business: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+<https://onto.qm.cn/ontorefactor/business/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/upper/1.0> .
+
+business:BusinessProfile a meta:MetaProfile .
+business:BusinessCapabilityType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+business:BusinessObjectType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+business:BusinessProcessType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Activity .
+business:BusinessRuleType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Rule .
+business:BusinessEventType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Result .
+business:BusinessTermType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Term .

+ 17 - 0
ontology/m2/cross-profile-relations.ttl

@@ -0,0 +1,17 @@
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix business: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix software: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix governance: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+cross:REALIZED_BY a owl:ObjectProperty ; rdfs:domain business:BusinessCapabilityType ; rdfs:range software:ModuleType .
+cross:REPRESENTS a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range business:BusinessObjectType .
+cross:OPERATES_ON a owl:ObjectProperty ; rdfs:domain software:ApiOperationType ; rdfs:range business:BusinessObjectType .
+cross:GOVERNED_BY a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range upper:Rule .
+cross:OWNED_BY a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range governance:OwnerType .
+cross:VALIDATES a owl:ObjectProperty ; rdfs:domain governance:QualityRuleType ; rdfs:range upper:Object .
+cross:EVALUATES a owl:ObjectProperty ; rdfs:domain governance:QualityGateType ; rdfs:range upper:Object .
+cross:CONTAINS a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range upper:Object .
+cross:AFFECTS a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range upper:Object .

+ 22 - 0
ontology/m2/governance-profile.ttl

@@ -0,0 +1,22 @@
+@prefix governance: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+<https://onto.qm.cn/ontorefactor/governance/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/upper/1.0> .
+
+governance:GovernanceProfile a meta:MetaProfile .
+governance:OrganizationType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Subject .
+governance:OwnerType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Subject .
+governance:StewardType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Subject .
+governance:PolicyType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Rule .
+governance:AccessPolicyType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf governance:PolicyType .
+governance:QualityRuleType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Rule .
+governance:ReviewType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Activity .
+governance:ApprovalType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Activity .
+governance:MeasurementType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Result .
+governance:IssueType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Result .
+governance:RiskType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Result .
+governance:QualityGateType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Result .

+ 19 - 0
ontology/m2/software-profile.ttl

@@ -0,0 +1,19 @@
+@prefix software: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+<https://onto.qm.cn/ontorefactor/software/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/upper/1.0> .
+
+software:SoftwareProfile a meta:MetaProfile .
+software:SoftwareSystemType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:ModuleType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:ClassType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:ApiOperationType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:TableType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:ColumnType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:EventContractType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:DeploymentType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Object .
+software:TestCaseType a owl:Class, meta:MetaEntityType ; rdfs:subClassOf upper:Activity .

+ 30 - 0
ontology/m2/upper.ttl

@@ -0,0 +1,30 @@
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+<https://onto.qm.cn/ontorefactor/upper/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/meta/1.0> .
+
+upper:Object a owl:Class, meta:MetaEntityType .
+upper:Subject a owl:Class, meta:MetaEntityType .
+upper:Activity a owl:Class, meta:MetaEntityType .
+upper:Rule a owl:Class, meta:MetaEntityType .
+upper:Result a owl:Class, meta:MetaEntityType .
+upper:Evidence a owl:Class, meta:MetaEntityType .
+upper:Assertion a owl:Class, meta:MetaAssertionType .
+upper:Term a owl:Class, meta:MetaEntityType .
+upper:State a owl:Class, meta:MetaEntityType .
+upper:TimeExtent a owl:Class, meta:MetaEntityType .
+
+upper:subject a owl:ObjectProperty ; rdfs:domain upper:Assertion ; rdfs:range upper:Object .
+upper:predicate a owl:ObjectProperty ; rdfs:domain upper:Assertion .
+upper:object a owl:ObjectProperty ; rdfs:domain upper:Assertion ; rdfs:range upper:Object .
+upper:evidencedBy a owl:ObjectProperty ; rdfs:domain upper:Assertion ; rdfs:range upper:Evidence .
+upper:generatedBy a owl:ObjectProperty ; rdfs:domain upper:Assertion ; rdfs:range upper:Subject .
+upper:confidence a owl:DatatypeProperty ; rdfs:domain upper:Assertion ; rdfs:range xsd:decimal .
+upper:validFrom a owl:DatatypeProperty ; rdfs:range xsd:dateTime .
+upper:validTo a owl:DatatypeProperty ; rdfs:range xsd:dateTime .
+upper:observedAt a owl:DatatypeProperty ; rdfs:range xsd:dateTime .
+upper:reviewStatus a owl:DatatypeProperty ; rdfs:domain upper:Assertion ; rdfs:range xsd:string .

+ 23 - 0
ontology/m3/meta-core.ttl

@@ -0,0 +1,23 @@
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+<https://onto.qm.cn/ontorefactor/meta/1.0> a owl:Ontology ;
+  rdfs:label "OntoRefactor M3 ontology modeling kernel"@en .
+
+meta:MetaElement a owl:Class .
+meta:MetaEntityType a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaRelationType a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaAttributeType a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaConstraintType a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaAssertionType a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaProfile a owl:Class ; rdfs:subClassOf meta:MetaElement .
+meta:MetaView a owl:Class ; rdfs:subClassOf meta:MetaElement .
+
+meta:instanceOf a owl:ObjectProperty ; rdfs:domain meta:MetaElement ; rdfs:range meta:MetaElement .
+meta:subTypeOf a owl:ObjectProperty ; rdfs:domain meta:MetaElement ; rdfs:range meta:MetaElement .
+meta:hasDomain a owl:ObjectProperty ; rdfs:domain meta:MetaRelationType ; rdfs:range meta:MetaEntityType .
+meta:hasRange a owl:ObjectProperty ; rdfs:domain meta:MetaRelationType ; rdfs:range meta:MetaEntityType .
+meta:hasAttribute a owl:ObjectProperty ; rdfs:domain meta:MetaEntityType ; rdfs:range meta:MetaAttributeType .
+meta:constrainedBy a owl:ObjectProperty ; rdfs:domain meta:MetaElement ; rdfs:range meta:MetaConstraintType .
+meta:expressedBy a owl:ObjectProperty ; rdfs:domain meta:MetaElement ; rdfs:range meta:MetaView .

+ 11 - 0
ontology/shapes/instance-shapes.ttl

@@ -0,0 +1,11 @@
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+upper:AssertionProvenanceShape a sh:NodeShape ;
+  sh:targetClass upper:Assertion ;
+  sh:property [ sh:path upper:evidencedBy ; sh:minCount 1 ; sh:class upper:Evidence ] ;
+  sh:property [ sh:path upper:generatedBy ; sh:minCount 1 ; sh:class upper:Subject ] ;
+  sh:property [ sh:path upper:validFrom ; sh:minCount 1 ; sh:datatype xsd:dateTime ] ;
+  sh:property [ sh:path upper:reviewStatus ; sh:minCount 1 ; sh:datatype xsd:string ] .

+ 8 - 0
ontology/shapes/meta-shapes.ttl

@@ -0,0 +1,8 @@
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+@prefix meta: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+meta:RelationTypeShape a sh:NodeShape ;
+  sh:targetClass meta:MetaRelationType ;
+  sh:property [ sh:path meta:hasDomain ; sh:minCount 1 ; sh:class meta:MetaEntityType ] ;
+  sh:property [ sh:path meta:hasRange ; sh:minCount 1 ; sh:class meta:MetaEntityType ] .

+ 12 - 0
ontology/shapes/model-shapes.ttl

@@ -0,0 +1,12 @@
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+@prefix business: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix software: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+
+business:CapabilityRealizationShape a sh:NodeShape ;
+  sh:targetClass business:BusinessCapabilityType ;
+  sh:property [ sh:path cross:REALIZED_BY ; sh:minCount 1 ; sh:class software:ModuleType ] .
+
+business:ObjectRepresentationShape a sh:NodeShape ;
+  sh:targetClass business:BusinessObjectType ;
+  sh:property [ sh:path [ sh:inversePath cross:REPRESENTS ] ; sh:minCount 1 ] .

+ 7 - 0
ontology/views/business-view.yaml

@@ -0,0 +1,7 @@
+id: business-view
+title: 业务能力视图
+layers: [M1]
+profiles: [business, cross]
+nodeKinds: [BusinessCapability, BusinessObject, BusinessRule, BusinessEvent]
+edgeKinds: [REALIZED_BY, REPRESENTS, OPERATES_ON, GOVERNED_BY]
+color: amber

+ 7 - 0
ontology/views/engineering-view.yaml

@@ -0,0 +1,7 @@
+id: engineering-view
+title: 软件工程与数据资产视图
+layers: [M1, M0]
+profiles: [software, cross]
+nodeKinds: [Module, Class, ApiOperation, Table, Column, EventContract, PhysicalTable, PhysicalColumn]
+edgeKinds: [CONTAINS, EXPOSES, READS, WRITES, INSTANCE_OF, AFFECTS]
+color: blue

+ 7 - 0
ontology/views/governance-view.yaml

@@ -0,0 +1,7 @@
+id: governance-view
+title: 数据治理与证据视图
+layers: [M2, M1, M0]
+profiles: [governance, cross]
+nodeKinds: [OwnerRole, AccessPolicy, QualityRule, QualityGate, Measurement, Evidence, Issue]
+edgeKinds: [OWNED_BY, GOVERNED_BY, VALIDATES, EVALUATES, EVIDENCED_BY]
+color: emerald

+ 40 - 0
tests/test_agentpack.py

@@ -27,6 +27,7 @@ from lambdagent.agentpack import (
     AgentPackError,
     load_manifest,
     validate_manifest_dict,
+    enforce_config_permissions,
 )
 from agentpaas.engine.agentpack_store import (
     install_from_zip,
@@ -111,6 +112,45 @@ def test_permission_summary_zh():
     assert "不访问网络" in s
 
 
+def test_permissions_are_enforced_on_runtime_config():
+    config = {
+        "type": "react",
+        "systemPrompt": "test",
+        "mcp": {
+            "localTools": [
+                "Bash", "RunTests", "WebSearch", "ReadFile", "WriteFile",
+                "KBSearch", "KBAdd", "terminate",
+            ],
+            "onlineTool": {"remote": ["dangerous_remote_tool"]},
+        },
+    }
+    effective, report = enforce_config_permissions(config, PackPermissions(
+        network=False, shell=False, file_write="workspace",
+        read_knowledge=True, read_filesystem=False,
+    ))
+
+    assert effective["mcp"]["localTools"] == ["WriteFile", "KBSearch", "terminate"]
+    assert "onlineTool" not in effective["mcp"]
+    assert set(report["removed_tools"]) == {
+        "Bash", "RunTests", "WebSearch", "ReadFile", "KBAdd",
+    }
+    assert effective["_agentpack_permissions"]["file_write"] == "workspace"
+
+
+def test_gateway_wraps_validated_builtin_tools_case_insensitively():
+    from lambdagent.tool_gateway import GatedTool, GatewayPolicy, ToolGateway
+    from lambdagent.validated_tool import ShellToolInput, ValidatedTool
+
+    executed = []
+    bash = ValidatedTool("Bash", lambda value: executed.append(value) or "ran", ShellToolInput)
+    gated = ToolGateway(GatewayPolicy()).wrap(bash)
+
+    assert isinstance(gated, GatedTool)
+    result = gated.apply('{"command":"rm -rf /"}')
+    assert result.startswith("[BLOCKED]")
+    assert executed == []
+
+
 # ─────────────────────────────────────────────────────────────────────────────
 # Helpers to build pack zips
 # ─────────────────────────────────────────────────────────────────────────────

+ 42 - 0
tests/test_api_endpoints.py

@@ -171,6 +171,48 @@ def test_list_agents_accepts_valid_key(client, tenant_and_key):
     assert isinstance(items, list)
 
 
+# ─────────────────────────────────────────────────────────────────────────────
+# OntoRefactor governance vertical slice
+# ─────────────────────────────────────────────────────────────────────────────
+
+def test_governance_requires_auth(client):
+    r = client.get("/api/v1/governance/projects")
+    assert r.status_code == 401
+
+
+def test_governance_bootstrap_and_agent_ingest(client, tenant_and_key):
+    _, key = tenant_and_key
+    seeded = client.post(
+        "/api/v1/governance/bootstrap/tenant-isolation", headers=_auth(key)
+    )
+    assert seeded.status_code == 200, seeded.text[:500]
+    body = seeded.json()
+    project_id = body["project"]["id"]
+    assert body["validation"]["valid"] is True
+    assert body["overview"]["layers"]["M3"] == 8
+
+    analysis = client.post(
+        f"/api/v1/governance/projects/{project_id}/agent-runs/analyze",
+        headers=_auth(key),
+        json={
+            "source_type": "ddl",
+            "source_name": "orders.sql",
+            "environment": "development",
+            "content": "CREATE TABLE dcp_order (id BIGINT PRIMARY KEY, amount DECIMAL(12,2));",
+        },
+    )
+    assert analysis.status_code == 200, analysis.text[:500]
+    assert analysis.json()["status"] == "completed"
+    assert len(analysis.json()["trace"]) >= 5
+
+    graph = client.get(
+        f"/api/v1/governance/projects/{project_id}/graph", headers=_auth(key)
+    )
+    assert graph.status_code == 200, graph.text[:500]
+    assert graph.json()["nodes"]
+    assert all("subject_name" in edge for edge in graph.json()["edges"])
+
+
 # ─────────────────────────────────────────────────────────────────────────────
 # Phase 0 verification — loopback guard on /bootstrap
 # ─────────────────────────────────────────────────────────────────────────────

+ 123 - 0
tests/test_governance.py

@@ -0,0 +1,123 @@
+"""End-to-end tests for the M3-M0 governance vertical slice."""
+from __future__ import annotations
+
+import os
+
+os.environ.setdefault("AGENTPAAS_DATABASE_URL", "sqlite:///:memory:")
+os.environ.setdefault("AGENTPAAS_TESTING", "1")
+
+import pytest
+
+from agentpaas.db.models import Database, now_utc
+from agentpaas.governance import service
+from agentpaas.governance.agents import execute_governance_agent
+
+
+@pytest.fixture
+def db():
+    value = Database("sqlite:///:memory:")
+    value.execute(
+        "INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,?,?,?,?)",
+        ("tenant-a", "Tenant A", "free", "active", now_utc()),
+    )
+    value.execute(
+        "INSERT INTO tenants (id,name,plan,status,created_at) VALUES (?,?,?,?,?)",
+        ("tenant-b", "Tenant B", "free", "active", now_utc()),
+    )
+    value.commit()
+    return value
+
+
+def test_reference_slice_is_complete_valid_and_idempotent(db):
+    first = service.seed_tenant_isolation(db, "tenant-a")
+    second = service.seed_tenant_isolation(db, "tenant-a")
+
+    assert first["project"]["id"] == second["project"]["id"]
+    assert second["validation"]["valid"] is True
+    assert second["validation"]["findings"] == []
+    assert second["overview"]["layers"]["M3"] == 8
+    assert second["overview"]["layers"]["M0"] == 3
+    assert second["overview"]["totals"]["pending_reviews"] == 0
+
+
+def test_impact_crosses_business_software_and_governance_profiles(db):
+    seeded = service.seed_tenant_isolation(db, "tenant-a")
+    project_id = seeded["project"]["id"]
+    elements = service.list_elements(db, "tenant-a", project_id, query="TenantIdColumn")["items"]
+    column = next(item for item in elements if item["name"] == "TenantIdColumn")
+
+    result = service.impact(db, "tenant-a", column["id"], max_depth=5)
+    names = {item["name"] for item in result["nodes"]}
+    assert {"Tenant", "IAMDataOwner", "TenantIsolationPolicy", "TenantIdCompleteness"} <= names
+    assert any(edge["predicate"] == "REPRESENTS" for edge in result["edges"])
+
+
+def test_tenant_scope_blocks_cross_tenant_reads(db):
+    seeded = service.seed_tenant_isolation(db, "tenant-a")
+    project_id = seeded["project"]["id"]
+    with pytest.raises(service.GovernanceError) as exc:
+        service.overview(db, "tenant-b", project_id)
+    assert exc.value.status == 404
+
+
+def test_ddl_agent_persists_facts_candidates_evidence_and_trace(db):
+    project = service.create_project(db, "tenant-a", {
+        "name": "DDL ingest", "namespace": "https://example.test/ddl",
+    })
+    ddl = """
+    CREATE TABLE dcp_customer (
+      id BIGINT PRIMARY KEY,
+      mobile VARCHAR(32) NOT NULL,
+      created_at TIMESTAMP
+    );
+    """
+    response = execute_governance_agent(db, "tenant-a", project["id"], {
+        "source_type": "ddl",
+        "source_name": "customer.sql",
+        "environment": "production",
+        "content": ddl,
+    })
+
+    assert response["status"] == "completed"
+    assert response["persisted"]["elements"] == 9
+    assert response["persisted"]["assertions"] == 11
+    assert response["persisted"]["issues"] == 2
+    assert {entry["term_name"] for entry in response["trace"]} >= {
+        "governance-source-normalizer", "asset-discovery-agent",
+        "business-semantics-agent", "governance-controls-agent",
+        "governance-synthesis-agent",
+    }
+
+    pending = service.list_assertions(
+        db, "tenant-a", project["id"], review_status="pending"
+    )
+    assert len(pending) == 1
+    assert pending[0]["predicate"] == "REPRESENTS"
+    assert pending[0]["assertion_type"] == "inferred"
+    assert pending[0]["evidence_ids"]
+
+    reviewed = service.review_assertion(
+        db, "tenant-a", pending[0]["id"], "accepted", "data-steward", "术语已确认"
+    )
+    assert reviewed["review_status"] == "accepted"
+    assert reviewed["reviewer"] == "data-steward"
+
+    runs = db.fetchall(
+        "SELECT status,trace_json FROM governance_agent_runs WHERE tenant_id=?",
+        ("tenant-a",),
+    )
+    assert len(runs) == 1 and runs[0]["status"] == "completed"
+
+
+def test_validation_detects_untyped_published_m0_fact(db):
+    project = service.create_project(db, "tenant-a", {
+        "name": "Validation", "namespace": "https://example.test/validation",
+    })
+    fact = service.upsert_element(db, "tenant-a", project["id"], {
+        "layer": "M0", "profile": "software", "kind": "PhysicalTable",
+        "name": "prod.orphan", "status": "published",
+    })
+    result = service.validate_project(db, "tenant-a", project["id"], persist=True)
+    assert result["valid"] is False
+    assert any(item["code"] == "M0_UNKNOWN_TYPE" and item["target_id"] == fact["id"]
+               for item in result["findings"])

+ 2 - 0
webui/src/App.tsx

@@ -14,6 +14,7 @@ import KnowledgeDetail from './pages/KnowledgeDetail'
 import AgentPacks from './pages/AgentPacks'
 import AgentWorkspace from './pages/AgentWorkspace'
 import Connections from './pages/Connections'
+import Governance from './pages/Governance'
 
 function RequireAuth({ children }: { children: React.ReactNode }) {
   const apiKey = useAppStore(s => s.apiKey)
@@ -47,6 +48,7 @@ export default function App() {
           <Route path="knowledge" element={<Knowledge />} />
           <Route path="knowledge/:kbId" element={<KnowledgeDetail />} />
           <Route path="agentpacks" element={<AgentPacks />} />
+          <Route path="governance" element={<Governance />} />
         </Route>
         <Route path="*" element={<Navigate to="/" replace />} />
       </Routes>

+ 5 - 2
webui/src/api/client.ts

@@ -27,8 +27,11 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
     let message = res.statusText
     try {
       const data = await res.json()
-      code = data?.error?.code ?? code
-      message = data?.error?.message ?? message
+      // FastAPI HTTPException serializes application errors under `detail`,
+      // while middleware responses use a top-level `error` object.
+      const error = data?.error ?? data?.detail?.error ?? data?.detail
+      code = error?.code ?? code
+      message = error?.message ?? (typeof error === 'string' ? error : message)
     } catch {
       // ignore parse error
     }

+ 180 - 0
webui/src/api/governance.ts

@@ -0,0 +1,180 @@
+import { api } from './client'
+
+export type OntologyLayer = 'M3' | 'M2' | 'M1' | 'M0'
+export type OntologyProfile = 'meta' | 'upper' | 'business' | 'software' | 'governance' | 'cross'
+
+export interface GovernanceProject {
+  id: string
+  name: string
+  description: string
+  namespace: string
+  version: string
+  status: string
+  updated_at: string
+}
+
+export interface OntologyElement {
+  id: string
+  project_id: string
+  uri: string
+  layer: OntologyLayer
+  profile: OntologyProfile
+  kind: string
+  name: string
+  description: string
+  status: 'candidate' | 'published' | 'deprecated' | 'retired'
+  version: string
+  attributes: Record<string, unknown>
+  source: string
+}
+
+export interface GovernanceAssertion {
+  id: string
+  subject_id: string
+  subject_name?: string
+  predicate: string
+  object_id?: string
+  object_name?: string
+  object_value: string
+  assertion_type: 'declared' | 'extracted' | 'observed' | 'inferred'
+  confidence: number
+  evidence_ids: string[]
+  generated_by: string
+  valid_from?: string
+  valid_to?: string
+  review_status: 'pending' | 'accepted' | 'rejected' | 'expired'
+  reviewer: string
+  review_note: string
+  updated_at: string
+}
+
+export interface GovernanceIssue {
+  id: string
+  rule_id: string
+  target_id: string
+  assertion_id: string
+  severity: 'low' | 'medium' | 'high' | 'critical'
+  title: string
+  description: string
+  status: 'open' | 'acknowledged' | 'resolved' | 'waived'
+  assigned_to: string
+  evidence_ids: string[]
+}
+
+export interface GovernanceRule {
+  id: string
+  name: string
+  description: string
+  rule_type: string
+  severity: string
+  expression: Record<string, unknown>
+  enabled: boolean
+}
+
+export interface GovernanceEvidence {
+  id: string
+  kind: string
+  uri: string
+  title: string
+  content: string
+  content_hash: string
+  source: string
+  observed_at: string
+  valid_from?: string
+  valid_to?: string
+  metadata: Record<string, unknown>
+  created_at: string
+}
+
+export interface GovernanceOverview {
+  project: GovernanceProject
+  layers: Record<OntologyLayer, number>
+  profiles: Record<OntologyProfile, number>
+  totals: {
+    elements: number
+    assertions: number
+    pending_reviews: number
+    evidence: number
+    rules: number
+    open_issues: number
+    agent_runs: number
+  }
+}
+
+export interface GovernanceGraph {
+  nodes: OntologyElement[]
+  edges: GovernanceAssertion[]
+}
+
+export interface ValidationResult {
+  valid: boolean
+  findings: Array<{
+    code: string
+    severity: string
+    title: string
+    description: string
+    target_id: string
+    assertion_id: string
+  }>
+  counts: Record<string, number>
+  checked: { elements: number; assertions: number }
+}
+
+export interface GovernanceAgentRun {
+  id: string
+  source_type: string
+  status: string
+  result_json: Record<string, unknown>
+  trace_json: Array<{ term_name: string; duration_ms: number }>
+  error: string
+  created_at: string
+  completed_at?: string
+}
+
+function qs(params: Record<string, string | number | boolean | undefined>) {
+  const search = new URLSearchParams()
+  Object.entries(params).forEach(([key, value]) => {
+    if (value !== undefined && value !== '') search.set(key, String(value))
+  })
+  const encoded = search.toString()
+  return encoded ? `?${encoded}` : ''
+}
+
+export const governanceApi = {
+  projects: (): Promise<{ projects: GovernanceProject[] }> => api.get('/governance/projects'),
+  createProject: (body: { name: string; description: string; namespace: string }): Promise<GovernanceProject> =>
+    api.post('/governance/projects', body),
+  bootstrap: (): Promise<{ project: GovernanceProject; overview: GovernanceOverview; validation: ValidationResult }> =>
+    api.post('/governance/bootstrap/tenant-isolation'),
+  overview: (projectId: string): Promise<GovernanceOverview> =>
+    api.get(`/governance/projects/${projectId}/overview`),
+  elements: (projectId: string, filters: { layer?: string; profile?: string; q?: string } = {}): Promise<{ items: OntologyElement[]; total: number }> =>
+    api.get(`/governance/projects/${projectId}/elements${qs({ ...filters, limit: 500 })}`),
+  graph: (projectId: string): Promise<GovernanceGraph> =>
+    api.get(`/governance/projects/${projectId}/graph?limit=500&include_pending=true`),
+  evidence: (projectId: string): Promise<{ evidence: GovernanceEvidence[] }> =>
+    api.get(`/governance/projects/${projectId}/evidence?limit=500`),
+  impact: (elementId: string): Promise<{ center: OntologyElement; nodes: Array<OntologyElement & { distance: number }>; edges: GovernanceAssertion[] }> =>
+    api.get(`/governance/elements/${elementId}/impact?max_depth=4`),
+  assertions: (projectId: string, reviewStatus = ''): Promise<{ assertions: GovernanceAssertion[] }> =>
+    api.get(`/governance/projects/${projectId}/assertions${qs({ review_status: reviewStatus, limit: 500 })}`),
+  reviewAssertion: (assertionId: string, status: 'accepted' | 'rejected', reviewer: string, note = ''): Promise<GovernanceAssertion> =>
+    api.post(`/governance/assertions/${assertionId}/review`, { status, reviewer, note }),
+  issues: (projectId: string, status = 'open'): Promise<{ issues: GovernanceIssue[] }> =>
+    api.get(`/governance/projects/${projectId}/issues${qs({ status })}`),
+  updateIssue: (issueId: string, status: GovernanceIssue['status'], assignedTo = ''): Promise<GovernanceIssue> =>
+    api.put(`/governance/issues/${issueId}`, { status, assigned_to: assignedTo }),
+  rules: (projectId: string): Promise<{ rules: GovernanceRule[] }> =>
+    api.get(`/governance/projects/${projectId}/rules`),
+  validate: (projectId: string): Promise<ValidationResult> =>
+    api.post(`/governance/projects/${projectId}/validate`),
+  agentRuns: (projectId: string): Promise<{ runs: GovernanceAgentRun[] }> =>
+    api.get(`/governance/projects/${projectId}/agent-runs`),
+  analyze: (projectId: string, body: { source_type: string; content: string; source_name: string; environment: string; persist: boolean }) =>
+    api.post<{
+      run_id: string
+      status: string
+      result: { summary: { elements: number; assertions: number; issues: number }; agents: string[] }
+      trace: Array<{ term_name: string; duration_ms: number }>
+    }>(`/governance/projects/${projectId}/agent-runs/analyze`, body),
+}

+ 2 - 1
webui/src/components/Sidebar.tsx

@@ -1,5 +1,5 @@
 import { NavLink, useNavigate } from 'react-router-dom'
-import { LayoutDashboard, Bot, Cpu, LogOut, Zap, LibraryBig, Package, Plug } from 'lucide-react'
+import { LayoutDashboard, Bot, Cpu, LogOut, Zap, LibraryBig, Package, Plug, Network } from 'lucide-react'
 import { useAppStore } from '../store/app'
 import { clsx } from '../lib/clsx'
 import { useMode, MODE_META, type DeploymentMode } from '../api/mode'
@@ -14,6 +14,7 @@ function navItems(mode: DeploymentMode) {
     { to: '/dashboard', label: desktop ? '今日工作' : '仪表盘', icon: LayoutDashboard },
     { to: '/agents', label: '智能体', icon: Bot },
     { to: '/agentpacks', label: '智能体包', icon: Package },
+    { to: '/governance', label: '数据治理', icon: Network },
     { to: '/knowledge', label: desktop ? '资料库' : '知识体', icon: LibraryBig },
     { to: '/settings/providers', label: desktop ? '模型与隐私' : '模型提供商', icon: Cpu },
     { to: '/settings/connections', label: '工具与连接', icon: Plug },

+ 650 - 0
webui/src/pages/Governance.tsx

@@ -0,0 +1,650 @@
+import { useEffect, useMemo, useState } from 'react'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import {
+  AlertTriangle, Bot, CheckCircle2, ChevronRight, CircleDot, Database,
+  FileCheck2, GitBranch, Layers3, Network, Play, Plus, RefreshCw, Search,
+  ShieldCheck, Sparkles, Workflow, X,
+} from 'lucide-react'
+import toast from 'react-hot-toast'
+import {
+  governanceApi, type GovernanceAssertion, type GovernanceIssue,
+  type GovernanceEvidence, type OntologyElement, type OntologyLayer, type OntologyProfile,
+} from '../api/governance'
+import { Badge, Button, Card, EmptyState, Input, PageHeader, Spinner, Tabs, Textarea } from '../components/ui'
+
+const LAYERS: Array<{ id: OntologyLayer; label: string; description: string; tone: string }> = [
+  { id: 'M3', label: 'M3 建模内核', description: '定义如何建立本体', tone: 'border-violet-200 bg-violet-50' },
+  { id: 'M2', label: 'M2 元模型与 Profile', description: '定义业务、软件与治理语言', tone: 'border-blue-200 bg-blue-50' },
+  { id: 'M1', label: 'M1 项目模型', description: '正式模型与智能体候选区', tone: 'border-amber-200 bg-amber-50' },
+  { id: 'M0', label: 'M0 实例与事实', description: '真实资产、观测、测量和证据', tone: 'border-emerald-200 bg-emerald-50' },
+]
+
+const PROFILE_META: Record<OntologyProfile, { label: string; color: string }> = {
+  meta: { label: '建模内核', color: 'bg-violet-100 text-violet-700' },
+  upper: { label: '通用上层', color: 'bg-slate-100 text-slate-700' },
+  business: { label: '业务 Profile', color: 'bg-amber-100 text-amber-700' },
+  software: { label: '软件 Profile', color: 'bg-blue-100 text-blue-700' },
+  governance: { label: '治理 Profile', color: 'bg-emerald-100 text-emerald-700' },
+  cross: { label: '跨域关系', color: 'bg-fuchsia-100 text-fuchsia-700' },
+}
+
+const SAMPLE_DDL = `CREATE TABLE dcp_customer (
+  id BIGINT PRIMARY KEY,
+  tenant_id VARCHAR(64) NOT NULL,
+  customer_name VARCHAR(128) NOT NULL,
+  mobile VARCHAR(32),
+  email VARCHAR(128),
+  created_at TIMESTAMP NOT NULL
+);`
+
+function severityBadge(severity: string) {
+  if (severity === 'critical') return <Badge variant="red">严重</Badge>
+  if (severity === 'high') return <Badge variant="yellow">高</Badge>
+  if (severity === 'medium') return <Badge variant="blue">中</Badge>
+  return <Badge>低</Badge>
+}
+
+function reviewBadge(status: string) {
+  if (status === 'accepted') return <Badge variant="green">已接受</Badge>
+  if (status === 'rejected') return <Badge variant="red">已拒绝</Badge>
+  if (status === 'expired') return <Badge>已过期</Badge>
+  return <Badge variant="yellow">待审核</Badge>
+}
+
+function profileChip(profile: OntologyProfile) {
+  const meta = PROFILE_META[profile] ?? PROFILE_META.upper
+  return <span className={`inline-flex px-2 py-0.5 rounded-full text-[11px] font-medium ${meta.color}`}>{meta.label}</span>
+}
+
+export default function Governance() {
+  const qc = useQueryClient()
+  const [activeProjectId, setActiveProjectId] = useState('')
+  const [tab, setTab] = useState('overview')
+  const [layerFilter, setLayerFilter] = useState('')
+  const [profileFilter, setProfileFilter] = useState('')
+  const [query, setQuery] = useState('')
+  const [selectedElementId, setSelectedElementId] = useState('')
+  const [showProjectForm, setShowProjectForm] = useState(false)
+  const [projectName, setProjectName] = useState('')
+  const [projectNamespace, setProjectNamespace] = useState('https://onto.qm.cn/ontorefactor/model/')
+  const [showAnalyze, setShowAnalyze] = useState(false)
+  const [sourceType, setSourceType] = useState('ddl')
+  const [sourceName, setSourceName] = useState('customer-schema.sql')
+  const [environment, setEnvironment] = useState('development')
+  const [sourceContent, setSourceContent] = useState(SAMPLE_DDL)
+  const [lastAgentResult, setLastAgentResult] = useState<null | {
+    run_id: string
+    result: { summary: { elements: number; assertions: number; issues: number }; agents: string[] }
+    trace: Array<{ term_name: string; duration_ms: number }>
+  }>(null)
+
+  const projectsQ = useQuery({
+    queryKey: ['governance', 'projects'], queryFn: governanceApi.projects, retry: false,
+  })
+  const projects = projectsQ.data?.projects ?? []
+
+  useEffect(() => {
+    if (!activeProjectId && projects.length > 0) setActiveProjectId(projects[0].id)
+    if (activeProjectId && projects.length > 0 && !projects.some(p => p.id === activeProjectId)) {
+      setActiveProjectId(projects[0].id)
+    }
+  }, [projects, activeProjectId])
+
+  const refresh = () => qc.invalidateQueries({ queryKey: ['governance'] })
+  const enabled = Boolean(activeProjectId)
+  const overviewQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'overview'],
+    queryFn: () => governanceApi.overview(activeProjectId), enabled,
+  })
+  const elementsQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'elements', layerFilter, profileFilter, query],
+    queryFn: () => governanceApi.elements(activeProjectId, { layer: layerFilter, profile: profileFilter, q: query }),
+    enabled,
+  })
+  const graphQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'graph'],
+    queryFn: () => governanceApi.graph(activeProjectId), enabled,
+  })
+  const assertionsQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'assertions'],
+    queryFn: () => governanceApi.assertions(activeProjectId), enabled,
+  })
+  const evidenceQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'evidence'],
+    queryFn: () => governanceApi.evidence(activeProjectId), enabled,
+  })
+  const issuesQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'issues'],
+    queryFn: () => governanceApi.issues(activeProjectId), enabled,
+  })
+  const rulesQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'rules'],
+    queryFn: () => governanceApi.rules(activeProjectId), enabled,
+  })
+  const runsQ = useQuery({
+    queryKey: ['governance', activeProjectId, 'agent-runs'],
+    queryFn: () => governanceApi.agentRuns(activeProjectId), enabled,
+  })
+  const impactQ = useQuery({
+    queryKey: ['governance', selectedElementId, 'impact'],
+    queryFn: () => governanceApi.impact(selectedElementId), enabled: Boolean(selectedElementId),
+  })
+
+  const bootstrap = useMutation({
+    mutationFn: governanceApi.bootstrap,
+    onSuccess: data => {
+      setActiveProjectId(data.project.id)
+      refresh()
+      toast.success('已加载 M3-M0 租户隔离纵向样例')
+    },
+    onError: (error: Error) => toast.error(error.message),
+  })
+  const createProject = useMutation({
+    mutationFn: () => governanceApi.createProject({
+      name: projectName.trim(), description: 'OntoRefactor 四层本体项目',
+      namespace: projectNamespace.trim(),
+    }),
+    onSuccess: project => {
+      setShowProjectForm(false)
+      setActiveProjectId(project.id)
+      setProjectName('')
+      refresh()
+      toast.success('本体项目已创建')
+    },
+    onError: (error: Error) => toast.error(error.message),
+  })
+  const validate = useMutation({
+    mutationFn: () => governanceApi.validate(activeProjectId),
+    onSuccess: result => {
+      refresh()
+      result.valid ? toast.success('模型通过全部高优先级门禁') : toast.error(`发现 ${result.findings.length} 个治理问题`)
+    },
+    onError: (error: Error) => toast.error(error.message),
+  })
+  const analyze = useMutation({
+    mutationFn: () => governanceApi.analyze(activeProjectId, {
+      source_type: sourceType, content: sourceContent, source_name: sourceName,
+      environment, persist: true,
+    }),
+    onSuccess: result => {
+      setLastAgentResult(result)
+      refresh()
+      toast.success(`智能体已生成 ${result.result.summary.elements} 个元素候选`)
+    },
+    onError: (error: Error) => toast.error(error.message),
+  })
+  const review = useMutation({
+    mutationFn: ({ assertion, status }: { assertion: GovernanceAssertion; status: 'accepted' | 'rejected' }) =>
+      governanceApi.reviewAssertion(assertion.id, status, 'ontology-expert', 'Web 工作台人工审核'),
+    onSuccess: () => { refresh(); toast.success('断言审核状态已更新') },
+    onError: (error: Error) => toast.error(error.message),
+  })
+  const updateIssue = useMutation({
+    mutationFn: ({ issue, status }: { issue: GovernanceIssue; status: GovernanceIssue['status'] }) =>
+      governanceApi.updateIssue(issue.id, status, issue.assigned_to),
+    onSuccess: () => { refresh(); toast.success('问题状态已更新') },
+    onError: (error: Error) => toast.error(error.message),
+  })
+
+  const elementsByLayer = useMemo(() => {
+    const grouped: Record<OntologyLayer, OntologyElement[]> = { M3: [], M2: [], M1: [], M0: [] }
+    for (const element of elementsQ.data?.items ?? []) grouped[element.layer].push(element)
+    return grouped
+  }, [elementsQ.data])
+
+  if (projectsQ.isLoading) return <div className="p-12 flex justify-center"><Spinner size={28} /></div>
+
+  return (
+    <div className="p-8 max-w-7xl mx-auto">
+      <PageHeader
+        title="数据治理与本体"
+        description="以 M3-M0 四层元建模统一业务、软件、治理 Profile,用证据和人工审核约束智能体结论。"
+        action={(
+          <div className="flex gap-2">
+            <Button variant="secondary" icon={<Plus size={14} />} onClick={() => setShowProjectForm(true)}>新建项目</Button>
+            <Button icon={<Sparkles size={14} />} loading={bootstrap.isPending} onClick={() => bootstrap.mutate()}>加载纵向样例</Button>
+          </div>
+        )}
+      />
+
+      {projects.length === 0 ? (
+        <Card>
+          <EmptyState
+            icon={<Network size={46} />}
+            title="还没有本体项目"
+            description="建议先加载“租户隔离”纵向样例,它包含 M3 内核、M2 三个 Profile、M1 DCP 模型和 M0 运行事实。"
+            action={<Button loading={bootstrap.isPending} onClick={() => bootstrap.mutate()}>加载 M3-M0 示例</Button>}
+          />
+        </Card>
+      ) : (
+        <>
+          <div className="mb-6 flex items-center gap-3 bg-white border border-gray-200 rounded-xl px-4 py-3">
+            <Layers3 size={18} className="text-indigo-500" />
+            <select
+              value={activeProjectId}
+              onChange={event => setActiveProjectId(event.target.value)}
+              className="min-w-64 text-sm font-medium text-gray-900 bg-transparent focus:outline-none"
+            >
+              {projects.map(project => <option key={project.id} value={project.id}>{project.name} · v{project.version}</option>)}
+            </select>
+            <span className="text-xs text-gray-400 truncate flex-1">{overviewQ.data?.project.namespace}</span>
+            <Button variant="secondary" size="sm" icon={<RefreshCw size={13} />} loading={validate.isPending} onClick={() => validate.mutate()}>
+              执行治理门禁
+            </Button>
+            <Button size="sm" icon={<Bot size={13} />} onClick={() => { setTab('agents'); setShowAnalyze(true) }}>
+              智能抽取
+            </Button>
+          </div>
+
+          <Tabs
+            tabs={[
+              { id: 'overview', label: '治理总览' },
+              { id: 'model', label: '四层模型' },
+              { id: 'assertions', label: `断言审核${overviewQ.data?.totals.pending_reviews ? ` (${overviewQ.data.totals.pending_reviews})` : ''}` },
+              { id: 'evidence', label: `证据库${overviewQ.data?.totals.evidence ? ` (${overviewQ.data.totals.evidence})` : ''}` },
+              { id: 'issues', label: `问题与门禁${overviewQ.data?.totals.open_issues ? ` (${overviewQ.data.totals.open_issues})` : ''}` },
+              { id: 'agents', label: '智能体运行' },
+            ]}
+            active={tab}
+            onChange={setTab}
+          />
+
+          {tab === 'overview' && <OverviewTab overview={overviewQ.data} rules={rulesQ.data?.rules ?? []} edges={graphQ.data?.edges ?? []} />}
+          {tab === 'model' && (
+            <ModelTab
+              elementsByLayer={elementsByLayer}
+              loading={elementsQ.isLoading}
+              layerFilter={layerFilter}
+              profileFilter={profileFilter}
+              query={query}
+              setLayerFilter={setLayerFilter}
+              setProfileFilter={setProfileFilter}
+              setQuery={setQuery}
+              onSelect={setSelectedElementId}
+              edges={graphQ.data?.edges ?? []}
+            />
+          )}
+          {tab === 'assertions' && (
+            <AssertionsTab
+              assertions={assertionsQ.data?.assertions ?? []}
+              loading={assertionsQ.isLoading}
+              onReview={(assertion, status) => review.mutate({ assertion, status })}
+              reviewing={review.isPending}
+            />
+          )}
+          {tab === 'evidence' && (
+            <EvidenceTab evidence={evidenceQ.data?.evidence ?? []} loading={evidenceQ.isLoading} />
+          )}
+          {tab === 'issues' && (
+            <IssuesTab
+              issues={issuesQ.data?.issues ?? []}
+              loading={issuesQ.isLoading}
+              onUpdate={(issue, status) => updateIssue.mutate({ issue, status })}
+            />
+          )}
+          {tab === 'agents' && (
+            <AgentsTab
+              showAnalyze={showAnalyze}
+              setShowAnalyze={setShowAnalyze}
+              sourceType={sourceType}
+              setSourceType={setSourceType}
+              sourceName={sourceName}
+              setSourceName={setSourceName}
+              environment={environment}
+              setEnvironment={setEnvironment}
+              content={sourceContent}
+              setContent={setSourceContent}
+              onAnalyze={() => analyze.mutate()}
+              analyzing={analyze.isPending}
+              lastResult={lastAgentResult}
+              runs={runsQ.data?.runs ?? []}
+            />
+          )}
+        </>
+      )}
+
+      {showProjectForm && (
+        <Modal title="新建四层本体项目" onClose={() => setShowProjectForm(false)}>
+          <div className="space-y-4">
+            <Input label="项目名称" value={projectName} onChange={event => setProjectName(event.target.value)} placeholder="例如:DCP 身份认证域" />
+            <Input label="命名空间" value={projectNamespace} onChange={event => setProjectNamespace(event.target.value)} />
+            <div className="flex justify-end gap-2">
+              <Button variant="secondary" onClick={() => setShowProjectForm(false)}>取消</Button>
+              <Button disabled={!projectName.trim() || !projectNamespace.trim()} loading={createProject.isPending} onClick={() => createProject.mutate()}>创建</Button>
+            </div>
+          </div>
+        </Modal>
+      )}
+
+      {selectedElementId && (
+        <Modal title="影响路径" onClose={() => setSelectedElementId('')} wide>
+          {impactQ.isLoading ? <div className="py-10 flex justify-center"><Spinner /></div> : (
+            <div>
+              <div className="flex items-center gap-2 mb-4">
+                <Network size={18} className="text-indigo-500" />
+                <span className="font-semibold">{impactQ.data?.center.name}</span>
+                {impactQ.data?.center && profileChip(impactQ.data.center.profile)}
+              </div>
+              <div className="space-y-2 max-h-[55vh] overflow-y-auto">
+                {(impactQ.data?.nodes ?? []).map(node => (
+                  <div key={node.id} className="flex items-center gap-3 border border-gray-100 rounded-lg px-3 py-2">
+                    <span className="w-8 h-8 rounded-lg bg-gray-100 flex items-center justify-center text-xs font-bold text-gray-600">{node.layer}</span>
+                    <div className="min-w-0 flex-1">
+                      <p className="text-sm font-medium truncate">{node.name}</p>
+                      <p className="text-xs text-gray-400">{node.kind} · 距离 {node.distance}</p>
+                    </div>
+                    {profileChip(node.profile)}
+                  </div>
+                ))}
+              </div>
+              <p className="mt-4 text-xs text-gray-500">共命中 {impactQ.data?.nodes.length ?? 0} 个节点、{impactQ.data?.edges.length ?? 0} 条已接受断言。</p>
+            </div>
+          )}
+        </Modal>
+      )}
+    </div>
+  )
+}
+
+function OverviewTab({ overview, rules, edges }: {
+  overview?: Awaited<ReturnType<typeof governanceApi.overview>>
+  rules: Array<{ id: string; name: string; severity: string; description: string; enabled: boolean }>
+  edges: GovernanceAssertion[]
+}) {
+  if (!overview) return <div className="py-12 flex justify-center"><Spinner /></div>
+  const totals = overview.totals
+  return (
+    <div className="space-y-6">
+      <div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
+        {[
+          { label: '本体元素', value: totals.elements, icon: <Database size={18} />, color: 'text-blue-500' },
+          { label: '统一断言', value: totals.assertions, icon: <GitBranch size={18} />, color: 'text-violet-500' },
+          { label: '证据', value: totals.evidence, icon: <FileCheck2 size={18} />, color: 'text-emerald-500' },
+          { label: '待审核', value: totals.pending_reviews, icon: <CircleDot size={18} />, color: 'text-amber-500' },
+          { label: '开放问题', value: totals.open_issues, icon: <AlertTriangle size={18} />, color: totals.open_issues ? 'text-red-500' : 'text-gray-400' },
+        ].map(item => (
+          <Card key={item.label} className="p-4">
+            <div className="flex items-center justify-between"><span className="text-xs text-gray-500">{item.label}</span><span className={item.color}>{item.icon}</span></div>
+            <p className="text-2xl font-bold mt-2">{item.value}</p>
+          </Card>
+        ))}
+      </div>
+
+      <div className="grid lg:grid-cols-2 gap-5">
+        <Card>
+          <h3 className="text-sm font-semibold mb-4 flex items-center gap-2"><Layers3 size={16} className="text-indigo-500" />四层语义覆盖</h3>
+          <div className="space-y-3">
+            {LAYERS.map(layer => {
+              const count = overview.layers[layer.id] ?? 0
+              const width = Math.max(4, Math.round((count / Math.max(totals.elements, 1)) * 100))
+              return (
+                <div key={layer.id}>
+                  <div className="flex justify-between text-xs mb-1"><span className="font-medium">{layer.label}</span><span className="text-gray-400">{count}</span></div>
+                  <div className="h-2 bg-gray-100 rounded-full overflow-hidden"><div className="h-full bg-indigo-500 rounded-full" style={{ width: `${width}%` }} /></div>
+                </div>
+              )
+            })}
+          </div>
+        </Card>
+        <Card>
+          <h3 className="text-sm font-semibold mb-4 flex items-center gap-2"><Workflow size={16} className="text-emerald-500" />Profile 覆盖</h3>
+          <div className="grid grid-cols-2 gap-3">
+            {(Object.keys(PROFILE_META) as OntologyProfile[]).map(profile => (
+              <div key={profile} className="border border-gray-100 rounded-lg p-3">
+                {profileChip(profile)}
+                <p className="text-xl font-semibold mt-2">{overview.profiles[profile] ?? 0}</p>
+              </div>
+            ))}
+          </div>
+        </Card>
+      </div>
+
+      <div className="grid lg:grid-cols-2 gap-5">
+        <Card>
+          <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"><ShieldCheck size={16} className="text-emerald-500" />确定性治理门禁</h3>
+          <div className="space-y-2">
+            {rules.slice(0, 6).map(rule => (
+              <div key={rule.id} className="flex items-start gap-3 px-3 py-2 bg-gray-50 rounded-lg">
+                <CheckCircle2 size={15} className={rule.enabled ? 'text-emerald-500 mt-0.5' : 'text-gray-300 mt-0.5'} />
+                <div className="flex-1"><p className="text-sm font-medium">{rule.name}</p><p className="text-xs text-gray-500 mt-0.5">{rule.description}</p></div>
+                {severityBadge(rule.severity)}
+              </div>
+            ))}
+          </div>
+        </Card>
+        <Card>
+          <h3 className="text-sm font-semibold mb-3 flex items-center gap-2"><GitBranch size={16} className="text-violet-500" />最近关系</h3>
+          <div className="space-y-2">
+            {edges.slice(0, 7).map(edge => (
+              <div key={edge.id} className="flex items-center gap-2 text-xs border-b border-gray-50 pb-2">
+                <span className="font-medium truncate max-w-[130px]">{edge.subject_name ?? edge.subject_id}</span>
+                <ChevronRight size={12} className="text-gray-300" />
+                <code className="text-indigo-600 bg-indigo-50 px-1.5 py-0.5 rounded">{edge.predicate}</code>
+                <ChevronRight size={12} className="text-gray-300" />
+                <span className="font-medium truncate">{edge.object_name ?? edge.object_id}</span>
+              </div>
+            ))}
+          </div>
+        </Card>
+      </div>
+    </div>
+  )
+}
+
+function ModelTab({ elementsByLayer, loading, layerFilter, profileFilter, query, setLayerFilter, setProfileFilter, setQuery, onSelect, edges }: {
+  elementsByLayer: Record<OntologyLayer, OntologyElement[]>
+  loading: boolean
+  layerFilter: string
+  profileFilter: string
+  query: string
+  setLayerFilter: (value: string) => void
+  setProfileFilter: (value: string) => void
+  setQuery: (value: string) => void
+  onSelect: (id: string) => void
+  edges: GovernanceAssertion[]
+}) {
+  return (
+    <div className="space-y-5">
+      <Card className="p-4">
+        <div className="grid md:grid-cols-[1fr_160px_180px] gap-3">
+          <div className="relative"><Search size={15} className="absolute left-3 top-2.5 text-gray-400" /><input value={query} onChange={event => setQuery(event.target.value)} placeholder="搜索名称、URI 或描述" className="w-full border border-gray-300 rounded-lg pl-9 pr-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" /></div>
+          <select value={layerFilter} onChange={event => setLayerFilter(event.target.value)} className="border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white"><option value="">全部层级</option>{LAYERS.map(layer => <option key={layer.id} value={layer.id}>{layer.label}</option>)}</select>
+          <select value={profileFilter} onChange={event => setProfileFilter(event.target.value)} className="border border-gray-300 rounded-lg px-3 py-2 text-sm bg-white"><option value="">全部 Profile</option>{(Object.keys(PROFILE_META) as OntologyProfile[]).map(profile => <option key={profile} value={profile}>{PROFILE_META[profile].label}</option>)}</select>
+        </div>
+      </Card>
+      {loading ? <div className="py-10 flex justify-center"><Spinner /></div> : LAYERS.map(layer => (
+        (!layerFilter || layerFilter === layer.id) && (
+          <section key={layer.id} className={`border rounded-xl p-4 ${layer.tone}`}>
+            <div className="flex items-center justify-between mb-3">
+              <div><h3 className="text-sm font-semibold">{layer.label}</h3><p className="text-xs text-gray-500">{layer.description}</p></div>
+              <Badge>{elementsByLayer[layer.id].length} 个元素</Badge>
+            </div>
+            <div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-2">
+              {elementsByLayer[layer.id].map(element => (
+                <button key={element.id} onClick={() => onSelect(element.id)} className="bg-white/90 border border-white rounded-lg p-3 text-left hover:shadow-sm hover:border-indigo-200 transition-all">
+                  <div className="flex items-start justify-between gap-2"><span className="text-sm font-medium truncate">{element.name}</span><ChevronRight size={13} className="text-gray-300 mt-1 shrink-0" /></div>
+                  <p className="text-[11px] text-gray-400 mt-1 truncate">{element.kind}</p>
+                  <div className="mt-2 flex items-center justify-between">{profileChip(element.profile)}{element.status === 'candidate' ? <Badge variant="yellow">候选</Badge> : <Badge variant="green">正式</Badge>}</div>
+                </button>
+              ))}
+              {elementsByLayer[layer.id].length === 0 && <p className="text-xs text-gray-400 py-4">当前筛选下没有元素</p>}
+            </div>
+          </section>
+        )
+      ))}
+      <Card>
+        <h3 className="text-sm font-semibold mb-3">同层语义与层间实例化关系</h3>
+        <div className="grid md:grid-cols-2 gap-2 max-h-72 overflow-y-auto">
+          {edges.map(edge => (
+            <div key={edge.id} className="flex items-center gap-2 text-xs px-3 py-2 bg-gray-50 rounded-lg">
+              <span className="font-medium truncate flex-1">{edge.subject_name ?? edge.subject_id}</span>
+              <code className="text-indigo-600">{edge.predicate}</code>
+              <span className="font-medium truncate flex-1 text-right">{edge.object_name ?? edge.object_id}</span>
+              {reviewBadge(edge.review_status)}
+            </div>
+          ))}
+        </div>
+      </Card>
+    </div>
+  )
+}
+
+function AssertionsTab({ assertions, loading, onReview, reviewing }: {
+  assertions: GovernanceAssertion[]
+  loading: boolean
+  onReview: (assertion: GovernanceAssertion, status: 'accepted' | 'rejected') => void
+  reviewing: boolean
+}) {
+  if (loading) return <div className="py-10 flex justify-center"><Spinner /></div>
+  return (
+    <Card className="p-0 overflow-hidden">
+      <div className="px-5 py-4 border-b border-gray-100"><h3 className="text-sm font-semibold">Assertion + Evidence + Time + Review</h3><p className="text-xs text-gray-500 mt-1">算法置信度不代替人工审核;inferred 关系默认进入候选区。</p></div>
+      <div className="divide-y divide-gray-100">
+        {assertions.map(assertion => (
+          <div key={assertion.id} className="p-4">
+            <div className="flex items-start gap-3">
+              <div className="flex-1 min-w-0">
+                <div className="flex items-center gap-2 text-sm flex-wrap">
+                  <span className="font-semibold">{assertion.subject_name ?? assertion.subject_id}</span>
+                  <code className="text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded">{assertion.predicate}</code>
+                  <span className="font-semibold">{assertion.object_name || assertion.object_value || assertion.object_id}</span>
+                </div>
+                <div className="flex items-center gap-2 mt-2 text-xs text-gray-500 flex-wrap">
+                  <Badge variant={assertion.assertion_type === 'inferred' ? 'yellow' : 'blue'}>{assertion.assertion_type}</Badge>
+                  {reviewBadge(assertion.review_status)}
+                  <span>置信度 {(assertion.confidence * 100).toFixed(0)}%</span>
+                  <span>证据 {assertion.evidence_ids.length}</span>
+                  {assertion.generated_by && <span className="truncate max-w-sm">生成者 {assertion.generated_by}</span>}
+                </div>
+              </div>
+              {assertion.review_status === 'pending' && (
+                <div className="flex gap-2 shrink-0">
+                  <Button size="sm" loading={reviewing} onClick={() => onReview(assertion, 'accepted')}>接受</Button>
+                  <Button size="sm" variant="danger" disabled={reviewing} onClick={() => onReview(assertion, 'rejected')}>拒绝</Button>
+                </div>
+              )}
+            </div>
+          </div>
+        ))}
+        {assertions.length === 0 && <EmptyState icon={<FileCheck2 size={40} />} title="暂无断言" />}
+      </div>
+    </Card>
+  )
+}
+
+function IssuesTab({ issues, loading, onUpdate }: {
+  issues: GovernanceIssue[]
+  loading: boolean
+  onUpdate: (issue: GovernanceIssue, status: GovernanceIssue['status']) => void
+}) {
+  if (loading) return <div className="py-10 flex justify-center"><Spinner /></div>
+  if (issues.length === 0) return <Card><EmptyState icon={<CheckCircle2 size={44} />} title="没有开放的高优先级治理问题" description="再次执行治理门禁可刷新检查结果。" /></Card>
+  return (
+    <div className="space-y-3">
+      {issues.map(issue => (
+        <Card key={issue.id} className="p-4">
+          <div className="flex items-start gap-3">
+            <AlertTriangle size={18} className={issue.severity === 'critical' ? 'text-red-500 mt-0.5' : 'text-amber-500 mt-0.5'} />
+            <div className="flex-1"><div className="flex items-center gap-2"><h3 className="text-sm font-semibold">{issue.title}</h3>{severityBadge(issue.severity)}</div><p className="text-sm text-gray-600 mt-1">{issue.description}</p><p className="text-xs text-gray-400 mt-2">规则 {issue.rule_id} · 证据 {issue.evidence_ids.length}</p></div>
+            <div className="flex gap-2"><Button variant="secondary" size="sm" onClick={() => onUpdate(issue, 'acknowledged')}>认领</Button><Button size="sm" onClick={() => onUpdate(issue, 'resolved')}>已整改</Button></div>
+          </div>
+        </Card>
+      ))}
+    </div>
+  )
+}
+
+function EvidenceTab({ evidence, loading }: { evidence: GovernanceEvidence[]; loading: boolean }) {
+  if (loading) return <div className="py-10 flex justify-center"><Spinner /></div>
+  if (evidence.length === 0) return <Card><EmptyState icon={<FileCheck2 size={44} />} title="暂无治理证据" description="运行智能抽取或通过 API 接入源码、配置、运行观测和审批记录。" /></Card>
+  return (
+    <div className="grid md:grid-cols-2 gap-4">
+      {evidence.map(item => (
+        <Card key={item.id} className="p-4">
+          <div className="flex items-start gap-3">
+            <div className="w-9 h-9 rounded-lg bg-emerald-50 text-emerald-600 flex items-center justify-center shrink-0"><FileCheck2 size={17} /></div>
+            <div className="min-w-0 flex-1">
+              <div className="flex items-center gap-2"><h3 className="text-sm font-semibold truncate">{item.title}</h3><Badge variant="green">{item.kind}</Badge></div>
+              <p className="text-xs text-gray-500 mt-2 line-clamp-2">{item.content || '仅保存外部证据定位符'}</p>
+              <div className="mt-3 text-[11px] text-gray-400 space-y-1">
+                <p className="truncate">来源:{item.source || item.uri || '未填写'}</p>
+                <p>观测时间:{item.observed_at}</p>
+                <p className="font-mono truncate">SHA-256 {item.content_hash}</p>
+              </div>
+            </div>
+          </div>
+        </Card>
+      ))}
+    </div>
+  )
+}
+
+function AgentsTab({ showAnalyze, setShowAnalyze, sourceType, setSourceType, sourceName, setSourceName, environment, setEnvironment, content, setContent, onAnalyze, analyzing, lastResult, runs }: {
+  showAnalyze: boolean
+  setShowAnalyze: (value: boolean) => void
+  sourceType: string
+  setSourceType: (value: string) => void
+  sourceName: string
+  setSourceName: (value: string) => void
+  environment: string
+  setEnvironment: (value: string) => void
+  content: string
+  setContent: (value: string) => void
+  onAnalyze: () => void
+  analyzing: boolean
+  lastResult: null | { run_id: string; result: { summary: { elements: number; assertions: number; issues: number }; agents: string[] }; trace: Array<{ term_name: string; duration_ms: number }> }
+  runs: Array<{ id: string; source_type: string; status: string; trace_json: Array<{ term_name: string; duration_ms: number }>; error: string; created_at: string }>
+}) {
+  return (
+    <div className="space-y-5">
+      <Card className="border-indigo-200 bg-gradient-to-br from-indigo-50 to-white">
+        <div className="flex items-start justify-between gap-4">
+          <div><div className="flex items-center gap-2"><Bot size={18} className="text-indigo-600" /><h3 className="font-semibold">LambdAgent 治理抽取流水线</h3></div><p className="text-sm text-gray-600 mt-2 max-w-3xl">来源标准化后,资产发现、业务语义和治理控制三个智能体并行工作,再由综合智能体合并;确定性事实自动附证据,推断映射进入人工审核。</p></div>
+          <Button icon={<Play size={14} />} onClick={() => setShowAnalyze(!showAnalyze)}>{showAnalyze ? '收起' : '运行抽取'}</Button>
+        </div>
+        {showAnalyze && (
+          <div className="mt-5 pt-5 border-t border-indigo-100 space-y-4">
+            <div className="grid md:grid-cols-3 gap-3">
+              <label className="text-sm"><span className="block text-xs font-medium text-gray-600 mb-1">来源类型</span><select value={sourceType} onChange={event => setSourceType(event.target.value)} className="w-full border border-gray-300 rounded-lg px-3 py-2 bg-white"><option value="ddl">DDL</option><option value="openapi">OpenAPI</option><option value="inventory">JSON Inventory</option></select></label>
+              <Input label="来源名称" value={sourceName} onChange={event => setSourceName(event.target.value)} />
+              <Input label="环境" value={environment} onChange={event => setEnvironment(event.target.value)} />
+            </div>
+            <Textarea label="待分析内容" rows={12} value={content} onChange={event => setContent(event.target.value)} />
+            <div className="flex justify-end"><Button loading={analyzing} disabled={!content.trim()} icon={<Sparkles size={14} />} onClick={onAnalyze}>启动多智能体分析并入候选图</Button></div>
+          </div>
+        )}
+      </Card>
+      {lastResult && (
+        <Card>
+          <h3 className="text-sm font-semibold mb-3">本次运行结果 · {lastResult.run_id}</h3>
+          <div className="grid grid-cols-3 gap-3 mb-4">
+            <div className="bg-blue-50 rounded-lg p-3"><p className="text-xs text-blue-600">元素</p><p className="text-xl font-bold">{lastResult.result.summary.elements}</p></div>
+            <div className="bg-violet-50 rounded-lg p-3"><p className="text-xs text-violet-600">断言</p><p className="text-xl font-bold">{lastResult.result.summary.assertions}</p></div>
+            <div className="bg-amber-50 rounded-lg p-3"><p className="text-xs text-amber-600">问题</p><p className="text-xl font-bold">{lastResult.result.summary.issues}</p></div>
+          </div>
+          <div className="flex flex-wrap gap-2">{lastResult.trace.map((trace, index) => <Badge key={`${trace.term_name}-${index}`} variant="purple">β {trace.term_name} · {trace.duration_ms.toFixed(0)}ms</Badge>)}</div>
+        </Card>
+      )}
+      <Card className="p-0 overflow-hidden">
+        <div className="px-5 py-4 border-b border-gray-100"><h3 className="text-sm font-semibold">运行审计</h3></div>
+        <div className="divide-y divide-gray-100">
+          {runs.map(run => (
+            <div key={run.id} className="px-5 py-3 flex items-center gap-3"><Bot size={15} className="text-indigo-400" /><div className="flex-1"><p className="text-sm font-medium">{run.source_type.toUpperCase()} · {run.id}</p><p className="text-xs text-gray-400">{run.created_at} · {run.trace_json.length} 个 β 规约步骤</p></div>{run.status === 'completed' ? <Badge variant="green">已完成</Badge> : <Badge variant="red">失败</Badge>}</div>
+          ))}
+          {runs.length === 0 && <EmptyState icon={<Bot size={40} />} title="暂无智能体运行" />}
+        </div>
+      </Card>
+    </div>
+  )
+}
+
+function Modal({ title, onClose, wide = false, children }: { title: string; onClose: () => void; wide?: boolean; children: React.ReactNode }) {
+  return (
+    <div className="fixed inset-0 z-50 bg-black/30 flex items-center justify-center p-4" onClick={onClose}>
+      <div className={`bg-white rounded-2xl shadow-xl w-full ${wide ? 'max-w-3xl' : 'max-w-lg'} p-5`} onClick={event => event.stopPropagation()}>
+        <div className="flex items-center justify-between mb-5"><h2 className="text-base font-semibold">{title}</h2><button onClick={onClose} className="text-gray-400 hover:text-gray-600"><X size={18} /></button></div>
+        {children}
+      </div>
+    </div>
+  )
+}