数据截至 (上游 commit 7830cc746c11)
上下文层:MDL 语义模型与项目文件
30 秒导读: 一个数据 agent 要可信,靠的不是模型多聪明,而是它手里那份"这张表叫什么、每列什么意思、哪些是规范口径"的知识有多干净。本章讲 Wren 怎么把这层知识建成一堆人能评审、能进 Git、能被校验、能平滑升级的 YAML 项目文件,以及它如何在"拆分 YAML"和"引擎吃的一坨 JSON"之间来回翻译。核心全在
core/wren/src/wren/context.py。
本章只覆盖上下文如何被定义 / 校验 / 版本化。SQL 改写与执行见 03;记忆与检索见 04;Wren 的全景与阅读顺序见 index。
1. 这是什么(零基础也能懂)
一句话定义
MDL(Modeling Definition Language,建模定义语言)是一层"语义地图":它把物理数据库里冷冰冰的表和列,标注成有名字、有描述、有关系的业务对象。context.py 是这层地图的管理器——负责把地图从磁盘上的 YAML 文件读进来、校验对不对、编译成引擎能用的 JSON。
解决什么问题 / 给谁用
假设你让 AI 帮你查"上个季度华东区的复购率"。模型面对的是 ods_t_ord_dtl、f_usr_beh 这种命名,外加 40 张长得差不多的表——它根本不知道哪张是"订单",哪列是"下单时间",更不知道"复购"在你公司是怎么算的。
MDL 就是补上这层缺失知识的地方:你告诉系统"orders 这个模型对应物理表 ods_t_ord_dtl,它有一列 ordered_at 是下单时间,描述是'订单创建的时间戳'"。之后 agent 写 SQL 只需要用 orders、ordered_at 这种语义名,由引擎翻译回物理 SQL。
关键设计:知识是"项目文件",不是数据库配置
Wren 最重要的取舍:这层知识不藏在某个后台的数据库里,而是一堆放在你代码仓库里的 YAML / Markdown 文件。
| 你得到的能力 | 因为知识是"文件" |
|---|---|
| 可评审 | 改一列描述 = 一个 diff,能走 code review |
| 可回滚 | 版本历史就是 Git 历史 |
| 可校验 | CI 里跑一条 wren context validate |
| 可迁移 | schema 升级 = 一次有计划的文件重排 |
这正好对应 README 里 Know 那一拍:让上下文变成可评审、可 Git 的东西。
用起来什么样
一个 Wren 项目在磁盘上大概长这样(schema_version 5 布局):
my-project/
├── wren_project.yml # 项目配置:名字、数据源、schema_version
├── models/
│ └── orders/
│ ├── metadata.yml # orders 模型:列、类型、描述、主键
│ └── ref_sql.sql # (可选) 用一段 SQL 定义模型,而非直接指向表
├── views/
│ └── revenue/
│ └── metadata.yml
├── relationships.yml # 模型之间的 join 关系
├── knowledge/
│ ├── knowledge.yml # 知识轴自己的版本号
│ └── rules/
│ └── general.md # 业务规则(schema 表达不了的口径)
├── AGENTS.md # Wren 生成给 agent 的工作流说明(见 §7)
└── target/
└── mdl.json # 编译产物:引擎真正吃的那份 JSON
你日常就四条命令(它们全是 context.py 里函数的薄封装):
wren context init # 生成上面的骨架
wren context validate # 检查结构对不对
wren context build # 把 YAML 编译成 target/mdl.json
wren context show # 看看当前有哪些模型
一句话直觉
把 MDL 想成"数据库的注释层 + 词汇表",而 context.py 是这本词汇表的编辑器和编译器。 编辑器保证词条格式合规(validate),编译器把散落的词条装订成引擎能查的成书(build)。
本节到此不碰底层实现。下面开始拆。
2. 顶层全景(它大概怎么转)
一张图:上下文的三条主线
context.py 干三件事:发现项目 → 加载 + 校验语义对象 → 编译 / 版本迁移。下图从左到右是数据流,认准三个方框各自的职责即可。
磁盘上的 YAML 项目 context.py 引擎与 agent
┌────────────────────┐ ┌───────────────────────┐ ┌────────────────────┐
│ wren_project.yml │─────▶│ ① 发现 │ │ │
│ models/*/… │ │ discover_project_path │ │ │
│ views/*/… │ │ load_project_config │ │ │
│ relationships.yml │ └──────────┬────────────┘ │ │
│ cubes/*/… │ │ │ │
│ knowledge/rules/* │ ┌──────────▼────────────┐ │ │
│ instructions.md │─────▶│ ② 加载(→ snake_case) │ │ │
└────────────────────┘ │ load_models/views/… │ │ │
│ load_rules/knowledge │ │ │
└──────────┬────────────┘ │ │
│ │ │
┌──────────▼────────────┐ │ │
│ ③ 校验 │ │ 错误/警告清单 │
│ validate_project │─────▶│ (人 & CI 读) │
│ _check_descriptions │ │ │
└──────────┬────────────┘ │ │
│ │ │
┌──────────▼────────────┐ │ │
│ ④ 编译(→ camelCase) │ │ target/mdl.json │
│ build_manifest │─────▶│ → wren-core 引擎 │
│ build_json/save_target│ │ → memory 索引 │
└───────────────────────┘ └────────────────────┘
另有两条支线:convert_mdl_to_project(JSON→拆分YAML,导入用)
plan/apply_upgrade(schema 版本迁移)
部件一句话职责
| 部件 | 干什么 | 关键符号 |
|---|---|---|
| 发现 | 找到"当前项目根在哪" | discover_project_path load_project_config |
| 加载 | 把各类 YAML 读成统一的 snake_case dict | load_models load_views load_cubes load_relationships |
| 非 schema 知识 | 读业务规则(Markdown) | load_rules load_knowledge_rules load_instructions |
| 校验 | 结构 + 描述完整性检查 | validate_project validate_manifest _check_descriptions |
| 编译 | YAML → 引擎吃的 JSON | build_manifest build_json save_target |
| 反向转换 | JSON → 拆分 YAML 项目 | convert_mdl_to_project write_project_files |
| 版本迁移 | schema 演进的平滑升级 | plan_upgrade apply_upgrade |
| agent 脚手架 | 往用户项目里写 AGENTS.md | _AGENTS_MD_TEMPLATE |
一条贯穿全篇的暗线:两种命名法
Wren 内部有个反复出现的翻译动作, 先记住它,后面处处都是它:
- 磁盘上的 YAML 用
snake_case(table_reference、ref_sql)——对人友好。 - 引擎吃的 JSON 用
camelCase(tableReference、refSql)——对 Rust 引擎友好。
所以每次"读进来"要 snake 化,每次"编译出去"要 camel 化。这对翻译函数是 _snake_to_camel / _camel_to_snake(见 §5.5)。
3. 核心原理(逐个机制,由浅入深)
3.1 项目发现:agent 怎么知道"当前在哪个项目"
要解决的小问题: 用户在终端某个子目录里敲 wren context build,系统怎么知道项目根在哪?
思路: 给出一条优先级链,从"最明确"到"最兜底"依次尝试,任何一步命中就返回。这样既能让 CI 用环境变量钉死,又能让人随手在项目里的任意子目录敲命令。
优先级从上到下(命中即停):
discover_project_path(explicit)
│
① explicit 参数(--project / --path)? ──有──▶ 用它
│无
② 环境变量 WREN_PROJECT_HOME? ──有──▶ 用它
│无
③ 从 cwd 向上逐级找 wren_project.yml ──找到──▶ 那一级就是根
│(到 home 或文件系统根就停)
│没找到
④ ~/.wren/config.yml 里的 default_project ──有──▶ 用它
│无
⑤ SystemExit:告诉你怎么创建项目
真实实现: context.py:380 的 discover_project_path 按上述顺序实现;第 3 步的向上遍历用 for parent in [current, *current.parents],并在 parent == Path.home() or parent == parent.parent 处 停下,避免走出用户目录(context.py:400-405)。找不到时不是返回 None,而是抛 SystemExit 附一句可操作的提示(context.py:412-416)——这是全文件一贯的"错误即人话"风格。
PROJECT_FILE = "wren_project.yml"(context.py:16)是发现机制的锚点文件;load_project_config 只是把它 yaml.safe_load 成 dict(context.py:419-424)。
一个小而稳的细节: 回写配置用的是 save_project_config(context.py:441),它按 _PROJECT_FIELD_ORDER(context.py:430)固定字段顺序输出,让 set-profile、upgrade 这类会重写文件的命令不会把人排好的字段打乱。代价写在 docstring 里:会丢 YAML 注释(要保注释得引 ruamel.yaml,不值当)。
3.2 加载语义对象:一份加载器,两套磁盘布局
要解决的小问题: Wren 的项目布局演进过——老版本(v1)把每个模型放一个扁平文件 models/orders.yml;新版本(v2 及以后)一个模型一个目录 models/orders/metadata.yml。加载器要同时认这两套。
思路: 对外只暴露一个 load_models,它先读 schema_version,再分派到 _load_models_v1 或 _load_models_v2。views、cubes 同理。
# 示意,非源码:分派的骨架
def load_models(project_path):
sv = get_schema_version(project_path) # 读 wren_project.yml
if sv == 1:
return _load_models_v1(project_path) # 扁平 models/*.yml
return _load_models_v2(project_path) # 目录 models/<name>/metadata.yml
真实实现: 分派逻辑在 context.py:549 的 load_models(v1 → _load_models_v1 at context.py:562,v2 → _load_models_v2 at context.py:596)。同样的双布局套路复用在:
| 对象 | 入口 | v1 布局 | v2+ 布局 |
|---|---|---|---|
| 模型 | load_models | models/*.yml | models/<name>/metadata.yml (+可选 ref_sql.sql) |
| 视图 | load_views | views.yml 单文件 | views/<name>/metadata.yml (+可选 sql.yml) |
| Cube | load_cubes | cubes/*.yml | cubes/<name>/metadata.yml |
| 关系 | load_relationships | relationships.yml | relationships.yml(不分版 本) |
一个值得记的设计:SQL 从 YAML 里拆出来。 v2 布局允许把模型的 ref_sql 单独放 ref_sql.sql、把视图的 statement 放 sql.yml。加载时,独立 SQL 文件的内容会覆盖 metadata.yml 里的内联值(_load_models_v2 at context.py:599-603;_load_views_v2 at context.py:660-664)。好处:一段几十行的 SQL 用 .sql 文件写,能享受编辑器高亮、diff 也干净,不用被 YAML 的缩进和引号折磨。
内部记账字段: 加载时每个对象会被塞一个 _source_dir(或 cube 的 _source_file)记住它来自哪个目录(context.py:596)。这个字段只服务校验时的报错定位(见 §3.4),编译前会被 build_manifest 用 pop 剥掉(context.py:834-839)——不让内部记账泄进引擎。
3.3 非 schema 知识:那些"表结构表达不了"的口径
要解决的小问题: 有些业务知识没法用列和类型表达——"营收一律用 orders_clean 这张去重表""金额字段单位是分不是元""状态码 3 表示已退款"。这类规则得有地方放。
思路: 放进 knowledge/rules/*.md,当成自由文本的业务规则,注入给 agent 当上下文。这就是 README Know 那一拍里"可评审、可 Git 的上下文"的落点。
加载有三层,注意新旧关系:
| 函数 | 读什么 | 状态 |
|---|---|---|
load_knowledge_rules | knowledge/rules/*.md(排序后拼接) | v5 正道 |
load_instructions | instructions.md(单文件) | 已废弃(legacy) |
load_rules | 上面两者合并,并回报是否用了 legacy | 对外统一入口 |
真实实现: load_knowledge_rules(context.py:754)把 knowledge/rules/ 下所有 .md 排序后拼接,用海象运算符 := 顺带过滤空文件(context.py:759-764)。load_rules(context.py:787)把新规则和 legacy instructions.md 合并,并返回 (content, used_legacy) 二元组——used_legacy 让 CLI 能提示用户"你还在用废弃的 instructions.md,该迁移了"(context.py:781-784)。这个 flag 是基于文件存在性判断的:哪怕 instructions.md 是空的,只要文件在就算命中废弃模式。
知识轴有自己的版本号。 knowledge/knowledge.yml 里的 schema_version 和 wren_project.yml 里的 MDL schema_version 是两条独立的轴(load_knowledge_config at context.py:787,get_knowledge_schema_version at context.py:796)。设计意图写在常量注释里(context.py:470-476):知识的演进节奏和 MDL schema 的演进节奏解耦,各升各的。get_knowledge_schema_version 在完全没有 knowledge/ 目录时返回 0,以区分"没有知识层"和"知识层版本 1"。
3.4 结构校验:为什么"每列都要有描述"是检索质量的前提
要解决的小问题: 一份 MDL 可能各种残缺——模型没名字、列缺类型、关系指向不存在的模型、主键列压根不在列表里。这些错要在编译前就抓出来,而不是等引擎报一句看不懂的 Rust 报错。
思路:分两层校验,各管一段。
| 层 | 函数 | 管什么 | 何时跑 |
|---|---|---|---|
| 结构校验 | validate_project | 纯 YAML 层面的完整性 / 引用 / 重名 | 纯本地,不碰引擎 |
| 语义校验 | validate_manifest | 视图 SQL 能否 dry-plan + 描述完整性 | 需要引擎,wren context validate |
结构校验查的东西(validate_project at context.py:890):
- 每个模型有
name、至少一列;每列有name、type(context.py:1045-1144) - 模型必须恰好有一个
table_reference或ref_sql——两个都有或都没有都报错(context.py:1065-1082) - 关系引用的模型名必须存在(
context.py:1352-1371) - 手改 YAML 的形态错检:模型/列/视图的
properties非映射、views.yml/relationships.yml条目 非对象,都会被点名报错(加载器会悄悄丢掉这类条目,校验负责把问题亮出来,context.py:1200-1298) - 模型 / 视图不重名;列不重名(
context.py:1058-1061、context.py:1127-1135) primary_key列必须真的在 columns 里(context.py:1162-1170)- 版本相关的门禁:复合主键(list 形式)要求
schema_version >= 4(context.py:1174-1181);dialect字段要求>= 3(context.py:1184-1193)
校验不抛异常,而是攒一份清单。 每个问题是一个 ValidationError(context.py:878),带 level("error" / "warning")、path(精确到 models/orders/metadata.yml > orders > columns[2])、message 三件套。这就是 §3.2 里 _source_dir 记账的用途——报错能指到具体文件具体位置。validate_project 返回一整个 list[ValidationError],让人 / CI 一次看全所有问题,而不是修一个报一个。
为什么描述这么重要: 语义校验里的 _check_descriptions(context.py:1906)会对没有 properties.description 的模型 / 视图 / 列发警告。原因直接写在警告文案里(context.py:1912-1930):
- 模型没描述 →
"improve memory search and agent comprehension"(拖累 04 的向量检索召回) - 视图没描述 → 有描述的视图会被当作 NL-SQL 示例索引进 memory
换句话说:描述不是给人看的注释,它是检索系统的输入。 一列没描述,agent 就更难在成百上 千的列里把它召回来。所以 Wren 把"补描述"从"良好习惯"升格成"可校验的治理项"——strict 级别会连列级描述缺失一起查(context.py:1916-1922)。
validate_manifest 的三档 level 是渐进的(context.py:1935):
| level | 查什么 | 用途 |
|---|---|---|
error | 只查视图 SQL 能否 dry-plan | CI/CD 硬门禁 |
warning(默认) | + 模型 / 视图缺描述 | 日常 |
strict | + 列缺描述 | 追求高检索质量 |
3.5 编译:YAML 项目 → 引擎吃的 mdl.json
要解决的小问题: 引擎(wren-core, Rust)不认识散落的 YAML 目录,它要一份 camelCase 的 JSON manifest。
思路:三步流水线,一步只干一件事:
build_manifest ──▶ build_json ──▶ save_target
(装配 snake dict) (转 camel + (写 target/mdl.json)
盖 layoutVersion)
逐步看:
-
build_manifest(context.py:820):调用各load_*装配成一个 snake_case 的大 dict,填上catalog(默认"wren")、schema(默认"public")、models / views / relationships / cubes。顺带剥掉_source_dir/_source_file内部记账字段(context.py:834-839)。注意:instructions 不进 manifest——业务规则是独立通道,不混进 schema JSON(docstringcontext.py:825)。 -
build_json(context.py:855):用_convert_keys把整棵 dict 递归 snake→camel(context.py:860),再按schema_version查_LAYOUT_VERSION_MAP盖上layoutVersion(context.py:862)。layoutVersion是给引擎看的"这份 JSON 是哪代线格式"。 -
save_target(context.py:866):把 JSON 写到target/mdl.json,用ensure_ascii=False保留中文等非 ASCII 字符原样(context.py:871)。
_TARGET_DIR = "target" / _TARGET_FILE = "mdl.json" 是常量(context.py:17-18)。CLI 侧 wren context build 就是把这三步串起来(context_cli.py:653-660)。
4. 深入实现:反向转换与版本迁移
前面讲的是"正向":YAML → JSON。这一节讲两条更硬核的支线——反向转换(导入别处的 MDL)和版本迁移(schema 升级)。
4.1 反向:MDL JSON → 拆分 YAML 项目
场景: 你从旧 Wren、或某个导出工具手里拿到一份完整的 mdl.json,想把它变成一个"可评审、可 Git"的拆分 YAML 项目。这正是 convert_mdl_to_project(context.py:155)干的。
它返回一串 ProjectFile(context.py:147,relative_path + content 的 dataclass),而不是直接写盘——把"算出要写哪些文件"和"真去写盘"解耦。真正落盘由 write_project_files(context.py:306)负责。
convert_mdl_to_project 把一坨 JSON 炸开成拆分文件:
| 输入 JSON 片段 | 产出文件 |
|---|---|
| 顶层 name/catalog/schema/dataSource | wren_project.yml |
| 每个 model | models/<name>/metadata.yml(+ ref_sql.sql 若有) |
| 每个 view | views/<name>/metadata.yml(+ sql.yml 若多行) |
| relationships | relationships.yml |
_instructions | knowledge/rules/general.md |
| (固定) | knowledge/knowledge.yml、AGENTS.md |
炸开时对每个对象做 camel→snake(_convert_keys_to_snake,context.py:201),让落盘的 YAML 是人友好的 snake_case——这是 §2 暗线的反方向。
一个有意思的映射:layoutVersion → schema_version。 导入时输入 JSON 的 layoutVersion(引擎线格式)要落到项目应该用的 schema_version。因为 layoutVersion 3 同时覆盖 v4(复合主键)和 v5,新导入直接对齐当前布局 v5(_LAYOUT_TO_SCHEMA at context.py:171,注释在 context.py:167-170)。
写盘的两道安全闸(write_project_files):
- 非
--force时,任何目标文件已存在就整体拒绝,报"用 --force 覆盖"(context.py:354-362)——不会悄悄覆盖你手写的东西。 - 路径逃逸防护:写盘前先对每个待写路径做一轮统一预检——
resolve()后必须仍在输出目录内、 且不能正好等于输出根,否则SystemExit(context.py:320-331)。防的是恶意 / 畸形的relative_path(比如../../etc/x)把文件写到项目外。--force模式只清理一个白名单里的 托管路径(context.py:336-345),不碰你项目里的其它文件。
4.2 schema 版本:识别、门禁与迁移
要解决的小问题: 上游 schema 会演进(v1→v2 布局重排、v3 加 dialect、v4 复合主键、v5 引入 knowledge/)。老项目要能被识别、被平滑升级,而不是一升级就散架。
支持的版本和它们的 差异:
| schema_version | 关键变化 | layoutVersion |
|---|---|---|
| 1 | 扁平文件布局 | 1 |
| 2 | 目录-per-模型布局 | 1 |
| 3 | 引入 dialect 字段 | 2 |
| 4 | 复合主键(list 形式) | 3 |
| 5 | knowledge/ 成为一等项目布局 | 3 |
_SUPPORTED_SCHEMA_VERSIONS = {1,2,3,4,5}(context.py:463),_LAYOUT_VERSION_MAP 是 schema→引擎 layoutVersion 的映射(context.py:468)。注意 v4 和 v5 都映射到 layoutVersion 3——因为 v5 只是项目布局的演进,不改引擎吃的 JSON 格式(注释 context.py:465-467)。
识别 + 门禁: get_schema_version(context.py:503)读版本号,非整数就报人话错误;require_schema_version(context.py:515)在此之上多一道门禁——版本不在支持集里就叫你"升级 wren CLI"。
迁移分两阶段:先算计划,再落盘。 这是全文件反复出现的 plan/apply 分离模式:
plan_upgrade(project, target) | apply_upgrade(project, result) |
|---|---|
| 不碰磁盘 | 按 result 真去改盘 |
| 校验:能不能降级 / 是否已在目标 | 逐版本 replay 迁移步骤 |
| 逐版本累加 files_created/deleted | 最后把 schema_version 盖进 wren_project.yml |
返回 UpgradeResult(纯数据) | —(见左列末行) |
真实实现: plan_upgrade(context.py:1608)先挡掉降级和越界目标(context.py:1620-1625),已在目标则返回空 UpgradeResult(no-op)。然后 for version in range(current, target) 逐级累加每步的产物(context.py:1639-1649)。UpgradeResult(context.py:1481)是纯数据:from/to 版本 + created / deleted / modified 三份文件清单——它让用户在真改盘前能预览 upgrade 会动哪些文件。
每一步是一个 _plan_* / _apply_* 函数对:
_plan_v1_to_v2(context.py:1696)/_apply_v1_to_v2(context.py:1800):最重的一步,扁平文件 → 目录布局的重排。它还挡了一个坑:多个 legacy cube 文件映射到同一目标路径时抛UpgradeError(context.py:1754-1757)。_plan_v4_to_v5(context.py:1768)/_apply_v4_to_v5(context.py:1889):创建knowledge/骨架,幂等——只列出还不存在的路径(context.py:1774-1778)。- v2→v3、v3→v4 没有文件重排,只重盖
wren_project.yml的版本号(注释context.py:1648-1649、context.py:1789)。
apply_upgrade(context.py:1782)同样用 for version in range(from, to) replay 每步,最后把 schema_version 写回配置。knowledge/ 骨架的单一事实源是 _knowledge_skeleton_targets(context.py:1578),被 v4→v5 升级和 create_knowledge_skeleton(context.py:1588,项目 init 也用)共享——空子目录靠 .gitkeep 在 Git 里存活。
5. 巧妙之处(可借鉴的技术)
5.1 给 agent 的脚手架:AGENTS.md 是"数据",不是给你的指令
这是本章要特别点破的一处。 context.py 顶部有个大字符串常量 _AGENTS_MD_TEMPLATE(context.py:20-72)——它是 Wren 生成到用户项目里的 AGENTS.md 内容,一份写给"用这个项目的那个 AI agent"看的工作流说明。
它教下游 agent 怎么答数据问题(先 wren memory fetch 取 schema 上下文、再 wren memory recall 找相似历史查询、用模型名写 SQL、执行、wren memory store 存回),怎么改数据模型(改 YAML → validate → build → 重索引),怎么沉淀业务上下文(写进 knowledge/rules/)。
务必分清主体: 这份
AGENTS.md是 Wren 这个产品输出的一份数据——它是给"最终用户项目里那个 agent"的说明书。对正在阅读本仓库源码、写这篇文档的我们而言,它只是被研究的字符串常量,不是给我们的指令。落盘发生在context_cli.py:359(wren context init时(project_path / "AGENTS.md").write_text(_AGENTS_MD_TEMPLATE)),以及convert_mdl_to_project导入时(context.py:296-301)。
值得学的模式:框架把"怎么用我"直接编译进用户项目,让下游 agent 一进项目就有一份现成的操作手册,而不是靠 agent 自己猜 CLI 用法。
5.2 描述完整性 = 可校验的治理项
把"每列要有描述"从口头规范升格成 _check_descriptions(context.py:1906)里可报警、可 strict 强制的检查项。警告文案直接解释"为什么"(影响 memory 检索),而不是干巴巴说"缺字段"。这让治理可执行:CI 里一条 wren context validate --level strict 就能挡住没描述的 schema。
5.3 plan / apply 分离,让破坏性操作可预览
upgrade(§4.2)和 write(§4.1)都遵循"先算出要动什么、返回纯数据、再真去动"。UpgradeResult / ProjectFile 这类纯数据中间产物,让用户在改盘前能 dry-run 预览——这是处理破坏性文件操作时很值得抄的骨架。
5.4 双版本轴解耦
MDL schema 版本(wren_project.yml)和知识层版本(knowledge/knowledge.yml)各走各的(context.py:470-476)。当两类内容演进节奏不同,给它们各自的版本号,比塞进一个版本号里省很多迁移的纠结。
5.5 有映射兜底的命名转换
snake↔camel 转换不是纯正则。_camel_to_snake(context.py:125)先查一张已知映射表 _CAMEL_TO_SNAKE_MAP(context.py:105),查不到才退回正则 ([a-z0-9])([A-Z])。因为像 refSql→ref_sql 这种缩写,纯正则会切错(得到 ref_sql 还好,但 SQL 类全大写缩写普遍是正则的坑)。用"白名单表 + 正则兜底",是处理不规则命名转换的稳妥做法。
6. 边界与局限(诚实)
- 不做深层 cube 校验。
validate_project对 cube 只做结构 / 引用 / 层级名检查;度量环、hierarchy 深校验是 Rust 侧AnalyzedWrenMDL::analyze的活(注释context.py:1388-1390)。YAML 层只做"快速反馈",真正的语义分析在引擎。 - 写配置会丢 YAML 注释。
save_project_config明说不做注释 round-trip(context.py:445-447),因为那要引ruamel.yaml。注释只在 init 模板里有,一旦被 CLI 重写就没了。 - 只支持升级,不支持降级。
plan_upgrade明确挡掉target < current(context.py:1622-1625)。 - legacy 判定是"存在即命中"。 哪怕
instructions.md是空文件,load_rules也会报used_legacy=True(context.py:781-783)——想去掉警告必须删文件,清空不够。 - 视图 dry-plan 校验要真引擎。
validate_manifest里error级的视图 SQL 校验需要起WrenEngine(context.py:1983-1985),纯结构校验才是零依赖的。
7. 横向对比(同组其它章)
本章讲的是"上下文如何被定义 / 校验 / 版本化"。它是下面几章的上游:
| 关切 | 在哪讲 | 与本章的关系 |
|---|---|---|
| Agent 驱动的 CLI 与 skills | 01 | wren context * 命令怎么被 agent 调用 |
| 模型名 SQL → 受治理物理 SQL | 03 | 消费本章编译出的 target/mdl.json |
| 记忆与检索 | 04 | 索引本章的 schema + 描述;§3.4 的描述完整性直接决定其召回质量 |
| GenBI 仪表盘与部署 | 05 | 依赖已建好的语义模型 |
一句话:本章把"知识"建好、校好、编译好,后面几章 才有可信的地基去改写 SQL、去检索、去出图。
8. 代码地图(导航索引)
全部符号在 core/wren/src/wren/context.py,除最后一行外。用符号名 grep 比行号抗漂移。
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 项目发现(优先级链) | core/wren/src/wren/context.py | discover_project_path |
| 读 / 写项目配置 | core/wren/src/wren/context.py | load_project_config save_project_config PROJECT_FILE |
| 全局配置 | core/wren/src/wren/context.py | load_global_config |
| 模型加载(双布局分派) | core/wren/src/wren/context.py | load_models _load_models_v1 _load_models_v2 |
| 视图 / cube / 关系加载 | core/wren/src/wren/context.py | load_views load_cubes load_relationships |
| 业务规则(非 schema 知识) | core/wren/src/wren/context.py | load_rules load_knowledge_rules load_instructions load_knowledge_config |
| 知识轴版本 | core/wren/src/wren/context.py | get_knowledge_schema_version _KNOWLEDGE_SUBDIRS |
| 结构校验 | core/wren/src/wren/context.py | validate_project ValidationError |
| 语义 / 描述校验 | core/wren/src/wren/context.py | validate_manifest _check_descriptions _prop_description |
| 编译流水线 | core/wren/src/wren/context.py | build_manifest build_json save_target |
| JSON→拆分 YAML | core/wren/src/wren/context.py | convert_mdl_to_project write_project_files ProjectFile |
| 命名转换 | core/wren/src/wren/context.py | _snake_to_camel _camel_to_snake _convert_keys _CAMEL_TO_SNAKE_MAP |
| schema 版本 / 门禁 | core/wren/src/wren/context.py | get_schema_version require_schema_version _SUPPORTED_SCHEMA_VERSIONS _LAYOUT_VERSION_MAP |
| 版本迁移(plan/apply) | core/wren/src/wren/context.py | plan_upgrade apply_upgrade _plan_v1_to_v2 _plan_v4_to_v5 UpgradeResult |
| 知识骨架 | core/wren/src/wren/context.py | create_knowledge_skeleton _knowledge_skeleton_targets |
| agent 脚手架模板 | core/wren/src/wren/context.py | _AGENTS_MD_TEMPLATE |
| 连接字段登记 | core/wren/src/wren/model/field_registry.py | DATASOURCE_MODELS FieldDef get_fields get_variants |
附:field_registry.py 的角色(core/wren/src/wren/model/field_registry.py)。 它不属于 MDL 上下文,但同属"让配置可信"的一环:从 Pydantic 连接信息模型自动派生每个数据源的连接字段定义,做成 FieldDef(field_registry.py:73),供 CLI 交互提示、MCP web 表单、文档生成三处共用一份事实源(模块 docstring field_registry.py:1-7)。核心是 DATASOURCE_MODELS(field_registry.py:45,数据源→Pydantic 模型映射)和 get_fields(field_registry.py:341,产出有序字段列表,自动排除不适合表单的 dict 字段)。派生结果可被 _MODEL_UI_OVERRIDES / _DATASOURCE_UI_OVERRIDES(field_registry.py:92、field_registry.py:181)微调 label / 输入类型。这样新增一个数据源只改 Pydantic 模型,三处 UI 自动跟上。