Skip to main content

veriself

面向 AI agent 的类型化指标执行层。 LLM 不能写 SQL,只能组合已注册的指标——每个数字都带口径版本、粒度、血缘与审计头。

License: Apache-2.0 Python tests


30 秒演示

下面的输出是实跑的,不是手写的:本机 veriself synth && veriself demo 原文,仅删除整行 (删除处标 …)与行尾空格。面板偏宽是因为输出被重定向时宽度固定为 120 列 (见 interfaces/render.py 的 _NON_TTY_WIDTH)。

真实用户在 MCP 客户端里说的是一句自然语言("我最近睡眠债有多严重?")。把它翻成下面这个 结构化查询对象是客户端的事——veriself 只接受这个对象,没有任何参数能传原生语句。 所以"LLM 写出错误 SQL"这个失败模式在这里根本不存在。

下面这条命令的前提是先跑过 veriself synth(生成 3 年确定性合成数据,输出见折叠区):

$ veriself demo
── 步骤 1/4 · 建库 · 加载契约 · 物化指标 ────────
┌─ 步骤 1 完成 ────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 契约数量        18                                                                                                   │
│ 建表函数        veriself.warehouse.loader.ensure_schema                                                              │
│ 契约写入维度表  18                                                                                                   │
│ 物化指标        18                                                                                                   │
│ 物化行数        14408                                                                                                │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

── 步骤 2/4 · 正常查询:subject.sleep_debt_7d(结构化对象) ────────
提交给 veriself 的查询对象:
{
  "metrics": [
    "subject.sleep_debt_7d"
  ],
  "dimensions": [
    "date.weekday"
  ],
  "filters": {
    "date.between": [
      "2026-09-01",
      "2026-09-30"
    ]
  },
  "grain": "day",
  "limit": 30
}

 date.day     date.weekday   subject.sleep_debt_7d
 ─────────────────────────────────────────────────
 2026-09-01   Tuesday        3.03
 2026-09-02   Wednesday      3.62
 2026-09-03   Thursday       3.28
 2026-09-04   Friday         3.43
 2026-09-05   Saturday       4.24
 2026-09-06   Sunday         4.67
…
 2026-09-25   Friday         5.44

    共 30 行,仅显示前 25 行(--json 可拿全量)
┌─ 审计头 audit(契约第 4 节) ────────────────────────────────────────────────────────────────────────────────────────┐
│ 查询角色        owner                                                                                                │
│ 指标版本        subject.sleep_debt_7d = 2                                                                            │
│ 契约哈希        subject.sleep_debt_7d = sha256:9d32e8f0fa6dfed3                                                      │
│ RLS 改写        owner_only                                                                                           │
│ 强制校验        registered → dimensions → grain → ast_join_path → rls                                                │
│ as-of 口径      2026-10-01                                                                                           │
│ 查询时间        2026-10-07T17:08:30+08:00                                                                            │
│ row_versioning  valid_to IS NULL AND metric_version = contract.version                                               │
│ actor_role      owner                                                                                                │
…

── 步骤 3/4 · 拒绝演示:越界请求必须失败(这才是卖点) ────────
┌─ ❌ 拒绝 ────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 请求已被指标契约拒绝。                                                                                               │
│                                                                                                                      │
│ reason: dimension_not_allowed: 维度 'date.hour' 不在指标 'subject.sleep_debt_7d' 的 allowed_                         │
│         dimensions ['date.weekday', 'date.day_of_week', 'date.month', 'date.quarter'] 中                             │
│ 触发的校验: dimension_not_allowed                                                                                    │
│ 强制校验链: registered → dimensions → grain → ast_join_path → rls                                                    │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
…
提示:这次拒绝也写进了审计(veriself audit 可查)。

这个 demo 的重点不是"它答对了",而是"它无法答错":第二个请求里 LLM 想越界,架构不允许它越界——拒绝发生在生成 SQL 之前(拒绝原因是 dimension_not_allowed,指向契约里的白名单),并且这次拒绝同样写进审计。

