Browse Source

feat: create standalone ontology governance platform

caozheng 2 months ago
commit
2ea92bd1cb

+ 10 - 0
.dockerignore

@@ -0,0 +1,10 @@
+.git
+.venv
+__pycache__
+.pytest_cache
+*.pyc
+*.db
+data
+build
+dist
+*.egg-info

+ 4 - 0
.env.example

@@ -0,0 +1,4 @@
+ONTOREFACTOR_DATABASE_URL=sqlite:///./ontorefactor.db
+ONTOREFACTOR_HOST=127.0.0.1
+ONTOREFACTOR_PORT=8010
+ONTOREFACTOR_DEFAULT_TENANT=local

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+.venv/
+__pycache__/
+*.py[cod]
+*.db
+*.db-*
+.env
+data/
+.pytest_cache/
+dist/
+build/
+*.egg-info/

+ 26 - 0
Dockerfile

@@ -0,0 +1,26 @@
+FROM python:3.12-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+    PYTHONUNBUFFERED=1 \
+    ONTOREFACTOR_DATABASE_URL=sqlite:////data/ontorefactor.db \
+    ONTOREFACTOR_HOST=0.0.0.0 \
+    ONTOREFACTOR_PORT=8010
+
+WORKDIR /app
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends git ca-certificates \
+    && rm -rf /var/lib/apt/lists/*
+COPY pyproject.toml README.md ./
+COPY src ./src
+COPY ontology ./ontology
+RUN python -m pip install --no-cache-dir . \
+    && addgroup --system ontorefactor \
+    && adduser --system --ingroup ontorefactor ontorefactor \
+    && mkdir -p /data \
+    && chown ontorefactor:ontorefactor /data
+
+USER ontorefactor
+VOLUME ["/data"]
+EXPOSE 8010
+
+CMD ["ontorefactor-governance", "--host", "0.0.0.0", "--port", "8010"]

+ 128 - 0
README.md

@@ -0,0 +1,128 @@
+# OntoRefactor Governance
+
+OntoRefactor Governance 是一个可独立部署的、由本体驱动的数据治理平台。它把业务概念、软件资产、数据对象、治理规则、证据与运行事实统一到 M3–M0 四层模型中,并通过 LambdAgent 流水线从 DDL、OpenAPI 或资产清单自动发现治理对象。
+
+它与 `lambdagentpaas` 已解耦:运行时只依赖可由 pip 从 Gogs 安装的 `lambdagent` Python 包,不导入 AgentPaaS 的 API、数据库、鉴权、前端或配置。AgentPaaS 可以作为以后可选的部署适配器,但不是本项目启动的前置条件。
+
+## 这是什么系统
+
+平台解决六件事:
+
+1. 用 M3–M0 本体表达“元模型—领域类型—项目模型—运行事实”。
+2. 管理业务、软件/数据、治理三个 Profile 及其跨域关系。
+3. 把来源、置信度、生成者、有效期和人工复核绑定到每条语义断言。
+4. 从 DDL、OpenAPI、JSON 资产清单发现模型,并把候选关系送入人工闭环。
+5. 执行层级、来源、Owner、敏感字段策略、业务到软件映射等确定性门禁。
+6. 提供影响分析、治理问题、智能体轨迹和 JSON-LD 导出。
+
+## 快速开始
+
+需要 Python 3.10 或更高版本以及 Git。`pip install` 会从你指定的 Gogs 仓库安装其中的 `lambdagent` 子项目。
+
+```powershell
+cd ontorefactor-governance
+python -m venv .venv
+.\.venv\Scripts\Activate.ps1
+python -m pip install -U pip
+pip install -e ".[dev]"
+ontorefactor-governance
+```
+
+打开 [http://127.0.0.1:8010](http://127.0.0.1:8010),点击“创建演示项目”即可得到一个通过治理校验的租户隔离参考模型。交互式 API 文档位于 [http://127.0.0.1:8010/docs](http://127.0.0.1:8010/docs)。
+
+默认数据文件是当前目录下的 `ontorefactor.db`。可通过环境变量修改:
+
+```powershell
+$env:ONTOREFACTOR_DATABASE_URL = "sqlite:///./data/governance.db"
+$env:ONTOREFACTOR_DEFAULT_TENANT = "my-team"
+$env:ONTOREFACTOR_API_KEY = "replace-with-a-secret"
+ontorefactor-governance --host 0.0.0.0 --port 8010
+```
+
+启用 `ONTOREFACTOR_API_KEY` 后,客户端需要发送 `Authorization: Bearer <key>`。生产环境还应由网关验证用户身份并生成可信的 `X-Tenant-ID`;不要把未经验证的租户请求头直接暴露到公网。
+
+## 如何使用
+
+- 治理总览:观察模型数量、开放问题、待审断言和 M3–M0 分布。
+- 本体模型:检索业务、软件、数据和治理元素,点击对象执行关系影响分析。
+- 语义断言:接受或驳回智能体推断出的候选关系。
+- 证据中心:追溯断言对应的 DDL、OpenAPI、运行观测或人工声明。
+- 治理问题:运行规则并把高风险问题推进到关闭状态。
+- 智能体工作台:粘贴 DDL、OpenAPI YAML/JSON 或资产清单,查看执行轨迹和落库结果。
+- JSON-LD:在“本体模型”中导出当前项目,供图数据库、语义工具或其他平台使用。
+
+资产清单格式如下:
+
+```json
+{
+  "tables": [
+    {
+      "schema": "public",
+      "name": "customer",
+      "columns": [
+        {"name": "id", "data_type": "INTEGER", "primary_key": true},
+        {"name": "mobile", "data_type": "VARCHAR", "sensitive": true}
+      ]
+    }
+  ],
+  "apis": [],
+  "schemas": []
+}
+```
+
+## 为什么仍然有多个“智能体”
+
+这里没有启动许多微服务,也没有让多个大模型自由协商。系统只有一条进程内 LambdAgent 流水线:
+
+```text
+输入规范化
+    ├── 资产发现
+    ├── 业务语义推断      (三个独立分析分支并行)
+    └── 治理控制分析
+            ↓
+      合并、去重、校验、落库
+```
+
+这些名称表达职责边界,便于单独测试和替换。资产发现与门禁是确定性代码;业务语义推断产生 `pending` 断言,不能直接触发高风险动作。每次执行只有一个 `run_id`,完整轨迹保存在同一个 SQLite 数据库中。
+
+## 本体分层
+
+| 层级 | 表达内容                             | 平台中的例子                                              |
+| ---- | ------------------------------------ | --------------------------------------------------------- |
+| M3   | 如何定义类型、关系、属性、约束、断言 | `MetaEntityType`、`MetaAssertionType`                 |
+| M2   | 跨项目复用的领域类型                 | `BusinessCapability`、`Table`、`QualityRule`        |
+| M1   | 某项目的逻辑模型                     | `Tenant`、`DcpTenantTable`、`TenantIsolationPolicy` |
+| M0   | 真实运行实例与观测                   | 生产列、代码提交、质量测量                                |
+
+版本化 Turtle 和 SHACL 文件位于 [`ontology`](ontology/README.md)。数据库关系被重新实体化为断言,因此每条关系都能携带证据、置信度、生成者、复核状态与有效期。
+
+## 容器运行
+
+```powershell
+docker compose up --build
+```
+
+控制台仍位于 `http://127.0.0.1:8010`,SQLite 数据持久化在 Docker 命名卷 `governance-data` 中。
+
+## 开发与验证
+
+```powershell
+pip install -e ".[dev]"
+pytest -q
+```
+
+测试覆盖租户隔离、参考模型幂等性、治理校验、影响分析、JSON-LD、LambdAgent 执行与审计、HTTP API,以及全部 Turtle 资源解析。
+
+## 独立边界
+
+```text
+ontorefactor-governance
+├── FastAPI + 静态 Web 控制台
+├── SQLite 治理存储
+├── M3–M0 本体与 SHACL
+└── lambdagent(pip 依赖)
+
+lambdagentpaas(可选、当前无运行时依赖)
+```
+
+更细的决策与扩展点见 [`docs/architecture.md`](docs/architecture.md)。

+ 14 - 0
compose.yaml

@@ -0,0 +1,14 @@
+services:
+  governance:
+    build: .
+    ports:
+      - "8010:8010"
+    environment:
+      ONTOREFACTOR_DEFAULT_TENANT: local
+      ONTOREFACTOR_API_KEY: ${ONTOREFACTOR_API_KEY:-}
+    volumes:
+      - governance-data:/data
+    restart: unless-stopped
+
+volumes:
+  governance-data:

+ 49 - 0
docs/architecture.md

@@ -0,0 +1,49 @@
+# 架构与扩展边界
+
+## 模块职责
+
+- `db.py`:唯一持久化边界,提供租户隔离的 SQLite 表与事务。
+- `service.py`:本体、断言、证据、规则、问题、影响分析和导出领域逻辑。
+- `agents.py`:只使用 `lambdagent` 公共 API 组合治理流水线。
+- `api.py`:FastAPI 输入校验、租户作用域、认证开关与静态页面托管。
+- `static/`:无 Node.js 构建依赖的治理控制台。
+- `ontology/`:版本化 M3–M0 Turtle 与 SHACL 语义合同。
+
+依赖方向固定为 `API → service/agents → db`,`agents → lambdagent`。领域层不知道 AgentPaaS,也不依赖 FastAPI。
+
+## 数据和智能体的控制边界
+
+| 治理部分 | 智能体表达 | 确定性控制 |
+|---|---|---|
+| 资产目录 | 从 DDL/OpenAPI/清单抽取表、列、API、Schema | 唯一 URI、字段校验、租户作用域 |
+| 业务术语 | 根据名称提出业务对象候选 | 人工接受后才成为正式断言 |
+| 血缘与关系 | 生成 `CONTAINS`、`INSTANCE_OF`、`REPRESENTS` | 层级邻接、同层继承、证据完整性 |
+| 数据质量 | 识别可能需要质量规则的对象 | 阈值和门禁由规则引擎执行 |
+| 安全与合规 | 标记疑似敏感字段 | 敏感生产字段必须关联策略和 Owner |
+| 责任体系 | 推荐责任域和待确认对象 | 正式 Owner 关系需要声明或审核 |
+| 变更影响 | 沿已接受断言构建影响子图 | 最大深度、租户边界和状态过滤 |
+| 审计 | 保留每一步轨迹和输入哈希 | 运行、证据、断言、审核状态持久化 |
+
+原则是“智能体提出候选,确定性规则裁决,人工负责高风险确认”。
+
+## 替换存储
+
+SQLite 是独立版的默认实现。迁移到 PostgreSQL 或图数据库时,应保留 `service.py` 使用的数据合同:
+
+- 所有实体都包含 `tenant_id` 与 `project_id`。
+- 元素 URI 在项目内唯一。
+- 关系以断言实体存在,不能压缩成无来源的裸边。
+- `accepted` 的非声明断言必须包含 evidence 和 generatedBy。
+- M0 已发布事实必须实例化一个 M1 类型。
+
+可以新增 repository 接口并保留现有 SQLite 作为本地和测试实现,不需要改智能体的发现结果协议。
+
+## 与 AgentPaaS 的可选集成
+
+如果将来要放回 AgentPaaS,只新增外层适配器:
+
+- 把 PaaS 身份映射为 `tenant_id` 和 reviewer。
+- 把平台任务请求转发到独立 API。
+- 把 `run_id`、结果摘要和链接回写到 PaaS。
+
+不要让独立项目导入 AgentPaaS 的 ORM、路由或配置。这样 Web 控制台、命令行、容器和 PaaS 四种入口可以共享同一治理内核。

+ 14 - 0
ontology/README.md

@@ -0,0 +1,14 @@
+# OntoRefactor 本体包
+
+该目录是平台的可交换语义合同,与 SQLite 运行时模型保持同一套 M3–M0 结构。
+
+- `m3/meta-core.ttl`:稳定元模型、可追溯断言和层级属性。
+- `m2/upper.ttl`:对象、主体、活动、规则、结果、证据等通用概念。
+- `m2/business.ttl`:业务能力、业务对象、过程、事件与业务规则。
+- `m2/software.ttl`:模块、类、API、表、列与事件契约。
+- `m2/governance.ttl`:Owner、Steward、策略、质量规则、门禁和测量。
+- `m2/cross-profile.ttl`:连接业务、软件与治理对象的受控关系。
+- `m1/dcp-tenant-model.ttl`:租户隔离领域的 M1 模型和 M0 事实样例。
+- `shapes/governance-shapes.ttl`:可由 SHACL 引擎执行的确定性治理门禁。
+
+平台 API 可把数据库中的项目导出为 JSON-LD;静态 TTL 用于版本管理、跨系统交换和本体审查。

+ 29 - 0
ontology/m1/dcp-tenant-model.ttl

@@ -0,0 +1,29 @@
+@prefix dcp: <https://onto.qm.cn/ontorefactor/model/dcp/1.0#> .
+@prefix biz: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix sw: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix gov: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix or: <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/model/dcp/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/cross/1.0> ;
+  rdfs:label "DCP 租户与组织域项目本体"@zh ; owl:versionInfo "1.0.0" .
+
+dcp:TenantManagement a biz:BusinessCapability ; rdfs:label "租户管理能力"@zh ; or:layer "M1" ; or:profile "business" ; cross:realizedBy dcp:BaseIAM .
+dcp:Tenant a biz:BusinessObject ; rdfs:label "租户"@zh ; or:layer "M1" ; or:profile "business" ; upper:governedBy dcp:TenantIsolation .
+dcp:TenantIsolation a biz:BusinessRule ; rdfs:label "租户隔离规则"@zh ; or:layer "M1" ; or:profile "business" .
+
+dcp:BaseIAM a sw:Module ; rdfs:label "统一身份模块"@zh ; or:layer "M1" ; or:profile "software" ; cross:ownedBy dcp:IAMDataOwner ; sw:exposes dcp:CreateTenantAPI .
+dcp:CreateTenantAPI a sw:ApiOperation ; rdfs:label "创建租户 API"@zh ; or:layer "M1" ; or:profile "software" ; cross:operatesOn dcp:Tenant .
+dcp:DcpTenantTable a sw:Table ; rdfs:label "租户数据表"@zh ; or:layer "M1" ; or:profile "software" ; sw:containsColumn dcp:TenantIdColumn ; cross:represents dcp:Tenant ; cross:ownedBy dcp:IAMDataOwner .
+dcp:TenantIdColumn a sw:Column ; rdfs:label "租户标识列"@zh ; or:layer "M1" ; or:profile "software" ; cross:governedBy dcp:TenantIsolationPolicy ; cross:validatedBy dcp:TenantIdCompleteness .
+
+dcp:IAMDataOwner a gov:Owner ; rdfs:label "IAM 数据责任人"@zh ; or:layer "M1" ; or:profile "governance" .
+dcp:TenantIsolationPolicy a gov:AccessPolicy ; rdfs:label "租户隔离访问策略"@zh ; or:layer "M1" ; or:profile "governance" .
+dcp:TenantIdCompleteness a gov:QualityRule ; rdfs:label "租户标识完整性"@zh ; or:layer "M1" ; or:profile "governance" ; gov:threshold 1.0 .
+
+<db://prod/dcp_tenant/tenant_id> a dcp:TenantIdColumn ; rdfs:label "生产库 tenant_id"@zh ; or:layer "M0" ; or:profile "software" ; cross:ownedBy dcp:IAMDataOwner ; cross:governedBy dcp:TenantIsolationPolicy .
+<measurement:tenant-id:20260817> a gov:Measurement ; rdfs:label "租户标识完整性测量"@zh ; or:layer "M0" ; or:profile "governance" ; gov:measuredValue 0.9987 .

+ 19 - 0
ontology/m2/business.ttl

@@ -0,0 +1,19 @@
+@prefix biz: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix or: <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> ;
+  rdfs:label "业务本体剖面"@zh ; owl:versionInfo "1.0.0" .
+
+biz:BusinessCapability a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "业务能力"@zh ; or:layer "M2" ; or:profile "business" .
+biz:BusinessObject a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "业务对象"@zh ; or:layer "M2" ; or:profile "business" .
+biz:BusinessProcess a owl:Class ; rdfs:subClassOf upper:Activity ; rdfs:label "业务过程"@zh ; or:layer "M2" ; or:profile "business" .
+biz:BusinessEvent a owl:Class ; rdfs:subClassOf upper:Result ; rdfs:label "业务事件"@zh ; or:layer "M2" ; or:profile "business" .
+biz:BusinessRule a owl:Class ; rdfs:subClassOf upper:Rule ; rdfs:label "业务规则"@zh ; or:layer "M2" ; or:profile "business" .
+
+biz:requiresCapability a owl:ObjectProperty ; rdfs:domain biz:BusinessProcess ; rdfs:range biz:BusinessCapability .
+biz:operatesOn a owl:ObjectProperty ; rdfs:domain biz:BusinessProcess ; rdfs:range biz:BusinessObject .
+biz:emitsEvent a owl:ObjectProperty ; rdfs:domain biz:BusinessProcess ; rdfs:range biz:BusinessEvent .

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

@@ -0,0 +1,19 @@
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix biz: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix sw: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix gov: <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#> .
+
+<https://onto.qm.cn/ontorefactor/cross/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/business/1.0>,
+              <https://onto.qm.cn/ontorefactor/software/1.0>,
+              <https://onto.qm.cn/ontorefactor/governance/1.0> ;
+  rdfs:label "跨剖面映射本体"@zh ; owl:versionInfo "1.0.0" .
+
+cross:realizedBy a owl:ObjectProperty ; rdfs:domain biz:BusinessCapability ; rdfs:range sw:Module ; rdfs:label "由软件实现"@zh .
+cross:represents a owl:ObjectProperty ; rdfs:domain sw:Table ; rdfs:range biz:BusinessObject ; rdfs:label "表达业务对象"@zh .
+cross:operatesOn a owl:ObjectProperty ; rdfs:domain sw:ApiOperation ; rdfs:range biz:BusinessObject ; rdfs:label "操作业务对象"@zh .
+cross:ownedBy a owl:ObjectProperty ; rdfs:range gov:Owner ; rdfs:label "由责任人负责"@zh .
+cross:governedBy a owl:ObjectProperty ; rdfs:range gov:AccessPolicy ; rdfs:label "受策略治理"@zh .
+cross:validatedBy a owl:ObjectProperty ; rdfs:range gov:QualityRule ; rdfs:label "由质量规则验证"@zh .

+ 24 - 0
ontology/m2/governance.ttl

@@ -0,0 +1,24 @@
+@prefix gov: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix or: <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/governance/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/upper/1.0> ;
+  rdfs:label "数据治理本体剖面"@zh ; owl:versionInfo "1.0.0" .
+
+gov:Owner a owl:Class ; rdfs:subClassOf upper:Subject ; rdfs:label "数据责任人"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:DataSteward a owl:Class ; rdfs:subClassOf upper:Subject ; rdfs:label "数据管理员"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:AccessPolicy a owl:Class ; rdfs:subClassOf upper:Rule ; rdfs:label "访问策略"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:QualityRule a owl:Class ; rdfs:subClassOf upper:Rule ; rdfs:label "质量规则"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:QualityGate a owl:Class ; rdfs:subClassOf upper:Rule ; rdfs:label "质量门禁"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:Classification a owl:Class ; rdfs:subClassOf upper:Term ; rdfs:label "分类分级"@zh ; or:layer "M2" ; or:profile "governance" .
+gov:Measurement a owl:Class ; rdfs:subClassOf upper:Result ; rdfs:label "质量测量"@zh ; or:layer "M2" ; or:profile "governance" .
+
+gov:validates a owl:ObjectProperty ; rdfs:domain gov:QualityRule ; rdfs:range upper:Object .
+gov:evaluates a owl:ObjectProperty ; rdfs:domain gov:QualityGate ; rdfs:range upper:Object .
+gov:classifiedAs a owl:ObjectProperty ; rdfs:range gov:Classification .
+gov:threshold a owl:DatatypeProperty ; rdfs:domain gov:QualityRule ; rdfs:range xsd:decimal .
+gov:measuredValue a owl:DatatypeProperty ; rdfs:domain gov:Measurement ; rdfs:range xsd:decimal .

+ 21 - 0
ontology/m2/software.ttl

@@ -0,0 +1,21 @@
+@prefix sw: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix or: <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> ;
+  rdfs:label "软件与数据本体剖面"@zh ; owl:versionInfo "1.0.0" .
+
+sw:Module a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "软件模块"@zh ; or:layer "M2" ; or:profile "software" .
+sw:Class a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "代码类"@zh ; or:layer "M2" ; or:profile "software" .
+sw:ApiOperation a owl:Class ; rdfs:subClassOf upper:Activity ; rdfs:label "API 操作"@zh ; or:layer "M2" ; or:profile "software" .
+sw:Table a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "数据表"@zh ; or:layer "M2" ; or:profile "software" .
+sw:Column a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "数据列"@zh ; or:layer "M2" ; or:profile "software" .
+sw:EventContract a owl:Class ; rdfs:subClassOf upper:Object ; rdfs:label "事件契约"@zh ; or:layer "M2" ; or:profile "software" .
+
+sw:belongsTo a owl:ObjectProperty ; rdfs:range sw:Module .
+sw:containsColumn a owl:ObjectProperty ; rdfs:domain sw:Table ; rdfs:range sw:Column .
+sw:exposes a owl:ObjectProperty ; rdfs:domain sw:Module ; rdfs:range sw:ApiOperation .
+sw:calls a owl:ObjectProperty ; rdfs:domain sw:ApiOperation ; rdfs:range sw:ApiOperation .

+ 25 - 0
ontology/m2/upper.ttl

@@ -0,0 +1,25 @@
+@prefix upper: <https://onto.qm.cn/ontorefactor/upper/1.0#> .
+@prefix or: <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/upper/1.0> a owl:Ontology ;
+  owl:imports <https://onto.qm.cn/ontorefactor/meta/1.0> ;
+  rdfs:label "OntoRefactor 通用上层本体"@zh ;
+  owl:versionInfo "1.0.0" .
+
+upper:Object a owl:Class ; rdfs:label "对象"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Subject a owl:Class ; rdfs:label "主体"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Activity a owl:Class ; rdfs:label "活动"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Rule a owl:Class ; rdfs:label "规则"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Result a owl:Class ; rdfs:label "结果"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Evidence a owl:Class ; rdfs:label "证据"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Assertion a owl:Class ; rdfs:label "断言"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:Term a owl:Class ; rdfs:label "术语"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:State a owl:Class ; rdfs:label "状态"@zh ; or:layer "M2" ; or:profile "upper" .
+upper:TimeExtent a owl:Class ; rdfs:label "时间范围"@zh ; or:layer "M2" ; or:profile "upper" .
+
+upper:ownedBy a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range upper:Subject ; rdfs:label "由……负责"@zh .
+upper:governedBy a owl:ObjectProperty ; rdfs:domain upper:Object ; rdfs:range upper:Rule ; rdfs:label "由……治理"@zh .
+upper:produces a owl:ObjectProperty ; rdfs:domain upper:Activity ; rdfs:range upper:Result ; rdfs:label "产生"@zh .
+upper:supportedBy a owl:ObjectProperty ; rdfs:range upper:Evidence ; rdfs:label "证据支持"@zh .

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

@@ -0,0 +1,37 @@
+@prefix or: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+@prefix owl: <http://www.w3.org/2002/07/owl#> .
+@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+<https://onto.qm.cn/ontorefactor/meta/1.0> a owl:Ontology ;
+  rdfs:label "OntoRefactor M3 元模型"@zh ;
+  owl:versionInfo "1.0.0" .
+
+or:MetaElement a owl:Class ; rdfs:label "元元素"@zh .
+or:MetaEntityType a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元实体类型"@zh .
+or:MetaRelationType a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元关系类型"@zh .
+or:MetaAttributeType a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元属性类型"@zh .
+or:MetaConstraintType a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元约束类型"@zh .
+or:MetaAssertionType a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元断言类型"@zh .
+or:MetaProfile a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元剖面"@zh .
+or:MetaView a owl:Class ; rdfs:subClassOf or:MetaElement ; rdfs:label "元视图"@zh .
+
+or:Assertion a owl:Class ; rdfs:label "可追溯断言"@zh .
+or:subject a owl:ObjectProperty ; rdfs:domain or:Assertion .
+or:predicate a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:string .
+or:object a owl:ObjectProperty ; rdfs:domain or:Assertion .
+or:objectValue a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:string .
+or:assertionType a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:string .
+or:confidence a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:decimal .
+or:evidence a owl:ObjectProperty ; rdfs:domain or:Assertion .
+or:generatedBy a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:string .
+or:reviewStatus a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:string .
+or:validFrom a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:dateTime .
+or:validTo a owl:DatatypeProperty ; rdfs:domain or:Assertion ; rdfs:range xsd:dateTime .
+
+or:layer a owl:DatatypeProperty ; rdfs:range xsd:string .
+or:profile a owl:DatatypeProperty ; rdfs:range xsd:string .
+or:status a owl:DatatypeProperty ; rdfs:range xsd:string .
+or:instanceOf a owl:ObjectProperty ; rdfs:label "实例化"@zh .
+or:subTypeOf a owl:ObjectProperty ; rdfs:label "子类型"@zh .

+ 25 - 0
ontology/shapes/governance-shapes.ttl

@@ -0,0 +1,25 @@
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+@prefix biz: <https://onto.qm.cn/ontorefactor/business/1.0#> .
+@prefix sw: <https://onto.qm.cn/ontorefactor/software/1.0#> .
+@prefix gov: <https://onto.qm.cn/ontorefactor/governance/1.0#> .
+@prefix cross: <https://onto.qm.cn/ontorefactor/cross/1.0#> .
+@prefix or: <https://onto.qm.cn/ontorefactor/meta/1.0#> .
+
+<https://onto.qm.cn/ontorefactor/shapes/1.0> a sh:NodeShape .
+
+<https://onto.qm.cn/ontorefactor/shapes/CapabilityRealization> a sh:NodeShape ;
+  sh:targetClass biz:BusinessCapability ;
+  sh:property [ sh:path cross:realizedBy ; sh:minCount 1 ; sh:class sw:Module ; sh:message "业务能力必须至少有一个软件模块实现"@zh ] .
+
+<https://onto.qm.cn/ontorefactor/shapes/TableOwnership> a sh:NodeShape ;
+  sh:targetClass sw:Table ;
+  sh:property [ sh:path cross:ownedBy ; sh:minCount 1 ; sh:class gov:Owner ; sh:message "数据表必须明确数据责任人"@zh ] .
+
+<https://onto.qm.cn/ontorefactor/shapes/AssertionProvenance> a sh:NodeShape ;
+  sh:targetClass or:Assertion ;
+  sh:property [ sh:path or:subject ; sh:minCount 1 ] ;
+  sh:property [ sh:path or:predicate ; sh:minCount 1 ; sh:datatype xsd:string ] ;
+  sh:property [ sh:path or:assertionType ; sh:minCount 1 ; sh:in ("declared" "extracted" "observed" "inferred") ] ;
+  sh:property [ sh:path or:confidence ; sh:minInclusive 0 ; sh:maxInclusive 1 ] ;
+  sh:property [ sh:path or:reviewStatus ; sh:minCount 1 ; sh:in ("pending" "accepted" "rejected" "expired") ] .

+ 46 - 0
pyproject.toml

@@ -0,0 +1,46 @@
+[build-system]
+requires = ["setuptools>=77", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "ontorefactor-governance"
+version = "0.1.0"
+description = "Standalone M3-M0 ontology-driven data governance platform powered by LambdAgent"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {text = "BUSL-1.1"}
+dependencies = [
+  "lambdagent @ git+https://gogs.seec.seecoder.cn/CaoZheng/lambdagent.git@226148f6260bf5ee82346bf101319cc7c6b6d82a#subdirectory=lambdagent",
+  "fastapi>=0.100,<1.0",
+  "uvicorn[standard]>=0.20",
+  "pydantic>=2,<3",
+  "pyyaml>=6,<7",
+]
+
+[project.optional-dependencies]
+dev = ["pytest>=7", "httpx>=0.24", "rdflib>=7"]
+
+[project.scripts]
+ontorefactor-governance = "ontorefactor_governance.__main__:main"
+
+[tool.setuptools]
+package-dir = {"" = "src"}
+include-package-data = true
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+[tool.setuptools.package-data]
+ontorefactor_governance = ["static/*"]
+
+[tool.setuptools.data-files]
+"share/ontorefactor-governance/ontology/m3" = ["ontology/m3/*.ttl"]
+"share/ontorefactor-governance/ontology/m2" = ["ontology/m2/*.ttl"]
+"share/ontorefactor-governance/ontology/m1" = ["ontology/m1/*.ttl"]
+"share/ontorefactor-governance/ontology/shapes" = ["ontology/shapes/*.ttl"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+filterwarnings = [
+  "ignore:Using `httpx` with `starlette.testclient` is deprecated:starlette.exceptions.StarletteDeprecationWarning",
+]

+ 3 - 0
src/ontorefactor_governance/__init__.py

@@ -0,0 +1,3 @@
+"""Standalone OntoRefactor data-governance platform."""
+
+__version__ = "0.1.0"

+ 18 - 0
src/ontorefactor_governance/__main__.py

@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+import argparse
+import os
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="OntoRefactor standalone governance platform")
+    parser.add_argument("--host", default=os.getenv("ONTOREFACTOR_HOST", "127.0.0.1"))
+    parser.add_argument("--port", type=int, default=int(os.getenv("ONTOREFACTOR_PORT", "8010")))
+    parser.add_argument("--reload", action="store_true")
+    args = parser.parse_args()
+    import uvicorn
+    uvicorn.run("ontorefactor_governance.api:app", host=args.host, port=args.port, reload=args.reload)
+
+
+if __name__ == "__main__":
+    main()

+ 285 - 0
src/ontorefactor_governance/agents.py

@@ -0,0 +1,285 @@
+"""Deterministic governance analysis composed with the pip LambdAgent SDK."""
+from __future__ import annotations
+
+import hashlib
+import json
+import re
+from dataclasses import asdict
+from typing import Any
+
+import yaml
+from lambdagent import Context, Guard, Par, Tool
+
+from .db import gen_id, now_utc
+from .service import (
+    GovernanceError, create_assertion, create_evidence, overview,
+    upsert_element, validate_project,
+)
+
+
+SOURCE_TYPES = ("ddl", "openapi", "inventory")
+_SENSITIVE = ("mobile", "phone", "email", "id_card", "idcard", "password", "secret", "token", "bank", "address", "手机号", "身份证", "邮箱")
+
+
+def _safe(value: str) -> str:
+    return re.sub(r"[^A-Za-z0-9_-]+", "-", value.strip()).strip("-") or "unnamed"
+
+
+def _pascal(value: str) -> str:
+    return "".join(word[:1].upper() + word[1:] for word in re.split(r"[^A-Za-z0-9\u4e00-\u9fff]+|_+", value) if word) or "Unnamed"
+
+
+def _sensitive(name: str) -> bool:
+    return any(token in name.lower() for token in _SENSITIVE)
+
+
+def _split_columns(body: str) -> list[str]:
+    result, current, depth, quote = [], [], 0, ""
+    for char in body:
+        if quote:
+            current.append(char)
+            if char == quote:
+                quote = ""
+        elif 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:
+            result.append("".join(current).strip()); current = []
+        else:
+            current.append(char)
+    if current:
+        result.append("".join(current).strip())
+    return result
+
+
+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_columns(body):
+            if re.match(r"^(PRIMARY|FOREIGN|UNIQUE|CONSTRAINT|KEY|INDEX|CHECK)\b", raw, re.IGNORECASE):
+                continue
+            column = re.match(r"^[`\"]?([\w-]+)[`\"]?\s+([A-Za-z]+(?:\s*\([^)]*\))?)", raw)
+            if column:
+                name, data_type = column.groups()
+                columns.append({"name": name, "data_type": re.sub(r"\s+", "", data_type).upper(),
+                    "nullable": not bool(re.search(r"\bNOT\s+NULL\b", raw, re.IGNORECASE)),
+                    "primary_key": bool(re.search(r"\bPRIMARY\s+KEY\b", raw, re.IGNORECASE)),
+                    "sensitive": _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:
+        document = yaml.safe_load(content) or {}
+    except yaml.YAMLError as exc:
+        raise GovernanceError(f"OpenAPI 解析失败: {exc}", code="INVALID_OPENAPI") from exc
+    if not isinstance(document, dict) or not isinstance(document.get("paths"), dict):
+        raise GovernanceError("OpenAPI 缺少 paths", code="INVALID_OPENAPI")
+    apis = []
+    for path, methods in document["paths"].items():
+        if not isinstance(methods, dict):
+            continue
+        for method, operation in methods.items():
+            if method.lower() in {"get", "post", "put", "patch", "delete", "head", "options"}:
+                operation = operation if isinstance(operation, dict) else {}
+                apis.append({"path": str(path), "method": method.upper(),
+                    "operation_id": operation.get("operationId") or f"{method}-{path}"})
+    schemas = []
+    components = document.get("components") if isinstance(document.get("components"), dict) else {}
+    for name, schema in (components.get("schemas") or {}).items():
+        properties = schema.get("properties", {}) if isinstance(schema, dict) else {}
+        schemas.append({"name": name, "properties": [{"name": prop, "data_type": str((spec or {}).get("type", "object")),
+            "sensitive": _sensitive(prop)} for prop, spec in properties.items()]})
+    return {"tables": [], "apis": apis, "schemas": schemas}
+
+
+def _parse_inventory(content: str) -> dict:
+    try:
+        document = json.loads(content)
+    except ValueError as exc:
+        raise GovernanceError("inventory 必须是 JSON", code="INVALID_INVENTORY") from exc
+    if not isinstance(document, dict):
+        raise GovernanceError("inventory 顶层必须是对象", code="INVALID_INVENTORY")
+    return {key: document.get(key) or [] for key in ("tables", "apis", "schemas")}
+
+
+def normalize_source(payload: dict) -> dict:
+    source_type, content = str(payload.get("source_type", "")).lower(), 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()) > 512 * 1024:
+        raise GovernanceError("输入超过 512KB", code="SOURCE_TOO_LARGE", status=413)
+    parsed = {"ddl": _parse_ddl, "openapi": _parse_openapi, "inventory": _parse_inventory}[source_type](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(source: dict) -> dict:
+    elements, assertions, environment = [], [], source["environment"]
+    for table in source["parsed"]["tables"]:
+        table_name, schema = str(table.get("name", "unnamed")), str(table.get("schema") or "default")
+        m1, m0 = f"m1:table:{schema}:{table_name}", f"m0:table:{schema}:{table_name}"
+        elements += [
+            {"local_id": m1, "layer": "M1", "profile": "software", "kind": "Table", "name": f"{_pascal(table_name)}Table",
+             "status": "candidate", "uri": f"urn:ontorefactor:table:{_safe(schema)}:{_safe(table_name)}", "attributes": {"schema": schema}},
+            {"local_id": m0, "layer": "M0", "profile": "software", "kind": "PhysicalTable", "name": f"{environment}.{schema}.{table_name}",
+             "status": "published", "uri": f"db://{_safe(environment)}/{_safe(schema)}/{_safe(table_name)}", "attributes": {"environment": environment}},
+        ]
+        assertions.append({"subject": m0, "predicate": "INSTANCE_OF", "object": m1, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"})
+        for column in table.get("columns") or []:
+            name = str(column.get("name", "unnamed"))
+            c1, c0 = f"m1:column:{schema}:{table_name}:{name}", f"m0:column:{schema}:{table_name}:{name}"
+            attrs = {"environment": environment, "data_type": column.get("data_type", ""), "nullable": column.get("nullable", True),
+                     "primary_key": column.get("primary_key", False), "sensitive": column.get("sensitive", _sensitive(name))}
+            elements += [
+                {"local_id": c1, "layer": "M1", "profile": "software", "kind": "Column", "name": f"{_pascal(table_name)}{_pascal(name)}Column",
+                 "status": "candidate", "uri": f"urn:ontorefactor:column:{_safe(schema)}:{_safe(table_name)}:{_safe(name)}", "attributes": attrs},
+                {"local_id": c0, "layer": "M0", "profile": "software", "kind": "PhysicalColumn", "name": f"{environment}.{schema}.{table_name}.{name}",
+                 "status": "published", "uri": f"db://{_safe(environment)}/{_safe(schema)}/{_safe(table_name)}/{_safe(name)}", "attributes": attrs},
+            ]
+            assertions += [
+                {"subject": m1, "predicate": "CONTAINS", "object": c1, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"},
+                {"subject": m0, "predicate": "CONTAINS", "object": c0, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"},
+                {"subject": c0, "predicate": "INSTANCE_OF", "object": c1, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"},
+            ]
+    for api in source["parsed"]["apis"]:
+        operation = str(api.get("operation_id") or "operation")
+        m1, m0 = f"m1:api:{operation}", f"m0:api:{operation}"
+        attrs = {"path": api.get("path"), "method": api.get("method")}
+        elements += [
+            {"local_id": m1, "layer": "M1", "profile": "software", "kind": "ApiOperation", "name": _pascal(operation),
+             "status": "candidate", "uri": f"urn:ontorefactor:api:{_safe(operation)}", "attributes": attrs},
+            {"local_id": m0, "layer": "M0", "profile": "software", "kind": "ApiObservation", "name": f"{api.get('method')} {api.get('path')}",
+             "status": "published", "uri": f"openapi://{_safe(source['source_name'])}/{_safe(operation)}", "attributes": {**attrs, "environment": environment}},
+        ]
+        assertions.append({"subject": m0, "predicate": "INSTANCE_OF", "object": m1, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"})
+    for schema in source["parsed"]["schemas"]:
+        schema_name, schema_id = str(schema.get("name", "Schema")), f"m1:schema:{schema.get('name', 'Schema')}"
+        elements.append({"local_id": schema_id, "layer": "M1", "profile": "software", "kind": "Schema", "name": _pascal(schema_name),
+            "status": "candidate", "uri": f"urn:ontorefactor:schema:{_safe(schema_name)}", "attributes": {}})
+        for prop in schema.get("properties") or []:
+            name, prop_id = str(prop.get("name", "property")), f"m1:schema-field:{schema_name}:{prop.get('name', 'property')}"
+            elements.append({"local_id": prop_id, "layer": "M1", "profile": "software", "kind": "Column", "name": f"{_pascal(schema_name)}{_pascal(name)}Field",
+                "status": "candidate", "uri": f"urn:ontorefactor:schema-field:{_safe(schema_name)}:{_safe(name)}",
+                "attributes": {"data_type": prop.get("data_type"), "sensitive": prop.get("sensitive", False)}})
+            assertions.append({"subject": schema_id, "predicate": "CONTAINS", "object": prop_id, "assertion_type": "extracted", "confidence": 1, "review_status": "accepted"})
+    return {"agent": "asset-discovery", "elements": elements, "assertions": assertions, "issues": []}
+
+
+def infer_semantics(source: dict) -> dict:
+    elements, assertions = [], []
+    for table in source["parsed"]["tables"]:
+        table_name, schema = str(table.get("name", "object")), str(table.get("schema") or "default")
+        stem = re.sub(r"^(t_|tbl_|dcp_)", "", table_name, flags=re.IGNORECASE)
+        object_id, table_id = f"m1:business:{stem}", f"m1:table:{schema}:{table_name}"
+        elements.append({"local_id": object_id, "layer": "M1", "profile": "business", "kind": "BusinessObject", "name": _pascal(stem),
+            "description": f"由表名 {table_name} 推断的候选业务对象", "status": "candidate",
+            "uri": f"urn:ontorefactor:business:{_safe(stem)}", "attributes": {"inference": "table-name"}})
+        assertions.append({"subject": table_id, "predicate": "REPRESENTS", "object": object_id,
+            "assertion_type": "inferred", "confidence": .72, "review_status": "pending"})
+    return {"agent": "business-semantics", "elements": elements, "assertions": assertions, "issues": []}
+
+
+def analyze_controls(source: dict) -> dict:
+    issues = []
+    for table in source["parsed"]["tables"]:
+        table_name, schema = str(table.get("name", "unnamed")), str(table.get("schema") or "default")
+        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 names:
+            issues.append({"code": "TENANT_ID_MISSING", "severity": "critical", "title": f"{table_name} 缺少 tenant_id",
+                "description": "租户域表必须补充隔离字段或由 Owner 说明", "target": f"m1:table:{schema}:{table_name}"})
+        for column in table.get("columns") or []:
+            if column.get("sensitive") or _sensitive(str(column.get("name", ""))):
+                issues.append({"code": "SENSITIVE_FIELD_REVIEW", "severity": "high", "title": f"敏感字段待策略确认:{table_name}.{column.get('name')}",
+                    "description": "需要补充分类分级、访问策略、脱敏方式和责任人", "target": f"m1:column:{schema}:{table_name}:{column.get('name')}"})
+    return {"agent": "governance-controls", "elements": [], "assertions": [], "issues": issues}
+
+
+def synthesize(branches: tuple) -> dict:
+    elements, assertions, issues, agents = {}, {}, {}, []
+    for branch in branches:
+        agents.append(branch["agent"])
+        elements.update({item["local_id"]: item for item in branch["elements"]})
+        assertions.update({(item["subject"], item["predicate"], item["object"]): item for item in branch["assertions"]})
+        issues.update({(item["code"], item["target"]): item for item in branch["issues"]})
+    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 build_governance_agent():
+    """λ source. Guard(synth(Par(asset, semantics, controls)(normalize(source))))"""
+    pipeline = Tool("source-normalizer", normalize_source) >> Par(
+        Tool("asset-discovery-agent", discover_assets),
+        Tool("business-semantics-agent", infer_semantics),
+        Tool("governance-controls-agent", analyze_controls),
+    ) >> Tool("governance-synthesis-agent", synthesize)
+    return Guard(pipeline, validator=lambda result: isinstance(result, dict) and "summary" in result, retry=0)
+
+
+def execute_governance_agent(db, tenant_id: str, project_id: str, payload: dict, *, persist: bool = True) -> dict:
+    run_id, now, content = gen_id("gar_"), now_utc(), str(payload.get("content", ""))
+    db.execute("INSERT INTO governance_agent_runs (id,tenant_id,project_id,source_type,input_hash,status,created_at) VALUES (?,?,?,?,?,'running',?)",
+        (run_id, tenant_id, project_id, str(payload.get("source_type", "")), hashlib.sha256(content.encode()).hexdigest(), now))
+    db.commit(); context = Context(run_id=run_id)
+    try:
+        result = build_governance_agent().apply(payload, context)
+        persisted = {"elements": 0, "assertions": 0, "issues": 0}
+        validation = None
+        if persist:
+            evidence = create_evidence(db, tenant_id, project_id, {"kind": "SourceArtifact", "title": str(payload.get("source_name") or "智能体输入"),
+                "uri": f"evidence://agent-run/{run_id}", "content": content, "source": str(payload.get("source_name") or "inline"),
+                "metadata": {"run_id": run_id, "source_type": payload.get("source_type")}})
+            id_map = {}
+            for item in result["elements"]:
+                saved = upsert_element(db, tenant_id, project_id, {**item, "source": f"agent-run:{run_id}"})
+                id_map[item["local_id"]] = saved["id"]; persisted["elements"] += 1
+            for item in result["assertions"]:
+                if item["subject"] in id_map and item["object"] in id_map:
+                    create_assertion(db, tenant_id, project_id, {"subject_id": id_map[item["subject"]], "predicate": item["predicate"],
+                        "object_id": id_map[item["object"]], "assertion_type": item["assertion_type"], "confidence": item["confidence"],
+                        "review_status": item["review_status"], "evidence_ids": [evidence["id"]],
+                        "generated_by": f"lambdagent:governance@0.1#{run_id}"})
+                    persisted["assertions"] += 1
+            issue_now = now_utc()
+            for item in result["issues"]:
+                db.execute("INSERT INTO governance_issues "
+                    "(id,tenant_id,project_id,rule_id,target_id,severity,title,description,status,evidence_ids,created_at,updated_at) "
+                    "VALUES (?,?,?,?,?,?,?,?,'open',?,?,?)",
+                    (gen_id("gi_"), tenant_id, project_id, f"agent:{item['code']}", id_map.get(item["target"], ""),
+                     item["severity"], item["title"], item["description"], json.dumps([evidence["id"]]), issue_now, issue_now))
+                persisted["issues"] += 1
+            db.commit(); validation = validate_project(db, tenant_id, project_id, persist=True)
+        trace = [asdict(item) for item in context.trace]
+        response = {"run_id": run_id, "status": "completed", "result": result, "persisted": persisted,
+                    "validation": validation, "trace": trace, "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(trace, ensure_ascii=False, default=str), now_utc(), run_id))
+        db.commit(); return response
+    except Exception as exc:
+        trace = [asdict(item) for item in context.trace]
+        db.execute("UPDATE governance_agent_runs SET status='failed',error=?,trace_json=?,completed_at=? WHERE id=?",
+            (str(exc)[:1000], json.dumps(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 = 100) -> list[dict]:
+    from .service import rows
+    return rows("governance_agent_runs", 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)),
+    ))

+ 249 - 0
src/ontorefactor_governance/api.py

@@ -0,0 +1,249 @@
+"""Standalone FastAPI surface and zero-build Web UI host."""
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+from typing import Any, Optional
+
+from fastapi import Depends, FastAPI, Header, HTTPException, Query
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import FileResponse
+from fastapi.staticfiles import StaticFiles
+from pydantic import BaseModel, Field
+
+from . import __version__
+from .agents import execute_governance_agent, list_agent_runs
+from .db import get_database
+from . import service
+
+
+app = FastAPI(
+    title="OntoRefactor Governance",
+    version=__version__,
+    description="Standalone M3-M0 ontology-driven data governance platform powered by the LambdAgent pip package.",
+)
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=[origin.strip() for origin in os.getenv("ONTOREFACTOR_CORS_ORIGINS", "http://127.0.0.1:8010,http://localhost:8010").split(",")],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+
+def scope(
+    x_tenant_id: str = Header(default="", alias="X-Tenant-ID"),
+    authorization: str = Header(default="", alias="Authorization"),
+) -> str:
+    expected = os.getenv("ONTOREFACTOR_API_KEY", "")
+    if expected and authorization != f"Bearer {expected}":
+        raise HTTPException(status_code=401, detail="invalid API key")
+    tenant = (x_tenant_id or os.getenv("ONTOREFACTOR_DEFAULT_TENANT", "local")).strip()
+    if not re.fullmatch(r"[A-Za-z0-9_.-]{1,64}", tenant):
+        raise HTTPException(status_code=400, detail="invalid X-Tenant-ID")
+    return tenant
+
+
+def call(fn, *args, **kwargs):
+    try:
+        return fn(*args, **kwargs)
+    except service.GovernanceError as exc:
+        raise HTTPException(status_code=exc.status, detail={"code": exc.code, "message": str(exc)}) from exc
+
+
+class ProjectInput(BaseModel):
+    name: str = Field(min_length=1, max_length=256)
+    namespace: str = Field(min_length=3, max_length=1024)
+    description: str = Field(default="", max_length=4096)
+    version: str = "1.0.0"
+
+
+class ElementInput(BaseModel):
+    uri: str = ""
+    layer: str
+    profile: str
+    kind: str
+    name: str
+    description: str = ""
+    status: str = "candidate"
+    version: str = "1.0.0"
+    attributes: dict[str, Any] = Field(default_factory=dict)
+    source: str = ""
+
+
+class EvidenceInput(BaseModel):
+    kind: str = "SourceArtifact"
+    uri: str = ""
+    title: str
+    content: str = Field(default="", max_length=524288)
+    source: str = ""
+    observed_at: Optional[str] = None
+    valid_from: Optional[str] = None
+    valid_to: Optional[str] = None
+    metadata: dict[str, Any] = Field(default_factory=dict)
+
+
+class AssertionInput(BaseModel):
+    subject_id: str
+    predicate: str
+    object_id: Optional[str] = None
+    object_value: str = ""
+    assertion_type: str = "inferred"
+    confidence: float = Field(default=0, ge=0, le=1)
+    evidence_ids: list[str] = Field(default_factory=list)
+    generated_by: str = ""
+    valid_from: Optional[str] = None
+    valid_to: Optional[str] = None
+    review_status: str = "pending"
+
+
+class ReviewInput(BaseModel):
+    status: str
+    reviewer: str
+    note: str = ""
+
+
+class RuleInput(BaseModel):
+    name: str
+    description: str = ""
+    rule_type: str = "integrity"
+    expression: dict[str, Any] = Field(default_factory=dict)
+    severity: str = "medium"
+    enabled: bool = True
+    version: str = "1.0.0"
+
+
+class IssueInput(BaseModel):
+    status: str
+    assigned_to: str = ""
+
+
+class AnalyzeInput(BaseModel):
+    source_type: str
+    content: str = Field(min_length=1, max_length=524288)
+    source_name: str = ""
+    environment: str = "development"
+    persist: bool = True
+
+
+@app.get("/api/health")
+def health():
+    return {"ok": True, "service": "ontorefactor-governance", "version": __version__, "lambdagent": True}
+
+
+@app.get("/api/v1/governance/projects")
+def projects(tenant: str = Depends(scope)):
+    return {"projects": service.list_projects(get_database(), tenant)}
+
+
+@app.post("/api/v1/governance/projects", status_code=201)
+def create_project(body: ProjectInput, tenant: str = Depends(scope)):
+    return call(service.create_project, get_database(), tenant, body.model_dump())
+
+
+@app.post("/api/v1/governance/bootstrap/tenant-isolation")
+def bootstrap(tenant: str = Depends(scope)):
+    return call(service.seed_tenant_isolation, get_database(), tenant)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/overview")
+def project_overview(project_id: str, tenant: str = Depends(scope)):
+    return call(service.overview, get_database(), tenant, project_id)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/elements")
+def elements(project_id: str, layer: str = "", profile: str = "", q: str = "",
+             limit: int = Query(default=500, ge=1, le=1000), tenant: str = Depends(scope)):
+    return call(service.list_elements, get_database(), tenant, project_id, layer=layer, profile=profile, query=q, limit=limit)
+
+
+@app.post("/api/v1/governance/projects/{project_id}/elements", status_code=201)
+def save_element(project_id: str, body: ElementInput, tenant: str = Depends(scope)):
+    return call(service.upsert_element, get_database(), tenant, project_id, body.model_dump())
+
+
+@app.get("/api/v1/governance/elements/{element_id}/impact")
+def element_impact(element_id: str, max_depth: int = Query(default=4, ge=1, le=8), tenant: str = Depends(scope)):
+    return call(service.impact, get_database(), tenant, element_id, max_depth)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/graph")
+def project_graph(project_id: str, include_pending: bool = True, tenant: str = Depends(scope)):
+    return call(service.graph, get_database(), tenant, project_id, include_pending=include_pending)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/export.jsonld")
+def project_export(project_id: str, tenant: str = Depends(scope)):
+    return call(service.export_jsonld, get_database(), tenant, project_id)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/evidence")
+def evidence(project_id: str, tenant: str = Depends(scope)):
+    return {"evidence": call(service.list_evidence, get_database(), tenant, project_id)}
+
+
+@app.post("/api/v1/governance/projects/{project_id}/evidence", status_code=201)
+def add_evidence(project_id: str, body: EvidenceInput, tenant: str = Depends(scope)):
+    return call(service.create_evidence, get_database(), tenant, project_id, body.model_dump())
+
+
+@app.get("/api/v1/governance/projects/{project_id}/assertions")
+def assertions(project_id: str, review_status: str = "", tenant: str = Depends(scope)):
+    return {"assertions": call(service.list_assertions, get_database(), tenant, project_id, review_status=review_status)}
+
+
+@app.post("/api/v1/governance/projects/{project_id}/assertions", status_code=201)
+def add_assertion(project_id: str, body: AssertionInput, tenant: str = Depends(scope)):
+    return call(service.create_assertion, get_database(), tenant, project_id, body.model_dump())
+
+
+@app.post("/api/v1/governance/assertions/{assertion_id}/review")
+def review(assertion_id: str, body: ReviewInput, tenant: str = Depends(scope)):
+    return call(service.review_assertion, get_database(), tenant, assertion_id, body.status, body.reviewer, body.note)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/rules")
+def rules(project_id: str, tenant: str = Depends(scope)):
+    return {"rules": call(service.list_rules, get_database(), tenant, project_id)}
+
+
+@app.post("/api/v1/governance/projects/{project_id}/rules", status_code=201)
+def add_rule(project_id: str, body: RuleInput, tenant: str = Depends(scope)):
+    return call(service.upsert_rule, get_database(), tenant, project_id, body.model_dump())
+
+
+@app.post("/api/v1/governance/projects/{project_id}/validate")
+def validate(project_id: str, tenant: str = Depends(scope)):
+    return call(service.validate_project, get_database(), tenant, project_id, persist=True)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/issues")
+def issues(project_id: str, status: str = "open", tenant: str = Depends(scope)):
+    return {"issues": call(service.list_issues, get_database(), tenant, project_id, status)}
+
+
+@app.put("/api/v1/governance/issues/{issue_id}")
+def change_issue(issue_id: str, body: IssueInput, tenant: str = Depends(scope)):
+    return call(service.update_issue, get_database(), tenant, issue_id, body.status, body.assigned_to)
+
+
+@app.post("/api/v1/governance/projects/{project_id}/agent-runs/analyze")
+def analyze(project_id: str, body: AnalyzeInput, tenant: str = Depends(scope)):
+    call(service.require_project, get_database(), tenant, project_id)
+    return call(execute_governance_agent, get_database(), tenant, project_id, body.model_dump(), persist=body.persist)
+
+
+@app.get("/api/v1/governance/projects/{project_id}/agent-runs")
+def runs(project_id: str, tenant: str = Depends(scope)):
+    call(service.require_project, get_database(), tenant, project_id)
+    return {"runs": list_agent_runs(get_database(), tenant, project_id)}
+
+
+_STATIC = Path(__file__).parent / "static"
+app.mount("/static", StaticFiles(directory=str(_STATIC)), name="static")
+
+
+@app.get("/", include_in_schema=False)
+def index():
+    return FileResponse(_STATIC / "index.html")

+ 134 - 0
src/ontorefactor_governance/db.py

@@ -0,0 +1,134 @@
+"""Small tenant-scoped SQLite store with no AgentPaaS dependency."""
+from __future__ import annotations
+
+import os
+import sqlite3
+import threading
+import uuid
+from datetime import datetime, timezone
+from typing import Any, Iterable, Optional
+
+
+def now_utc() -> str:
+    return datetime.now(timezone.utc).isoformat()
+
+
+def gen_id(prefix: str = "") -> str:
+    return f"{prefix}{uuid.uuid4().hex[:12]}"
+
+
+class Database:
+    """Thread-safe SQLite boundary used by the standalone API and tests."""
+
+    def __init__(self, database_url: str = "sqlite:///./ontorefactor.db"):
+        if not database_url.startswith("sqlite:///"):
+            raise ValueError("Only sqlite:/// URLs are supported in the standalone edition")
+        path = database_url.removeprefix("sqlite:///")
+        if path != ":memory:":
+            path = os.path.abspath(path)
+            os.makedirs(os.path.dirname(path), exist_ok=True)
+        self._lock = threading.RLock()
+        self.conn = sqlite3.connect(path, check_same_thread=False)
+        self.conn.row_factory = sqlite3.Row
+        self.conn.execute("PRAGMA foreign_keys=ON")
+        self.conn.execute("PRAGMA journal_mode=WAL")
+        self._init_schema()
+
+    def _init_schema(self) -> None:
+        with self._lock:
+            self.conn.executescript("""
+            CREATE TABLE IF NOT EXISTS governance_projects (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, name TEXT NOT NULL,
+                description TEXT DEFAULT '', namespace TEXT NOT NULL, version TEXT DEFAULT '1.0.0',
+                status TEXT DEFAULT 'active', created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
+                UNIQUE(tenant_id, namespace)
+            );
+            CREATE TABLE IF NOT EXISTS ontology_elements (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                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 NOT NULL, updated_at TEXT NOT NULL,
+                UNIQUE(tenant_id, project_id, uri),
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE
+            );
+            CREATE TABLE IF NOT EXISTS governance_evidence (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                kind TEXT NOT NULL, uri TEXT DEFAULT '', title TEXT NOT NULL, content TEXT DEFAULT '',
+                content_hash TEXT NOT NULL, source TEXT DEFAULT '', observed_at TEXT NOT NULL,
+                valid_from TEXT, valid_to TEXT, metadata TEXT DEFAULT '{}', created_at TEXT NOT NULL,
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE
+            );
+            CREATE TABLE IF NOT EXISTS governance_assertions (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                subject_id TEXT NOT NULL, predicate TEXT NOT NULL, object_id TEXT, object_value TEXT DEFAULT '',
+                assertion_type TEXT NOT NULL, confidence REAL DEFAULT 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 NOT NULL, updated_at TEXT NOT NULL,
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE,
+                FOREIGN KEY(subject_id) REFERENCES ontology_elements(id) ON DELETE CASCADE,
+                FOREIGN KEY(object_id) REFERENCES ontology_elements(id) ON DELETE SET NULL
+            );
+            CREATE TABLE IF NOT EXISTS governance_rules (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                name TEXT NOT NULL, description TEXT DEFAULT '', rule_type TEXT NOT NULL,
+                expression TEXT DEFAULT '{}', severity TEXT DEFAULT 'medium', enabled INTEGER DEFAULT 1,
+                version TEXT DEFAULT '1.0.0', created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
+                UNIQUE(tenant_id, project_id, name),
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE
+            );
+            CREATE TABLE IF NOT EXISTS governance_issues (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                rule_id TEXT DEFAULT '', target_id TEXT DEFAULT '', assertion_id TEXT DEFAULT '',
+                severity TEXT NOT NULL, title TEXT NOT NULL, description TEXT DEFAULT '',
+                status TEXT DEFAULT 'open', evidence_ids TEXT DEFAULT '[]', assigned_to TEXT DEFAULT '',
+                created_at TEXT NOT NULL, updated_at TEXT NOT NULL, resolved_at TEXT,
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE
+            );
+            CREATE TABLE IF NOT EXISTS governance_agent_runs (
+                id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, project_id TEXT NOT NULL,
+                source_type TEXT NOT NULL, input_hash TEXT NOT NULL, status TEXT NOT NULL,
+                result_json TEXT DEFAULT '{}', trace_json TEXT DEFAULT '[]', error TEXT DEFAULT '',
+                created_at TEXT NOT NULL, completed_at TEXT,
+                FOREIGN KEY(project_id) REFERENCES governance_projects(id) ON DELETE CASCADE
+            );
+            CREATE INDEX IF NOT EXISTS idx_elements_scope ON ontology_elements(tenant_id, project_id, layer, profile);
+            CREATE INDEX IF NOT EXISTS idx_assertions_scope ON governance_assertions(tenant_id, project_id, review_status);
+            CREATE INDEX IF NOT EXISTS idx_assertions_subject ON governance_assertions(subject_id, predicate);
+            CREATE INDEX IF NOT EXISTS idx_assertions_object ON governance_assertions(object_id, predicate);
+            CREATE INDEX IF NOT EXISTS idx_issues_scope ON governance_issues(tenant_id, project_id, status, severity);
+            CREATE INDEX IF NOT EXISTS idx_runs_scope ON governance_agent_runs(tenant_id, project_id, created_at DESC);
+            """)
+            self.conn.commit()
+
+    def execute(self, sql: str, params: Iterable[Any] = ()) -> sqlite3.Cursor:
+        with self._lock:
+            return self.conn.execute(sql, tuple(params))
+
+    def fetchone(self, sql: str, params: Iterable[Any] = ()) -> Optional[dict]:
+        with self._lock:
+            row = self.conn.execute(sql, tuple(params)).fetchone()
+            return dict(row) if row else None
+
+    def fetchall(self, sql: str, params: Iterable[Any] = ()) -> list[dict]:
+        with self._lock:
+            return [dict(row) for row in self.conn.execute(sql, tuple(params)).fetchall()]
+
+    def commit(self) -> None:
+        with self._lock:
+            self.conn.commit()
+
+    def close(self) -> None:
+        with self._lock:
+            self.conn.close()
+
+
+_database: Optional[Database] = None
+
+
+def get_database() -> Database:
+    global _database
+    if _database is None:
+        _database = Database(os.getenv("ONTOREFACTOR_DATABASE_URL", "sqlite:///./ontorefactor.db"))
+    return _database

+ 585 - 0
src/ontorefactor_governance/service.py

@@ -0,0 +1,585 @@
+"""M3-M0 ontology and governance domain service.
+
+The graph is stored in SQLite for zero-infrastructure deployment. Important
+relations are reified as assertions carrying evidence, time, confidence,
+generator and human-review state, so the storage contract can later be mapped
+to RDF or a graph database without changing the API.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+from collections import defaultdict, deque
+from typing import Any, Iterable, Optional
+
+from .db 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):
+    def __init__(self, message: str, *, code: str = "INVALID_GOVERNANCE_DATA", status: int = 400):
+        super().__init__(message)
+        self.code = code
+        self.status = status
+
+
+def _dump(value: Any) -> str:
+    return json.dumps(value, ensure_ascii=False, sort_keys=True, default=str)
+
+
+def _loads(value: Any, default: Any) -> Any:
+    if value in (None, ""):
+        return default
+    if isinstance(value, (list, dict)):
+        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
+    result = dict(value)
+    for field, default in _JSON_FIELDS.get(table, {}).items():
+        result[field] = _loads(result.get(field), default.copy())
+    if table == "governance_rules":
+        result["enabled"] = bool(result.get("enabled"))
+    return result
+
+
+def rows(table: str, values: Iterable[dict]) -> list[dict]:
+    return [row(table, value) for value in values]  # type: ignore[list-item]
+
+
+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) -> 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, now = gen_id("gp_"), 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 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 upsert_element(db, tenant_id: str, project_id: str, data: dict) -> 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, kind = str(data.get("name", "")).strip(), 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() or 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),
+    )
+    element_id, now = (existing["id"] if existing else str(data.get("id") or gen_id("oe_"))), now_utc()
+    values = (
+        layer, profile, kind, name, str(data.get("description", "")), status,
+        str(data.get("version", "1.0.0")), _dump(data.get("attributes") or {}),
+        str(data.get("source", "")), now,
+    )
+    if existing:
+        db.execute(
+            "UPDATE ontology_elements SET layer=?,profile=?,kind=?,name=?,description=?,status=?,"
+            "version=?,attributes=?,source=?,updated_at=? WHERE id=? AND tenant_id=?",
+            (*values, element_id, tenant_id),
+        )
+    else:
+        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, *values[:-1], 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 list_elements(db, tenant_id: str, project_id: str, *, layer: str = "", profile: str = "", query: str = "", limit: int = 500) -> dict:
+    require_project(db, tenant_id, project_id)
+    where, params = ["tenant_id=?", "project_id=?"], [tenant_id, project_id]
+    if layer:
+        where.append("layer=?"); params.append(layer.upper())
+    if profile:
+        where.append("profile=?"); params.append(profile.lower())
+    if query:
+        where.append("(name LIKE ? OR uri LIKE ? OR description LIKE ?)")
+        params.extend([f"%{query}%"] * 3)
+    clause = " AND ".join(where)
+    total = db.fetchone(f"SELECT COUNT(*) AS n FROM ontology_elements WHERE {clause}", params)["n"]
+    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 ?",
+        [*params, min(max(int(limit), 1), 1000)],
+    )
+    return {"items": rows("ontology_elements", values), "total": total}
+
+
+def create_evidence(db, tenant_id: str, project_id: str, data: dict) -> dict:
+    require_project(db, tenant_id, project_id)
+    title, uri = str(data.get("title", "")).strip(), str(data.get("uri", "")).strip()
+    if not title:
+        raise GovernanceError("证据 title 不能为空")
+    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]
+    evidence_id, now, content = gen_id("ev_"), now_utc(), str(data.get("content", ""))
+    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, str(data.get("kind", "SourceArtifact")), uri, title,
+         content, hashlib.sha256(content.encode()).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 = 500) -> 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), 1000)),
+    ))
+
+
+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 create_assertion(db, tenant_id: str, project_id: str, data: dict) -> 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 and get_element(db, tenant_id, object_id)["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))
+    if not predicate or assertion_type not in ASSERTION_TYPES or review_status not in REVIEW_STATUSES:
+        raise GovernanceError("断言 predicate、类型或审核状态不合法")
+    if not 0 <= confidence <= 1:
+        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}")
+    object_value = str(data.get("object_value", ""))
+    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, object_value),
+    )
+    if existing:
+        return row("governance_assertions", existing)  # type: ignore[return-value]
+    assertion_id, now = gen_id("as_"), 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, 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 list_assertions(db, tenant_id: str, project_id: str, *, review_status: str = "", limit: int = 500) -> list[dict]:
+    require_project(db, tenant_id, project_id)
+    where, params = ["a.tenant_id=?", "a.project_id=?"], [tenant_id, project_id]
+    if review_status:
+        where.append("a.review_status=?"); params.append(review_status)
+    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 "
+        f"WHERE {' AND '.join(where)} ORDER BY a.updated_at DESC LIMIT ?",
+        [*params, min(max(limit, 1), 1000)],
+    )
+    return rows("governance_assertions", values)
+
+
+def review_assertion(db, tenant_id: str, assertion_id: str, status: str, reviewer: str, note: str = "") -> dict:
+    get_assertion(db, tenant_id, assertion_id)
+    if status not in ("accepted", "rejected", "expired") or not reviewer.strip():
+        raise GovernanceError("审核状态或审核人不合法")
+    db.execute(
+        "UPDATE governance_assertions SET review_status=?,reviewer=?,review_note=?,updated_at=? WHERE id=? AND tenant_id=?",
+        (status, reviewer.strip(), note, now_utc(), 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) -> dict:
+    require_project(db, tenant_id, project_id)
+    name, severity = str(data.get("name", "")).strip(), str(data.get("severity", "medium")).lower()
+    if not name or severity not in SEVERITIES:
+        raise GovernanceError("规则名称或严重级别不合法")
+    existing = db.fetchone(
+        "SELECT id FROM governance_rules WHERE tenant_id=? AND project_id=? AND name=?", (tenant_id, project_id, name)
+    )
+    rule_id, now = (existing["id"] if existing else gen_id("gr_")), now_utc()
+    if existing:
+        db.execute(
+            "UPDATE governance_rules SET description=?,rule_type=?,expression=?,severity=?,enabled=?,version=?,updated_at=? "
+            "WHERE id=? AND tenant_id=?",
+            (str(data.get("description", "")), str(data.get("rule_type", "integrity")),
+             _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:
+        db.execute(
+            "INSERT INTO governance_rules "
+            "(id,tenant_id,project_id,name,description,rule_type,expression,severity,enabled,version,created_at,updated_at) "
+            "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
+            (rule_id, tenant_id, project_id, name, str(data.get("description", "")),
+             str(data.get("rule_type", "integrity")), _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:
+    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, incoming, findings = defaultdict(list), defaultdict(list), []
+    for assertion in assertions:
+        outgoing[assertion["subject_id"]].append(assertion)
+        if assertion.get("object_id"):
+            incoming[assertion["object_id"]].append(assertion)
+        subject, obj = by_id.get(assertion["subject_id"]), by_id.get(assertion.get("object_id"))
+        if assertion["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']} 必须实例化相邻上层,当前指向 {obj['name']}", subject["id"], assertion["id"]))
+        if assertion["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 assertion["predicate"] in {
+            "DELETE", "GRANT", "AUTHORIZE", "DOWNGRADE", "PUBLISH", "RETIRE",
+        }:
+            findings.append(_finding("INFERRED_HIGH_RISK_ACTION", "critical", "推断不能触发高风险动作",
+                "必须经过确定性证据与人工审批", 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 的非声明断言必须关联证据和生成者", assertion["subject_id"], assertion["id"]))
+    for element in elements:
+        accepted_out = [a for a in outgoing[element["id"]] if a["review_status"] == "accepted"]
+        accepted_in = [a for a in incoming[element["id"]] if a["review_status"] == "accepted"]
+        if element["layer"] == "M0" and element["status"] == "published" and not any(a["predicate"] == "INSTANCE_OF" for a in accepted_out):
+            findings.append(_finding("M0_UNKNOWN_TYPE", "high", "M0 事实未归类", "已发布 M0 必须实例化 M1 类型", element["id"]))
+        if element["layer"] == "M1" and element["kind"] == "BusinessCapability" and not any(a["predicate"] == "REALIZED_BY" for a in accepted_out):
+            findings.append(_finding("CAPABILITY_NOT_REALIZED", "high", "业务能力缺少软件实现", "至少需要一个 REALIZED_BY", element["id"]))
+        if element["layer"] == "M1" and element["kind"] == "BusinessObject" and not any(a["predicate"] == "REPRESENTS" for a in accepted_in):
+            findings.append(_finding("BUSINESS_OBJECT_NOT_REPRESENTED", "high", "业务对象缺少技术表达", "至少需要一个 REPRESENTS", element["id"]))
+        attrs = element.get("attributes") or {}
+        if element["profile"] == "software" and attrs.get("environment") == "production" and not any(a["predicate"] == "OWNED_BY" for a in accepted_out):
+            findings.append(_finding("PRODUCTION_ASSET_WITHOUT_OWNER", "critical", "生产资产缺少责任人", "必须关联 Owner/Steward", element["id"]))
+        if element["kind"] in ("Column", "PhysicalColumn") and attrs.get("sensitive"):
+            policies = [by_id.get(a.get("object_id"), {}) for a in accepted_out if a["predicate"] == "GOVERNED_BY"]
+            if not any(p.get("kind") in ("AccessPolicy", "SecurityPolicy") for p in policies):
+                findings.append(_finding("SENSITIVE_FIELD_WITHOUT_POLICY", "critical", "敏感字段缺少访问策略", "必须关联访问策略", element["id"]))
+    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 item 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:{item['code']}", item["target_id"],
+                 item["assertion_id"], item["severity"], item["title"], item["description"], now, now),
+            )
+        db.commit()
+    counts = {severity: sum(1 for item in findings if item["severity"] == severity) for severity in SEVERITIES}
+    return {"valid": not any(item["severity"] in ("high", "critical") for item in findings),
+            "findings": findings, "counts": 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, params = ["tenant_id=?", "project_id=?"], [tenant_id, project_id]
+    if status:
+        where.append("status=?"); params.append(status)
+    return rows("governance_issues", db.fetchall(
+        f"SELECT * FROM governance_issues WHERE {' AND '.join(where)} ORDER BY "
+        "CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,created_at DESC", 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("未知问题状态")
+    if not db.fetchone("SELECT id FROM governance_issues WHERE id=? AND tenant_id=?", (issue_id, tenant_id)):
+        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) -> dict:
+    nodes = list_elements(db, tenant_id, project_id, limit=1000)["items"]
+    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 "
+        f"WHERE a.tenant_id=? AND a.project_id=? AND a.review_status IN ({placeholders}) ORDER BY a.created_at LIMIT 2000",
+        (tenant_id, project_id, *statuses),
+    )
+    return {"nodes": nodes, "edges": rows("governance_assertions", values)}
+
+
+def impact(db, tenant_id: str, element_id: str, max_depth: int = 4) -> dict:
+    center = get_element(db, tenant_id, element_id)
+    graph_data = graph(db, tenant_id, center["project_id"], include_pending=False)
+    by_id = {item["id"]: item for item in graph_data["nodes"]}
+    adjacency = defaultdict(list)
+    for edge in graph_data["edges"]:
+        if edge.get("object_id"):
+            adjacency[edge["subject_id"]].append((edge["object_id"], edge))
+            adjacency[edge["object_id"]].append((edge["subject_id"], edge))
+    depth_limit, queue, visited, used = min(max(int(max_depth), 1), 8), deque([(element_id, 0)]), {element_id: 0}, {}
+    while queue:
+        current, depth = queue.popleft()
+        if depth >= depth_limit:
+            continue
+        for neighbour, edge in adjacency[current]:
+            used[edge["id"]] = edge
+            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.values()), "max_depth": depth_limit}
+
+
+def overview(db, tenant_id: str, project_id: str) -> dict:
+    project = require_project(db, tenant_id, project_id)
+    layers = {name: 0 for name in LAYERS}
+    profiles = {name: 0 for name in PROFILES}
+    for item in db.fetchall("SELECT layer,COUNT(*) n FROM ontology_elements WHERE tenant_id=? AND project_id=? GROUP BY layer", (tenant_id, project_id)):
+        layers[item["layer"]] = item["n"]
+    for item in db.fetchall("SELECT profile,COUNT(*) n FROM ontology_elements WHERE tenant_id=? AND project_id=? GROUP BY profile", (tenant_id, project_id)):
+        profiles[item["profile"]] = item["n"]
+    def count(table: str, suffix: str = "") -> int:
+        return int(db.fetchone(f"SELECT COUNT(*) n FROM {table} WHERE tenant_id=? AND project_id=? {suffix}", (tenant_id, project_id))["n"])
+    return {"project": project, "layers": layers, "profiles": profiles, "totals": {
+        "elements": sum(layers.values()), "assertions": count("governance_assertions"),
+        "pending_reviews": count("governance_assertions", "AND review_status='pending'"),
+        "evidence": count("governance_evidence"), "rules": count("governance_rules", "AND enabled=1"),
+        "open_issues": count("governance_issues", "AND status='open'"), "agent_runs": count("governance_agent_runs"),
+    }}
+
+
+def export_jsonld(db, tenant_id: str, project_id: str) -> dict:
+    project = require_project(db, tenant_id, project_id)
+    data = graph(db, tenant_id, project_id)
+    values = []
+    for item in data["nodes"]:
+        values.append({"@id": item["uri"], "@type": item["kind"], "name": item["name"],
+                       "layer": item["layer"], "profile": item["profile"], "status": item["status"], **item["attributes"]})
+    uri_by_id = {item["id"]: item["uri"] for item in data["nodes"]}
+    for assertion in data["edges"]:
+        values.append({"@id": f"urn:assertion:{assertion['id']}", "@type": "Assertion",
+                       "subject": {"@id": uri_by_id[assertion["subject_id"]]}, "predicate": assertion["predicate"],
+                       "object": {"@id": uri_by_id.get(assertion.get("object_id"), "")} if assertion.get("object_id") else assertion["object_value"],
+                       "assertionType": assertion["assertion_type"], "confidence": assertion["confidence"],
+                       "reviewStatus": assertion["review_status"], "evidence": assertion["evidence_ids"]})
+    return {"@context": {"name": "http://www.w3.org/2000/01/rdf-schema#label",
+                         "layer": "https://onto.qm.cn/ontorefactor/meta/1.0#layer",
+                         "profile": "https://onto.qm.cn/ontorefactor/meta/1.0#profile"},
+            "@id": project["namespace"], "@graph": values}
+
+
+def seed_tenant_isolation(db, tenant_id: str) -> dict:
+    """Install the idempotent, valid M3-M0 tenant-isolation reference slice."""
+    project = create_project(db, tenant_id, {"name": "DCP 租户与组织域", "namespace": "https://onto.qm.cn/ontorefactor/model/dcp/1.0",
+        "description": "业务、软件、治理 Profile 的 M3-M0 纵向样例"})
+    pid, nodes = project["id"], {}
+    def node(key: str, layer: str, profile: str, kind: str, description: str = "", attrs: Optional[dict] = None, uri: str = ""):
+        nodes[key] = upsert_element(db, tenant_id, pid, {"layer": layer, "profile": profile, "kind": kind,
+            "name": key, "description": description, "attributes": attrs or {}, "status": "published", "uri": uri,
+            "source": "builtin:tenant-isolation"})
+    for name in ("MetaElement", "MetaEntityType", "MetaRelationType", "MetaAttributeType", "MetaConstraintType", "MetaAssertionType", "MetaProfile", "MetaView"):
+        node(name, "M3", "meta", name, "稳定的 M3 建模内核", uri=f"https://onto.qm.cn/ontorefactor/meta/1.0#{name}")
+    for name in ("Object", "Subject", "Activity", "Rule", "Result", "Evidence", "Assertion", "Term", "State", "TimeExtent"):
+        node(name, "M2", "upper", "EntityType", "M2 通用上层概念", 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", f"M2 {profile} Profile 类型", uri=f"https://onto.qm.cn/ontorefactor/{profile}/1.0#{name}")
+    m1 = (
+        ("TenantManagement", "business", "BusinessCapability", {}), ("Tenant", "business", "BusinessObject", {"core": True}),
+        ("TenantIsolation", "business", "BusinessRule", {}), ("BaseIAM", "software", "Module", {"environment": "production"}),
+        ("TenantController", "software", "Class", {}), ("CreateTenantAPI", "software", "ApiOperation", {}),
+        ("DcpTenantTable", "software", "Table", {"environment": "production"}),
+        ("TenantIdColumn", "software", "Column", {"environment": "production", "sensitive": True}),
+        ("TenantCreatedTopic", "software", "EventContract", {}), ("IAMDataOwner", "governance", "OwnerRole", {}),
+        ("TenantIsolationPolicy", "governance", "AccessPolicy", {}),
+        ("TenantIdCompleteness", "governance", "QualityRule", {"threshold": 1.0}),
+        ("ReleaseSecurityGate", "governance", "QualityGate", {}),
+    )
+    for name, profile, kind, attrs in m1:
+        node(name, "M1", profile, kind, f"DCP 项目模型:{name}", attrs, f"https://onto.qm.cn/ontorefactor/model/dcp/1.0#{name}")
+    node("TenantControllerCommit", "M0", "software", "SourceArtifact", attrs={"commit": "abc123"}, uri="git://dcp-base-tenant/commit/abc123/TenantController.java")
+    node("TenantIdPhysical", "M0", "software", "PhysicalColumn", attrs={"environment": "production", "sensitive": True}, uri="db://prod/dcp_tenant/tenant_id")
+    node("CompletenessRun", "M0", "governance", "Measurement", attrs={"value": .9987, "threshold": 1.0, "result": "failed"}, uri="measurement:tenant-id:20260817")
+    evidence = create_evidence(db, tenant_id, pid, {"kind": "SourceArtifact", "uri": "evidence://dcp/tenant-isolation",
+        "title": "租户隔离设计与 DDL", "content": "tenant_id NOT NULL; every query is tenant scoped", "source": "builtin"})
+    runtime = create_evidence(db, tenant_id, pid, {"kind": "RuntimeObservation", "uri": "evidence://trace/98271",
+        "title": "生产调用 Trace 98271", "content": "observed tenant-scoped access", "source": "apm://trace/98271"})
+    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, "review_status": "accepted", "generated_by": "builtin"})
+    for name in (*sum((tuple(v) for v in profile_types.values()), ()), "Object", "Subject", "Activity", "Rule", "Result", "Evidence", "Assertion", "Term", "State", "TimeExtent"):
+        create_assertion(db, tenant_id, pid, {"subject_id": nodes[name]["id"], "predicate": "INSTANCE_OF", "object_id": nodes["MetaEntityType"]["id"],
+            "assertion_type": "declared", "confidence": 1, "review_status": "accepted", "generated_by": "builtin"})
+    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 if assertion_type == "declared" else .98,
+            "evidence_ids": [runtime["id"] if assertion_type == "observed" else evidence["id"]],
+            "generated_by": "builtin:tenant-isolation", "review_status": "accepted"})
+    for name, rule_type, severity, expression in (
+        ("相邻层实例化", "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"]}),
+    ):
+        upsert_rule(db, tenant_id, pid, {"name": name, "rule_type": rule_type, "severity": severity, "expression": expression,
+            "description": "OntoRefactor 确定性治理门禁"})
+    validation = validate_project(db, tenant_id, pid, persist=True)
+    return {"project": project, "overview": overview(db, tenant_id, pid), "validation": validation}

+ 309 - 0
src/ontorefactor_governance/static/app.js

@@ -0,0 +1,309 @@
+const API = "/api/v1/governance";
+const state = {
+  tenant: localStorage.getItem("or-tenant") || "local",
+  apiKey: localStorage.getItem("or-api-key") || "",
+  projectId: localStorage.getItem("or-project") || "",
+  projects: [],
+  activeTab: "overview",
+  overview: null,
+};
+
+const $ = (selector, root = document) => root.querySelector(selector);
+const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
+const esc = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({"&":"&amp;","<":"&lt;",">":"&gt;","'":"&#39;",'"':"&quot;"}[char]));
+const date = (value) => value ? new Intl.DateTimeFormat("zh-CN", {dateStyle:"medium", timeStyle:"short"}).format(new Date(value)) : "—";
+const badge = (value) => `<span class="badge ${esc(String(value).toLowerCase())}">${esc(value)}</span>`;
+
+async function request(path, options = {}) {
+  const headers = {"X-Tenant-ID": state.tenant, ...(options.headers || {})};
+  if (state.apiKey) headers.Authorization = `Bearer ${state.apiKey}`;
+  if (options.body && !(options.body instanceof FormData)) headers["Content-Type"] = "application/json";
+  const response = await fetch(`${API}${path}`, {...options, headers});
+  const type = response.headers.get("content-type") || "";
+  const payload = type.includes("json") ? await response.json() : await response.text();
+  if (!response.ok) {
+    const detail = typeof payload === "object" ? payload.detail : payload;
+    throw new Error(typeof detail === "object" ? JSON.stringify(detail) : detail || `HTTP ${response.status}`);
+  }
+  return payload;
+}
+
+function toast(message, error = false) {
+  const node = $("#toast");
+  node.textContent = message;
+  node.className = `toast show${error ? " error" : ""}`;
+  clearTimeout(toast.timer);
+  toast.timer = setTimeout(() => node.className = "toast", 3200);
+}
+
+function currentProject() { return state.projects.find((item) => item.id === state.projectId); }
+
+async function loadProjects(preferredId) {
+  state.projects = (await request("/projects")).projects;
+  if (preferredId && state.projects.some((p) => p.id === preferredId)) state.projectId = preferredId;
+  if (!state.projects.some((p) => p.id === state.projectId)) state.projectId = state.projects[0]?.id || "";
+  localStorage.setItem("or-project", state.projectId);
+  const select = $("#project-select");
+  select.innerHTML = state.projects.length
+    ? state.projects.map((p) => `<option value="${esc(p.id)}" ${p.id === state.projectId ? "selected" : ""}>${esc(p.name)}</option>`).join("")
+    : '<option value="">暂无项目</option>';
+  $("#validate-button").disabled = !state.projectId;
+  $("#project-hint").textContent = currentProject()?.description || "项目完全保存在本地 SQLite 中。";
+  $("#empty-state").classList.toggle("hidden", Boolean(state.projectId));
+  $("#content").classList.toggle("hidden", !state.projectId);
+  if (state.projectId) await renderActive();
+}
+
+async function bootstrap() {
+  setBusy($("#bootstrap-button"), true, "创建中…");
+  try {
+    const result = await request("/bootstrap/tenant-isolation", {method: "POST"});
+    await loadProjects(result.project.id);
+    toast(`已准备演示项目:${result.project.name}`);
+  } catch (error) { toast(error.message, true); }
+  finally { setBusy($("#bootstrap-button"), false, "创建演示项目"); }
+}
+
+async function createProject(event) {
+  event.preventDefault();
+  const button = $("#create-project"); setBusy(button, true, "创建中…");
+  try {
+    const project = await request("/projects", {method:"POST", body:JSON.stringify({
+      name:$("#project-name").value.trim(),
+      namespace:$("#project-namespace").value.trim(),
+      description:$("#project-description").value.trim(),
+      version:"1.0.0",
+    })});
+    $("#project-dialog").close();
+    $("#project-form").reset();
+    await loadProjects(project.id);
+    state.activeTab = "agent"; activateTab();
+    toast(`项目“${project.name}”已创建,可以导入资产`);
+  } catch (error) { toast(error.message, true); }
+  finally { setBusy(button, false, "创建项目"); }
+}
+
+function setBusy(button, busy, label) {
+  if (!button) return;
+  button.disabled = busy;
+  button.textContent = label;
+}
+
+async function validate() {
+  const button = $("#validate-button");
+  setBusy(button, true, "校验中…");
+  try {
+    const result = await request(`/projects/${state.projectId}/validate`, {method:"POST"});
+    toast(result.valid ? "治理校验通过" : `发现 ${result.findings?.length || 0} 个问题`);
+    state.activeTab = result.valid ? "overview" : "issues";
+    activateTab();
+  } catch (error) { toast(error.message, true); }
+  finally { setBusy(button, false, "运行治理校验"); }
+}
+
+function activateTab() {
+  $$(".tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.tab === state.activeTab));
+  renderActive().catch((error) => {
+    $("#content").innerHTML = `<div class="panel inline-empty">加载失败:${esc(error.message)}</div>`;
+    toast(error.message, true);
+  });
+}
+
+async function renderActive() {
+  if (!state.projectId) return;
+  const renderers = {overview: renderOverview, model: renderModel, assertions: renderAssertions, evidence: renderEvidence, issues: renderIssues, agent: renderAgent};
+  $("#content").innerHTML = '<div class="panel inline-empty">正在加载治理数据…</div>';
+  await renderers[state.activeTab]();
+}
+
+async function renderOverview() {
+  const data = state.overview = await request(`/projects/${state.projectId}/overview`);
+  const counts = data.totals || {};
+  const metrics = [
+    ["本体元素", counts.elements || 0, "四层知识模型"],
+    ["语义断言", counts.assertions || 0, `${counts.pending_reviews || 0} 条待复核`],
+    ["证据记录", counts.evidence || 0, "全链路可追溯"],
+    ["治理规则", counts.rules || 0, "持续自动校验"],
+    ["开放问题", counts.open_issues || 0, "等待治理处置"],
+    ["智能体运行", counts.agent_runs || 0, "步骤可审计"],
+  ];
+  const layers = data.layers || {};
+  const max = Math.max(1, ...Object.values(layers));
+  const issues = data.recent_issues || [];
+  $("#content").innerHTML = `
+    <div class="metric-grid">${metrics.map(([label, value, note]) => `<article class="metric panel"><div class="metric-label">${label}</div><div class="metric-value">${value}</div><div class="metric-note">${note}</div></article>`).join("")}</div>
+    <div class="dashboard-grid">
+      <article class="section-card panel">
+        <div class="section-head"><div><h2>M3—M0 本体分布</h2><p>治理概念由元模型逐层落到运行实例</p></div><button class="button small ghost" data-open-tab="model">查看模型</button></div>
+        <div class="layer-list">${["M3","M2","M1","M0"].map((layer) => `<div class="layer-row"><strong>${layer}</strong><div class="bar-track"><div class="bar" style="width:${((layers[layer] || 0) / max) * 100}%"></div></div><span>${layers[layer] || 0}</span></div>`).join("")}</div>
+      </article>
+      <article class="section-card panel">
+        <div class="section-head"><div><h2>治理状态</h2><p>当前项目质量快照</p></div></div>
+        <div class="status-stack">
+          <div class="status-card"><strong>${counts.open_issues ? "待处理" : "稳定"}</strong><small>完整性状态</small></div>
+          <div class="status-card"><strong>${counts.open_issues || 0}</strong><small>开放问题</small></div>
+          <div class="status-card"><strong>${counts.pending_reviews || 0}</strong><small>待审断言</small></div>
+          <div class="status-card"><strong>${counts.elements ? Math.round((counts.assertions / counts.elements) * 100) : 0}%</strong><small>关系密度</small></div>
+        </div>
+      </article>
+    </div>
+    <article class="section-card panel" style="margin-top:18px">
+      <div class="section-head"><div><h2>最近问题</h2><p>高风险项优先进入人工闭环</p></div><button class="button small ghost" data-open-tab="issues">全部问题</button></div>
+      ${issues.length ? `<ul class="activity-list">${issues.map((issue) => `<li><span class="dot"></span><span><strong>${esc(issue.title)}</strong><br><small class="muted">${esc(issue.description || "")}</small></span>${badge(issue.severity)}</li>`).join("")}</ul>` : '<div class="inline-empty">当前没有开放问题。</div>'}
+    </article>`;
+}
+
+async function renderModel() {
+  const [elementResult, graph] = await Promise.all([
+    request(`/projects/${state.projectId}/elements`),
+    request(`/projects/${state.projectId}/graph`),
+  ]);
+  const elements = elementResult.items;
+  $("#content").innerHTML = `
+    <div class="toolbar"><select id="layer-filter"><option value="">全部层级</option>${["M3","M2","M1","M0"].map(x => `<option>${x}</option>`).join("")}</select><select id="profile-filter"><option value="">全部剖面</option>${[...new Set(elements.map(x => x.profile))].filter(Boolean).map(x => `<option>${esc(x)}</option>`).join("")}</select><input id="model-search" placeholder="搜索名称、类型或标识"><span class="spacer"></span><button id="download-jsonld" class="button ghost">导出 JSON-LD</button></div>
+    <div class="table-wrap"><table><thead><tr><th>层级</th><th>名称</th><th>类型</th><th>剖面</th><th>来源</th><th>关系</th></tr></thead><tbody id="element-rows"></tbody></table></div>
+    <p class="muted">图谱统计:${graph.nodes?.length || 0} 个节点,${graph.edges?.length || 0} 条关系。点击元素名称可执行影响分析。</p>`;
+  const draw = () => {
+    const layer = $("#layer-filter").value, profile = $("#profile-filter").value, query = $("#model-search").value.toLowerCase();
+    const filtered = elements.filter((x) => (!layer || x.layer === layer) && (!profile || x.profile === profile) && (!query || `${x.name} ${x.kind} ${x.uri}`.toLowerCase().includes(query)));
+    $("#element-rows").innerHTML = filtered.map((x) => `<tr><td>${badge(x.layer)}</td><td><button class="link-button" data-impact="${esc(x.id)}">${esc(x.name)}</button><br><small class="code muted">${esc(x.uri)}</small></td><td>${esc(x.kind)}</td><td>${esc(x.profile || "—")}</td><td>${esc(x.source || "—")}</td><td>${graph.edges.filter(e => e.subject_id === x.id || e.object_id === x.id).length}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配元素</td></tr>';
+  };
+  ["#layer-filter", "#profile-filter", "#model-search"].forEach((selector) => $(selector).addEventListener("input", draw));
+  draw();
+  $("#download-jsonld").addEventListener("click", downloadJsonLd);
+}
+
+async function showImpact(id) {
+  try {
+    const result = await request(`/elements/${id}/impact?max_depth=3`);
+    $("#detail-title").textContent = `影响分析 · ${result.center.name}`;
+    $("#detail-content").innerHTML = `<p class="muted">沿已接受的本体关系向外分析三层,识别变更可能影响的对象。</p><div class="table-wrap"><table><thead><tr><th>距离</th><th>对象</th><th>类型</th><th>层级</th></tr></thead><tbody>${(result.nodes || []).filter(x => x.id !== id).map(x => `<tr><td>${x.distance}</td><td>${esc(x.name)}</td><td>${esc(x.kind)}</td><td>${badge(x.layer)}</td></tr>`).join("") || '<tr><td colspan="4" class="inline-empty">未发现关联影响</td></tr>'}</tbody></table></div>`;
+    $("#detail-dialog").showModal();
+  } catch (error) { toast(error.message, true); }
+}
+
+async function downloadJsonLd() {
+  try {
+    const data = await request(`/projects/${state.projectId}/export.jsonld`);
+    const blob = new Blob([JSON.stringify(data, null, 2)], {type:"application/ld+json"});
+    const url = URL.createObjectURL(blob), link = document.createElement("a");
+    link.href = url; link.download = `${currentProject()?.slug || "governance"}.jsonld`; link.click();
+    URL.revokeObjectURL(url); toast("JSON-LD 已导出");
+  } catch (error) { toast(error.message, true); }
+}
+
+async function renderAssertions() {
+  const rows = (await request(`/projects/${state.projectId}/assertions`)).assertions;
+  $("#content").innerHTML = `<div class="toolbar"><select id="assertion-status"><option value="">全部状态</option><option>pending</option><option>accepted</option><option>rejected</option></select><span class="spacer"></span><span class="muted">共 ${rows.length} 条语义关系</span></div><div class="table-wrap"><table><thead><tr><th>主语</th><th>谓词</th><th>宾语</th><th>状态</th><th>置信度</th><th>操作</th></tr></thead><tbody id="assertion-rows"></tbody></table></div>`;
+  const draw = () => {
+    const status = $("#assertion-status").value;
+    $("#assertion-rows").innerHTML = rows.filter(x => !status || x.review_status === status).map((x) => `<tr><td class="wrap">${esc(x.subject_name || x.subject_id)}</td><td><span class="code">${esc(x.predicate)}</span></td><td class="wrap">${esc(x.object_name || x.object_value || x.object_id)}</td><td>${badge(x.review_status)}</td><td>${Math.round((x.confidence || 0) * 100)}%</td><td>${x.review_status === "pending" ? `<button class="button small secondary" data-review="${esc(x.id)}" data-decision="accepted">接受</button> <button class="button small ghost" data-review="${esc(x.id)}" data-decision="rejected">驳回</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配断言</td></tr>';
+  };
+  $("#assertion-status").addEventListener("change", draw); draw();
+}
+
+async function reviewAssertion(id, decision) {
+  try {
+    await request(`/assertions/${id}/review`, {method:"POST", body:JSON.stringify({status:decision, reviewer:"web-console"})});
+    toast(decision === "accepted" ? "断言已接受" : "断言已驳回"); await renderAssertions();
+  } catch (error) { toast(error.message, true); }
+}
+
+async function renderEvidence() {
+  const rows = (await request(`/projects/${state.projectId}/evidence`)).evidence;
+  $("#content").innerHTML = `<div class="section-head"><div><h2>证据中心</h2><p>每条自动推断都可以追溯到原始输入和处理过程</p></div></div><div class="table-wrap"><table><thead><tr><th>名称</th><th>类型</th><th>来源</th><th>校验值</th><th>创建时间</th></tr></thead><tbody>${rows.map((x) => `<tr><td>${esc(x.title)}</td><td>${badge(x.kind)}</td><td class="wrap">${esc(x.source || x.uri || "—")}</td><td class="code">${esc((x.content_hash || "").slice(0, 16))}${x.content_hash ? "…" : ""}</td><td>${date(x.created_at)}</td></tr>`).join("") || '<tr><td colspan="5" class="inline-empty">暂无证据</td></tr>'}</tbody></table></div>`;
+}
+
+async function renderIssues() {
+  const rows = (await request(`/projects/${state.projectId}/issues?status=`)).issues;
+  $("#content").innerHTML = `<div class="toolbar"><select id="issue-status"><option value="">全部状态</option><option value="open">开放</option><option value="acknowledged">已确认</option><option value="resolved">已解决</option><option value="waived">已豁免</option></select><span class="spacer"></span><button class="button secondary" data-action="validate">重新校验</button></div><div class="table-wrap"><table><thead><tr><th>严重度</th><th>问题</th><th>规则</th><th>状态</th><th>发现时间</th><th>操作</th></tr></thead><tbody id="issue-rows"></tbody></table></div>`;
+  const draw = () => {
+    const status = $("#issue-status").value;
+    $("#issue-rows").innerHTML = rows.filter(x => !status || x.status === status).map((x) => `<tr><td>${badge(x.severity)}</td><td class="wrap"><strong>${esc(x.title)}</strong><br><small class="muted">${esc(x.description || "")}</small></td><td class="code">${esc(x.rule_code || "—")}</td><td>${badge(x.status)}</td><td>${date(x.created_at)}</td><td>${x.status === "open" ? `<button class="button small secondary" data-resolve="${esc(x.id)}">标记解决</button>` : "—"}</td></tr>`).join("") || '<tr><td colspan="6" class="inline-empty">没有匹配问题</td></tr>';
+  };
+  $("#issue-status").addEventListener("change", draw); draw();
+}
+
+async function resolveIssue(id) {
+  try {
+    await request(`/issues/${id}`, {method:"PUT", body:JSON.stringify({status:"resolved", assigned_to:"web-console"})});
+    toast("问题已关闭"); await renderIssues();
+  } catch (error) { toast(error.message, true); }
+}
+
+async function renderAgent() {
+  const runs = (await request(`/projects/${state.projectId}/agent-runs`)).runs;
+  $("#content").innerHTML = `<div class="agent-grid">
+    <article class="section-card panel"><div class="section-head"><div><h2>资产发现智能体</h2><p>由 LambdAgent 流水线完成规范化、并行分析、合成与治理落库</p></div></div>
+      <form id="agent-form" class="form-stack">
+        <div class="form-pair"><label>输入格式<select id="source-type"><option value="ddl">SQL DDL</option><option value="openapi">OpenAPI JSON/YAML</option><option value="inventory">资产清单 JSON/YAML</option></select></label><label>来源名称<input id="source-name" value="console-import" required></label></div>
+        <label>原始内容<textarea id="source-content" spellcheck="false">CREATE TABLE customer_order (
+  order_id INTEGER PRIMARY KEY,
+  customer_id INTEGER NOT NULL,
+  total_amount DECIMAL(12,2),
+  created_at TIMESTAMP
+);</textarea></label>
+        <label>运行说明<input id="agent-instruction" value="发现数据资产,推断业务语义并执行治理检查"></label>
+        <div class="button-row"><button id="run-agent" class="button primary" type="submit">运行智能体</button><span class="muted">自动结论以 pending 状态等待人工确认</span></div>
+      </form>
+      <div id="agent-result"></div>
+    </article>
+    <article class="section-card panel"><div class="section-head"><div><h2>运行历史</h2><p>完整保留每一步输入输出摘要</p></div></div>
+      ${runs.length ? `<div class="trace">${runs.map(x => `<div class="trace-step"><strong>${esc(x.source_type || "governance-analysis")}</strong><small>${date(x.created_at)} · ${esc(x.status)}</small><br><button class="link-button" data-run="${esc(x.id)}">查看执行轨迹</button></div>`).join("")}</div>` : '<div class="inline-empty">尚未运行智能体</div>'}
+    </article></div>`;
+  $("#agent-form").addEventListener("submit", runAgent);
+}
+
+async function runAgent(event) {
+  event.preventDefault();
+  const button = $("#run-agent"); setBusy(button, true, "智能体运行中…");
+  try {
+    const result = await request(`/projects/${state.projectId}/agent-runs/analyze`, {method:"POST", body:JSON.stringify({source_type:$("#source-type").value, source_name:$("#source-name").value, content:$("#source-content").value, environment:"development", persist:true})});
+    const summary = {...(result.result?.summary || {}), persisted:result.persisted};
+    $("#agent-result").innerHTML = `<h3>运行结果</h3><div class="result-box">${esc(JSON.stringify(summary, null, 2))}</div>`;
+    toast(`智能体已落库 ${result.persisted?.elements || 0} 个元素`);
+  } catch (error) { toast(error.message, true); }
+  finally { setBusy(button, false, "运行智能体"); }
+}
+
+async function showRun(id) {
+  try {
+    const runs = (await request(`/projects/${state.projectId}/agent-runs`)).runs, run = runs.find(x => x.id === id);
+    if (!run) return;
+    $("#detail-title").textContent = "智能体执行轨迹";
+    const trace = run.trace_json || [];
+    const result = run.result_json || {};
+    $("#detail-content").innerHTML = `<dl class="definition-list"><dt>运行标识</dt><dd class="code">${esc(run.id)}</dd><dt>状态</dt><dd>${badge(run.status)}</dd><dt>来源类型</dt><dd>${esc(run.source_type || "—")}</dd><dt>落库结果</dt><dd>${esc(JSON.stringify(result.persisted || {}))}</dd></dl><div class="trace">${trace.map((step, index) => `<div class="trace-step"><strong>${index + 1}. ${esc(step.node || step.step || step.name || "步骤")}</strong><small>${esc(step.summary || step.status || JSON.stringify(step))}</small></div>`).join("")}</div>`;
+    $("#detail-dialog").showModal();
+  } catch (error) { toast(error.message, true); }
+}
+
+document.addEventListener("click", (event) => {
+  const tab = event.target.closest(".tab, [data-open-tab]");
+  if (tab) { state.activeTab = tab.dataset.tab || tab.dataset.openTab; activateTab(); }
+  const action = event.target.closest("[data-action]")?.dataset.action;
+  if (action === "bootstrap") bootstrap();
+  if (action === "validate") validate();
+  const impact = event.target.closest("[data-impact]")?.dataset.impact; if (impact) showImpact(impact);
+  const review = event.target.closest("[data-review]"); if (review) reviewAssertion(review.dataset.review, review.dataset.decision);
+  const issue = event.target.closest("[data-resolve]")?.dataset.resolve; if (issue) resolveIssue(issue);
+  const run = event.target.closest("[data-run]")?.dataset.run; if (run) showRun(run);
+  const dialog = event.target.closest("[data-close-dialog]")?.dataset.closeDialog; if (dialog) $(`#${dialog}`).close();
+});
+
+async function init() {
+  $("#tenant-input").value = state.tenant;
+  $("#api-key-input").value = state.apiKey;
+  $("#bootstrap-button").addEventListener("click", bootstrap);
+  $("#new-project-button").addEventListener("click", () => $("#project-dialog").showModal());
+  $("#project-form").addEventListener("submit", createProject);
+  $("#validate-button").addEventListener("click", validate);
+  $("#project-select").addEventListener("change", async (event) => { state.projectId = event.target.value; localStorage.setItem("or-project", state.projectId); await renderActive(); });
+  $("#save-tenant").addEventListener("click", async () => { state.tenant = $("#tenant-input").value.trim() || "local"; localStorage.setItem("or-tenant", state.tenant); state.projectId = ""; await loadProjects(); toast(`已切换租户:${state.tenant}`); });
+  $("#settings-button").addEventListener("click", () => $("#settings-dialog").showModal());
+  $("#save-settings").addEventListener("click", () => { state.apiKey = $("#api-key-input").value.trim(); localStorage.setItem("or-api-key", state.apiKey); toast("连接设置已保存"); });
+  $("#close-detail").addEventListener("click", () => $("#detail-dialog").close());
+  try { await loadProjects(); } catch (error) { toast(error.message, true); $("#content").innerHTML = `<div class="panel inline-empty">无法连接服务:${esc(error.message)}</div>`; }
+}
+
+init();

+ 95 - 0
src/ontorefactor_governance/static/index.html

@@ -0,0 +1,95 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <meta name="description" content="OntoRefactor 独立数据治理平台">
+  <title>OntoRefactor · 数据治理控制台</title>
+  <link rel="stylesheet" href="/static/styles.css">
+  <script src="/static/app.js" defer></script>
+</head>
+<body>
+  <div class="shell">
+    <header class="topbar">
+      <a class="brand" href="/" aria-label="OntoRefactor 首页">
+        <span class="brand-mark">OR</span>
+        <span><strong>OntoRefactor</strong><small>Ontology-driven governance</small></span>
+      </a>
+      <div class="top-actions">
+        <label class="compact-field">租户
+          <input id="tenant-input" value="local" autocomplete="off">
+        </label>
+        <button id="save-tenant" class="button ghost">切换</button>
+        <button id="settings-button" class="icon-button" title="连接设置" aria-label="连接设置">⚙</button>
+      </div>
+    </header>
+
+    <main>
+      <section class="hero">
+        <div>
+          <p class="eyebrow">独立运行 · 四层本体 · 可审计智能体</p>
+          <h1>把数据资产,变成<br><span>可验证的治理知识。</span></h1>
+          <p class="hero-copy">从 DDL、OpenAPI 或资产清单发现对象,生成带证据的语义断言,并用治理规则持续校验。</p>
+        </div>
+        <div class="project-control panel">
+          <label>当前治理项目</label>
+          <select id="project-select"><option value="">正在加载…</option></select>
+          <div class="button-row">
+            <button id="new-project-button" class="button ghost">新建空白项目</button>
+            <button id="bootstrap-button" class="button primary">创建演示项目</button>
+            <button id="validate-button" class="button secondary" disabled>运行治理校验</button>
+          </div>
+          <p id="project-hint" class="muted">项目完全保存在本地 SQLite 中。</p>
+        </div>
+      </section>
+
+      <nav class="tabs" aria-label="功能导航">
+        <button class="tab active" data-tab="overview">治理总览</button>
+        <button class="tab" data-tab="model">本体模型</button>
+        <button class="tab" data-tab="assertions">语义断言</button>
+        <button class="tab" data-tab="evidence">证据中心</button>
+        <button class="tab" data-tab="issues">治理问题</button>
+        <button class="tab" data-tab="agent">智能体工作台</button>
+      </nav>
+
+      <section id="empty-state" class="empty-state panel hidden">
+        <div class="empty-icon">◇</div>
+        <h2>还没有治理项目</h2>
+        <p>创建演示项目可立即查看完整模型,也可以在智能体工作台导入自己的数据定义。</p>
+        <button class="button primary" data-action="bootstrap">创建演示项目</button>
+      </section>
+
+      <section id="content" aria-live="polite"></section>
+    </main>
+  </div>
+
+  <div id="toast" class="toast" role="status" aria-live="polite"></div>
+
+  <dialog id="settings-dialog">
+    <form method="dialog" class="dialog-body">
+      <div class="dialog-head"><h2>连接设置</h2><button value="cancel" class="icon-button" aria-label="关闭">×</button></div>
+      <label>API Key(服务端未启用时可留空)
+        <input id="api-key-input" type="password" autocomplete="off" placeholder="X-API-Key">
+      </label>
+      <div class="dialog-actions"><button value="cancel" class="button ghost">取消</button><button id="save-settings" value="default" class="button primary">保存</button></div>
+    </form>
+  </dialog>
+
+  <dialog id="project-dialog">
+    <form id="project-form" class="dialog-body">
+      <div class="dialog-head"><h2>新建治理项目</h2><button type="button" class="icon-button" data-close-dialog="project-dialog" aria-label="关闭">×</button></div>
+      <label>项目名称<input id="project-name" required maxlength="256" placeholder="例如:客户主数据治理"></label>
+      <label>本体命名空间<input id="project-namespace" required maxlength="1024" placeholder="例如:https://example.cn/ontology/customer/1.0"></label>
+      <label>项目说明<textarea id="project-description" style="min-height:100px" maxlength="4096" placeholder="说明治理范围、目标和责任域"></textarea></label>
+      <div class="dialog-actions"><button type="button" class="button ghost" data-close-dialog="project-dialog">取消</button><button id="create-project" type="submit" class="button primary">创建项目</button></div>
+    </form>
+  </dialog>
+
+  <dialog id="detail-dialog">
+    <div class="dialog-body wide">
+      <div class="dialog-head"><h2 id="detail-title">详情</h2><button id="close-detail" class="icon-button" aria-label="关闭">×</button></div>
+      <div id="detail-content"></div>
+    </div>
+  </dialog>
+</body>
+</html>

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

@@ -0,0 +1,169 @@
+:root {
+  --ink: #172129;
+  --muted: #68737b;
+  --paper: #f4f1ea;
+  --surface: #fffdf9;
+  --line: #ded9ce;
+  --green: #174f46;
+  --green-2: #28796a;
+  --lime: #c8eb67;
+  --amber: #e3a93b;
+  --red: #c65b4b;
+  --blue: #4b78a6;
+  --shadow: 0 18px 45px rgb(31 47 42 / 8%);
+}
+
+* { box-sizing: border-box; }
+
+body {
+  margin: 0;
+  color: var(--ink);
+  background:
+    radial-gradient(circle at 8% 0%, rgb(200 235 103 / 18%), transparent 29rem),
+    radial-gradient(circle at 92% 18%, rgb(40 121 106 / 10%), transparent 30rem),
+    var(--paper);
+  font: 15px/1.55 Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
+}
+
+button, input, select, textarea { font: inherit; }
+button { cursor: pointer; }
+.hidden { display: none !important; }
+.muted { color: var(--muted); }
+.shell { max-width: 1420px; min-height: 100vh; margin: auto; padding: 0 42px 64px; }
+
+.topbar { height: 84px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgb(23 33 41 / 11%); }
+.brand { display: flex; align-items: center; gap: 12px; color: inherit; text-decoration: none; }
+.brand-mark { display: grid; place-items: center; width: 42px; height: 42px; border-radius: 12px; color: var(--lime); background: var(--green); font-weight: 800; letter-spacing: -.08em; }
+.brand strong, .brand small { display: block; }
+.brand strong { font-size: 17px; letter-spacing: -.02em; }
+.brand small { color: var(--muted); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; }
+.top-actions, .button-row { display: flex; align-items: center; gap: 10px; }
+.compact-field { display: flex; align-items: center; gap: 8px; color: var(--muted); }
+.compact-field input { width: 120px; }
+
+input, select, textarea {
+  width: 100%; color: var(--ink); background: #fff; border: 1px solid var(--line); border-radius: 9px; padding: 10px 12px; outline: none;
+}
+input:focus, select:focus, textarea:focus { border-color: var(--green-2); box-shadow: 0 0 0 3px rgb(40 121 106 / 12%); }
+textarea { resize: vertical; min-height: 210px; font-family: "Cascadia Code", Consolas, monospace; font-size: 13px; }
+label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
+
+.button, .icon-button { border: 0; border-radius: 9px; transition: transform .15s, opacity .15s, background .15s; }
+.button { padding: 10px 15px; font-weight: 700; }
+.button:hover, .icon-button:hover { transform: translateY(-1px); }
+.button:disabled { cursor: not-allowed; opacity: .45; transform: none; }
+.button.primary { color: white; background: var(--green); }
+.button.secondary { color: var(--green); background: var(--lime); }
+.button.ghost { color: var(--ink); background: rgb(255 255 255 / 52%); border: 1px solid var(--line); }
+.button.danger { color: #fff; background: var(--red); }
+.button.small { padding: 6px 10px; font-size: 12px; }
+.icon-button { display: grid; place-items: center; width: 38px; height: 38px; color: var(--ink); background: transparent; font-size: 20px; }
+
+.hero { display: grid; grid-template-columns: 1fr 380px; gap: 64px; align-items: end; padding: 62px 0 48px; }
+.eyebrow { margin: 0 0 13px; color: var(--green-2); font-size: 12px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; }
+h1 { margin: 0; max-width: 780px; font-family: Georgia, "Noto Serif SC", serif; font-size: clamp(40px, 5vw, 70px); font-weight: 500; line-height: 1.04; letter-spacing: -.045em; }
+h1 span { color: var(--green-2); }
+.hero-copy { max-width: 660px; margin: 22px 0 0; color: var(--muted); font-size: 17px; }
+.panel { background: rgb(255 253 249 / 88%); border: 1px solid rgb(23 33 41 / 10%); border-radius: 16px; box-shadow: var(--shadow); }
+.project-control { display: grid; gap: 12px; padding: 22px; }
+.project-control select { font-weight: 700; }
+.project-control .button-row .button { flex: 1; }
+.project-control .button-row { flex-wrap: wrap; }
+.project-control .button-row .button:first-child { flex-basis: 100%; }
+.project-control p { margin: 0; font-size: 12px; }
+
+.tabs { display: flex; gap: 4px; overflow-x: auto; padding: 4px; border: 1px solid var(--line); border-radius: 12px; background: rgb(255 255 255 / 42%); }
+.tab { flex: 1; min-width: 120px; padding: 11px 14px; color: var(--muted); background: transparent; border: 0; border-radius: 8px; font-weight: 700; }
+.tab.active { color: #fff; background: var(--green); }
+#content { min-height: 430px; padding-top: 24px; }
+
+.metric-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; }
+.metric { position: relative; overflow: hidden; min-height: 128px; padding: 18px; }
+.metric::after { content: ""; position: absolute; right: -24px; bottom: -38px; width: 90px; height: 90px; border-radius: 50%; background: rgb(200 235 103 / 25%); }
+.metric-label { color: var(--muted); font-size: 12px; }
+.metric-value { margin-top: 8px; font-family: Georgia, serif; font-size: 39px; line-height: 1; }
+.metric-note { margin-top: 9px; color: var(--green-2); font-size: 11px; font-weight: 700; }
+.dashboard-grid { display: grid; grid-template-columns: 1.5fr 1fr; gap: 18px; margin-top: 18px; }
+.section-card { padding: 22px; }
+.section-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
+.section-head h2, .section-head h3 { margin: 0; font-size: 18px; }
+.section-head p { margin: 3px 0 0; color: var(--muted); font-size: 12px; }
+
+.layer-list { display: grid; gap: 11px; }
+.layer-row { display: grid; grid-template-columns: 110px 1fr 34px; gap: 13px; align-items: center; }
+.bar-track { height: 9px; overflow: hidden; border-radius: 99px; background: #e9e4da; }
+.bar { height: 100%; min-width: 2px; border-radius: inherit; background: linear-gradient(90deg, var(--green), var(--green-2)); }
+.status-stack { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
+.status-card { padding: 14px; border-radius: 11px; background: #f5f2eb; }
+.status-card strong { display: block; font-size: 23px; }
+.status-card small { color: var(--muted); }
+.activity-list { margin: 0; padding: 0; list-style: none; }
+.activity-list li { display: grid; grid-template-columns: 9px 1fr auto; gap: 10px; align-items: start; padding: 12px 0; border-top: 1px solid var(--line); }
+.activity-list li:first-child { border-top: 0; }
+.dot { width: 8px; height: 8px; margin-top: 7px; border-radius: 50%; background: var(--amber); }
+
+.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; }
+.toolbar input, .toolbar select { max-width: 250px; }
+.toolbar .spacer { flex: 1; }
+.table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 13px; background: var(--surface); }
+table { width: 100%; border-collapse: collapse; }
+th, td { padding: 13px 15px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; white-space: nowrap; }
+th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: #f5f2eb; font-size: 11px; letter-spacing: .05em; text-transform: uppercase; }
+tr:last-child td { border-bottom: 0; }
+td.wrap { min-width: 220px; max-width: 420px; white-space: normal; }
+.link-button { padding: 0; color: var(--green-2); background: none; border: 0; font-weight: 700; text-align: left; }
+.code { font-family: "Cascadia Code", Consolas, monospace; font-size: 12px; }
+.badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 999px; color: var(--green); background: rgb(40 121 106 / 12%); font-size: 11px; font-weight: 800; }
+.badge.pending, .badge.medium { color: #805d14; background: rgb(227 169 59 / 17%); }
+.badge.rejected, .badge.high, .badge.open { color: #8f3327; background: rgb(198 91 75 / 15%); }
+.badge.accepted, .badge.resolved, .badge.succeeded { color: var(--green); background: rgb(40 121 106 / 14%); }
+.badge.info, .badge.running { color: #305f8d; background: rgb(75 120 166 / 15%); }
+
+.empty-state { margin-top: 24px; padding: 68px 24px; text-align: center; }
+.empty-state h2 { margin: 10px 0 5px; }
+.empty-state p { max-width: 540px; margin: 0 auto 20px; color: var(--muted); }
+.empty-icon { font-size: 48px; color: var(--green-2); }
+.inline-empty { padding: 42px 20px; color: var(--muted); text-align: center; }
+
+.agent-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(340px, .8fr); gap: 18px; }
+.form-stack { display: grid; gap: 14px; }
+.form-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
+.trace { display: grid; gap: 10px; margin-top: 14px; }
+.trace-step { padding: 12px 14px; border-left: 3px solid var(--lime); border-radius: 0 9px 9px 0; background: #f6f3ec; }
+.trace-step strong { display: block; }
+.trace-step small { color: var(--muted); }
+.result-box { padding: 15px; border-radius: 11px; color: #dbece8; background: #183d38; white-space: pre-wrap; word-break: break-word; font: 12px/1.6 "Cascadia Code", monospace; }
+
+.toast { position: fixed; right: 28px; bottom: 28px; z-index: 10; max-width: 420px; padding: 13px 17px; color: white; background: var(--green); border-radius: 10px; box-shadow: var(--shadow); opacity: 0; pointer-events: none; transform: translateY(12px); transition: .2s; }
+.toast.show { opacity: 1; transform: none; }
+.toast.error { background: var(--red); }
+dialog { width: min(620px, calc(100vw - 30px)); max-height: calc(100vh - 40px); overflow: auto; padding: 0; border: 0; border-radius: 17px; box-shadow: 0 30px 90px rgb(0 0 0 / 24%); }
+dialog::backdrop { background: rgb(12 25 24 / 45%); backdrop-filter: blur(4px); }
+.dialog-body { display: grid; gap: 18px; padding: 24px; }
+.dialog-body.wide { width: 100%; }
+.dialog-head, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
+.dialog-head h2 { margin: 0; }
+.dialog-actions { justify-content: flex-end; }
+.definition-list { display: grid; grid-template-columns: 140px 1fr; gap: 9px 16px; }
+.definition-list dt { color: var(--muted); }
+.definition-list dd { margin: 0; overflow-wrap: anywhere; }
+
+@media (max-width: 1080px) {
+  .metric-grid { grid-template-columns: repeat(3, 1fr); }
+  .hero { grid-template-columns: 1fr 340px; gap: 28px; }
+}
+
+@media (max-width: 760px) {
+  .shell { padding: 0 16px 40px; }
+  .topbar { height: auto; min-height: 76px; gap: 12px; }
+  .brand small, .compact-field { display: none; }
+  .hero { grid-template-columns: 1fr; padding: 42px 0 30px; }
+  h1 { font-size: 42px; }
+  .metric-grid { grid-template-columns: repeat(2, 1fr); }
+  .dashboard-grid, .agent-grid { grid-template-columns: 1fr; }
+  .tabs { justify-content: flex-start; }
+  .tab { flex: 0 0 auto; }
+  .toolbar { align-items: stretch; flex-direction: column; }
+  .toolbar input, .toolbar select { max-width: none; }
+  .form-pair { grid-template-columns: 1fr; }
+}

+ 8 - 0
tests/conftest.py

@@ -0,0 +1,8 @@
+import sys
+from pathlib import Path
+
+
+ROOT = Path(__file__).parents[1]
+SRC = ROOT / "src"
+if str(SRC) not in sys.path:
+    sys.path.insert(0, str(SRC))

+ 54 - 0
tests/test_agents.py

@@ -0,0 +1,54 @@
+from pathlib import Path
+
+import pytest
+
+from ontorefactor_governance.agents import execute_governance_agent
+from ontorefactor_governance.db import Database
+from ontorefactor_governance.service import GovernanceError, create_project, list_assertions, list_elements
+
+
+def project(db):
+    return create_project(db, "tenant-a", {"name": "Agent import", "namespace": "urn:test:agent"})
+
+
+def test_lambdagent_pipeline_discovers_and_persists_assets():
+    db = Database("sqlite:///:memory:")
+    item = project(db)
+    result = execute_governance_agent(db, "tenant-a", item["id"], {
+        "source_type": "ddl",
+        "source_name": "customer.sql",
+        "environment": "production",
+        "content": """
+            CREATE TABLE dcp_customer (
+                id INTEGER PRIMARY KEY,
+                mobile VARCHAR(32) NOT NULL
+            );
+        """,
+    })
+
+    assert result["status"] == "completed"
+    assert result["result"]["summary"] == {"elements": 7, "assertions": 8, "issues": 2}
+    assert result["persisted"] == {"elements": 7, "assertions": 8, "issues": 2}
+    assert len(result["trace"]) >= 5
+    assert list_elements(db, "tenant-a", item["id"])["total"] == 7
+    assert len(list_assertions(db, "tenant-a", item["id"])) == 8
+    assert any(value["review_status"] == "pending" for value in list_assertions(db, "tenant-a", item["id"]))
+    db.close()
+
+
+def test_invalid_ddl_creates_a_failed_audited_run():
+    db = Database("sqlite:///:memory:")
+    item = project(db)
+    with pytest.raises(GovernanceError):
+        execute_governance_agent(db, "tenant-a", item["id"], {"source_type": "ddl", "content": "SELECT 1"})
+    run = db.fetchone("SELECT status,error FROM governance_agent_runs")
+    assert run["status"] == "failed"
+    assert run["error"]
+    db.close()
+
+
+def test_runtime_source_has_no_agentpaas_dependency():
+    source_root = Path(__file__).parents[1] / "src" / "ontorefactor_governance"
+    source = "\n".join(path.read_text(encoding="utf-8") for path in source_root.glob("*.py"))
+    assert "import agentpaas" not in source
+    assert "from agentpaas" not in source

+ 59 - 0
tests/test_api.py

@@ -0,0 +1,59 @@
+from fastapi.testclient import TestClient
+
+from ontorefactor_governance import api
+from ontorefactor_governance.db import Database
+
+
+def test_standalone_api_and_web_console(monkeypatch):
+    db = Database("sqlite:///:memory:")
+    monkeypatch.setattr(api, "get_database", lambda: db)
+    client = TestClient(api.app)
+    headers = {"X-Tenant-ID": "api-test"}
+
+    assert client.get("/api/health").json()["ok"] is True
+    page = client.get("/")
+    assert page.status_code == 200
+    assert "OntoRefactor" in page.text
+
+    seeded = client.post("/api/v1/governance/bootstrap/tenant-isolation", headers=headers)
+    assert seeded.status_code == 200
+    project_id = seeded.json()["project"]["id"]
+
+    projects = client.get("/api/v1/governance/projects", headers=headers).json()["projects"]
+    assert len(projects) == 1
+    assert projects[0]["id"] == project_id
+
+    overview = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers=headers).json()
+    assert overview["totals"]["elements"] == 48
+    assert overview["totals"]["open_issues"] == 0
+
+    exported = client.get(f"/api/v1/governance/projects/{project_id}/export.jsonld", headers=headers)
+    assert exported.status_code == 200
+    assert exported.json()["@graph"]
+
+    other_tenant = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers={"X-Tenant-ID": "other"})
+    assert other_tenant.status_code == 404
+    db.close()
+
+
+def test_agent_endpoint(monkeypatch):
+    db = Database("sqlite:///:memory:")
+    monkeypatch.setattr(api, "get_database", lambda: db)
+    client = TestClient(api.app)
+    headers = {"X-Tenant-ID": "agent-api"}
+    project_id = client.post("/api/v1/governance/projects", headers=headers, json={
+        "name": "Imported governance", "namespace": "urn:api:imported"
+    }).json()["id"]
+
+    response = client.post(f"/api/v1/governance/projects/{project_id}/agent-runs/analyze", headers=headers, json={
+        "source_type": "ddl",
+        "source_name": "orders.sql",
+        "environment": "development",
+        "content": "CREATE TABLE orders (id INTEGER PRIMARY KEY, tenant_id INTEGER NOT NULL);",
+    })
+    assert response.status_code == 200
+    assert response.json()["persisted"]["elements"] == 7
+    runs = client.get(f"/api/v1/governance/projects/{project_id}/agent-runs", headers=headers).json()["runs"]
+    assert len(runs) == 1
+    assert runs[0]["status"] == "completed"
+    db.close()

+ 13 - 0
tests/test_ontology.py

@@ -0,0 +1,13 @@
+from pathlib import Path
+
+from rdflib import Graph
+
+
+def test_all_turtle_resources_parse():
+    ontology = Path(__file__).parents[1] / "ontology"
+    files = sorted(ontology.rglob("*.ttl"))
+    assert len(files) == 8
+    for path in files:
+        graph = Graph()
+        graph.parse(path, format="turtle")
+        assert len(graph) > 0, path

+ 58 - 0
tests/test_service.py

@@ -0,0 +1,58 @@
+import pytest
+
+from ontorefactor_governance.db import Database
+from ontorefactor_governance.service import (
+    GovernanceError,
+    export_jsonld,
+    impact,
+    list_elements,
+    require_project,
+    seed_tenant_isolation,
+)
+
+
+@pytest.fixture
+def db():
+    value = Database("sqlite:///:memory:")
+    yield value
+    value.close()
+
+
+def test_reference_slice_is_valid_and_idempotent(db):
+    first = seed_tenant_isolation(db, "tenant-a")
+    second = seed_tenant_isolation(db, "tenant-a")
+
+    assert first["validation"]["valid"] is True
+    assert second["validation"]["valid"] is True
+    assert second["overview"]["totals"] == {
+        "elements": 48,
+        "assertions": 56,
+        "pending_reviews": 0,
+        "evidence": 2,
+        "rules": 5,
+        "open_issues": 0,
+        "agent_runs": 0,
+    }
+    assert second["overview"]["layers"] == {"M3": 8, "M2": 24, "M1": 13, "M0": 3}
+
+
+def test_tenant_scope_is_enforced(db):
+    result = seed_tenant_isolation(db, "tenant-a")
+    with pytest.raises(GovernanceError) as error:
+        require_project(db, "tenant-b", result["project"]["id"])
+    assert error.value.status == 404
+
+
+def test_impact_and_jsonld_export(db):
+    result = seed_tenant_isolation(db, "tenant-a")
+    project_id = result["project"]["id"]
+    elements = list_elements(db, "tenant-a", project_id)["items"]
+    tenant = next(item for item in elements if item["name"] == "Tenant")
+
+    affected = impact(db, "tenant-a", tenant["id"], 3)
+    exported = export_jsonld(db, "tenant-a", project_id)
+
+    assert affected["center"]["name"] == "Tenant"
+    assert any(node["name"] == "DcpTenantTable" for node in affected["nodes"])
+    assert exported["@id"] == result["project"]["namespace"]
+    assert len(exported["@graph"]) == 104