其余命令的完整输出(实跑原文,未删改)

veriself synth

 对象                  行数
 ──────────────────────────
 dim_date             1,004
 dim_subject              2
 dim_source               4
 dim_context              4
 fact_observation   106,424
 fact_event           5,882
 latent_daily         1,004
 obs_daily           10,040
 obs_intraday       144,576

┌─ veriself synth 完成 ────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 数仓      <仓库根>                                                                                                   │
│ 种子      20261001                                                                                                   │
│ 日期范围  2024-01-01 → 2026-09-30                                                                                    │
│ 建表      已确保 schema                                                                                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

veriself query --json:契约第 4 节的 data + audit

命令:veriself query -m subject.sleep_debt_7d --filters '{"date.between": ["2026-09-28", "2026-09-30"]}' --role owner --json

{
  "data": [
    {
      "date.day": "2026-09-28",
      "subject.sleep_debt_7d": 6.970000000000001
    },
    {
      "date.day": "2026-09-29",
      "subject.sleep_debt_7d": 7.15
    },
    {
      "date.day": "2026-09-30",
      "subject.sleep_debt_7d": 6.720000000000001
    }
  ],
  "audit": {
    "metric_versions": {
      "subject.sleep_debt_7d": 2
    },
    "contract_hashes": {
      "subject.sleep_debt_7d": "sha256:9d32e8f0fa6dfed3"
    },
    "compiled_sql": "SELECT \"d\".\"date\" AS \"date.day\", ANY_VALUE(CASE WHEN \"f\".\"metric_id\" = ? THEN \"f\".\"value\" END) AS \"subject.sleep_debt_7d\" FROM \"fact_metric_value\" AS f INNER JOIN \"dim_date\" AS d ON \"d\".\"date_key\" = \"f\".\"date_key\" WHERE \"f\".\"valid_to\" IS NULL AND (\"f\".\"metric_id\" = ? AND \"f\".\"metric_version\" = ?) AND \"f\".\"subject_id\" = ? AND \"d\".\"date\" >= CAST(? AS DATE) AND \"d\".\"date\" <= CAST(? AS DATE) GROUP BY 1 ORDER BY \"date.day\" ASC LIMIT ?",
    "rls_applied": [
      "owner_only"
    ],
    "enforced_checks": [
      "registered",
      "dimensions",
      "grain",
      "ast_join_path",
      "rls"
    ],
    "as_of_definition": "2026-10-01",
    "queried_at": "2026-10-07T17:08:30+08:00",
    "row_versioning": "valid_to IS NULL AND metric_version = contract.version",
    "actor_role": "owner"
  }
}

为什么存在

每个 text-to-SQL 工具都在优化同一个问题:SQL 跑通了吗? 但近期的研究表明,在可执行的查询里,仍有 73%–99% 与提问者本意存在静默的语义分歧, 而且只对齐指标口径还不够——访问策略也必须被强制执行。

我们优化的是另一件事:让"返回一个无人能解释的数字"成为不可能。 口径与权限由代码强制,而不是在 prompt 里请求。


这不是什么

  • 不是 text-to-SQL 工具。 LLM 在这里从不写 SQL,只能组合已注册的指标—— 因此不存在"幻觉出一个数字"的攻击面。
  • 不是语义格式。 口径是 YAML,并将保持与 Open Semantic Interchange / Apache Ossie 兼容。我们不在格式上竞争。
  • 不是 BI 仪表盘。 不提供拖拽式看板,图表是下游的事。
  • 不是记忆框架。 不做检索或 embedding。我们做的是记忆框架跳过的那部分:版本、粒度、as-of、血缘、审计。
  • 还不是领域无关的。 执行引擎(contract → compile → enforce → audit)本身与领域无关, 但物理模型映射目前硬接在"个人/纵向数据"这个形态上(日粒度、(date_key, subject_id) 骨架、 声明的 JOIN 路径仍是模块级常量)。泛化成 domains/*.yml 描述符是 路线图 v0.2 第 5 项——在此之前,换一个领域意味着改引擎代码。
  • 无遥测、无云、无账号。 一个文件,local-first。

商业版缺失的那一半

治理能力在这些项目里都被放进了商业版:

项目 被管控的部分在哪
Cube Core RBAC 与多租户 → 商业版
WrenAI 行/列级安全 → 商业版
dbt Semantic Layer 指标查询 → dbt Cloud Team/Enterprise
DataHub 指标值查询:不支持

于是我们把缺的那一半开源做出来:强制校验层。


五条强制校验

每条查询按顺序通过以下五条;失败是拒绝,不是警告。

# 校验 失败时
1 registered — 指标存在且未弃用 拒绝:unknown_metric: / deprecated_metric:
2 dimensions — 请求的维度/过滤器在契约白名单内 拒绝:dimension_not_allowed: / filter_not_allowed:
3 grain — 日粒度指标不能以更细的粒度查询 拒绝:grain_not_compatible:
4 ast_join_path — SQL 用 sqlglot 解析;只允许声明的表、声明的 JOIN 路径,禁止子查询/UNION/CTE 逃逸,禁止 SELECT * 拒绝:ast_violation:
5 rls — 按角色注入行级策略(owner_only / aggregate_min5 / no_pii) 改写 SQL,并写入审计头

执行前另有基于 EXPLAIN 的扫描量预检。


架构

CLI (veriself)   MCP server   ← LLM 仅有的两个入口
     │            │
     └─────┬──────┘   只接受结构化查询对象,绝不接受 SQL 字符串
           ▼
    semantic/   契约 → 校验 → 编译(sqlglot)→ 强制校验 → 审计
           ▼
   warehouse/   DuckDB:dim_*(SCD2)→ fact_* → 物化指标值
           ▼
      synth/    确定性合成主体数据(含植入的潜在结构)

完整接口契约:docs/00-接口契约.md 指标清单:docs/01-指标清单.md

与业界术语的对应关系

如果你见过 dbt MetricFlow、Cube 或 LookML:这里没有为改而改地重命名概念—— 这张表只是翻译器(字段名已冻结,见接口契约):

veriself dbt MetricFlow / 业界 说明
semantic_models/*.yml semantic_models.yml 物理表之上的逻辑列:通道 → 日粒度列、事件表达式、SCD2 维度列
metrics/*.yml metrics.yml 一个文件一个指标,带版本,每个指标有冻结的维度/过滤器白名单
formula_sql type_params.expr(派生指标) 只有派生指标允许 SQL;这里被限制为日粒度标量表达式
bucket: {agg: ...} 指标的 type_params.window + time_granularity 非日粒度的桶内聚合(声明式,不写 OVER() 样板)
agg(顶层) measure agg 跨粒度上卷:请求粒度比指标粒度粗时如何聚合
direction polarity / improves_when(Avo) higher_better / lower_better / neutral
lineage.sources / upstream_metrics 语义模型的 measure + 指标依赖 formula_sql 里仅有的两种引用形式
rls_policy data-mesh 策略 / 行级安全 治理写在指标口径里,不在旁挂目录里
contract_hash + metric_version dbt 节点唯一 ID + 工件版本 每个数字把口径版本与哈希带进审计头

快速开始

git clone <this repo> && cd veriself
uv sync --extra dev          # uv.lock 是依赖的唯一事实来源

veriself init          # 建 DuckDB schema、加载指标契约
veriself synth         # 生成 3 年确定性合成数据
veriself metrics list  # 查看 18 个已注册指标
veriself query --metrics subject.sleep_debt_7d --filters '{"date.last_n_days": 30}' --role owner
veriself reject subject.focus_skore    # 看拒绝原因与相近建议
veriself audit --limit 5               # 每次查询都留痕

不想激活环境时,把 veriself 换成 uv run veriself。

MCP(stdio)可用于任何 MCP 客户端:

// claude_desktop_config.json
{ "mcpServers": { "veriself": { "command": "veriself", "args": ["mcp"] } } }

关于列式存储

我本职工作里的其中一个数仓是 MySQL InnoDB,我反对过把它换成列式引擎。本项目把它的每一条约束都反过来了:

本职工作 本项目
查询形态 点查 跨多年、多维度聚合
表形态 窄 EAV 宽星型模型
写入模式 删+插(毁掉历史) 追加 + SCD2(历史就是功能)
部署 MySQL 一个本地文件,秒级重建

同样的问题,不同的约束,不同的答案——这就是这里用 DuckDB 的原因。


现状

v0.1.0 —— 强制校验层与合成数据集是必须做对的部分;Web UI 有意推迟。

验证结果(本机实测):

项 结果
测试 uv run python -m pytest tests → 373 passed
端到端红队验收 uv run python -m pytest tests/test_e2e_redteam.py → 22 passed(真实链路,非 mock)
合成数据 1,004 天 · fact_observation 106,424 行 · fact_event 5,882 行
指标物化 18 个指标 · 14,408 行 · 重算幂等(无变化时零写入,不累积历史)
植入效应可检出 睡眠 ≤6h 的次日专注度 44.14 vs ≥7.5h 的 50.06(Welch t=−5.29, p=2.3e−06)
内部自洽 独立重算"近 7 日睡眠债" vs 物化结果:最大绝对误差 0.0
双时间轴 同一 (metric, subject, date_key) 在当前版本内多行有效 = 0;值变化才留痕
周内排序 date.day_of_week 按星期序(1→7);date.weekday 是名字,按它排序是字母序

逐文件用例明细、改动后必须满足的验收条件、以及各模块的边界,见 AGENTS.md。

已知限制:

  • dim_context 与事实表没有连接键,因此 dim_context.is_travel 维度/过滤器暂不可用(已在契约里禁止放进白名单,列为 v0.2 工作项)。
  • *_7d 滚动指标的 agg=mean:day→month 上卷得到的是"滚动值的均值",不等于月度均值。逐日展示不受影响。
  • metrics/history/ 里的历史口径版本只用于 as-of 演示与文档,不进 dim_metric(该表主键只有 metric_id)。
  • 容器化尚未交付:v0.1 没有 Dockerfile / compose(记在 docs/03-路线图.md)。
  • 同一指标可同时存在多个有效版本:物化只作用于声明的 metric_version,升级口径不会关闭旧版本的 有效行。因此读取必须带 metric_version 过滤——semantic 已如此实现, 但裸查 fact_metric_value 会同时读到多个版本。

许可证

Apache-2.0 — 见 LICENSE。

Metadata

Release files for veriself 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for veriself 0.1.0
File Size Uploaded
veriself-0.1.0.tar.gz 342.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for veriself 0.1.0
File Interpreter ABI Platform
veriself-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 507.0 kB

Release files / veriself-0.1.0.tar.gz

Download URL veriself-0.1.0.tar.gz
Size 342.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ae293a65a0f93fc7e2a75fdcb58e4150eb6afb6e6ea248bfed16568b53b16c7d
BLAKE2b-256 checksum
How to use checksums
844db46e8f8a5705ec2ae506eec806374936c3664ef2fa0800eb9d5351c1e523
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / veriself-0.1.0-py3-none-any.whl

Download URL veriself-0.1.0-py3-none-any.whl
Size 164.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7ea335c6c7a8eba58a835438e0b706a17be36a1b8ee95e62b6850310a39f075c
BLAKE2b-256 checksum
How to use checksums
9d478e14c833ebec855f0f0ed8e6a0a96c8bd686d763b3de39b02adf06af7b84
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page