跳到主要内容

上下文层: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_dtlf_usr_beh 这种命名,外加 40 张长得差不多的表——它根本不知道哪张是"订单",哪列是"下单时间",更不知道"复购"在你公司是怎么算的。

MDL 就是补上这层缺失知识的地方:你告诉系统"orders 这个模型对应物理表 ods_t_ord_dtl,它有一列 ordered_at 是下单时间,描述是'订单创建的时间戳'"。之后 agent 写 SQL 只需要用 ordersordered_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 dictload_models load_views load_cubes load_relationships
非 schema 知识读业务规则(Markdown)load_rules load_knowledge_rules load_instructions
校验结构 + 描述完整性检查validate_project validate_manifest _check_descriptions
编译YAML → 引擎吃的 JSONbuild_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_referenceref_sql)——对人友好。
  • 引擎吃的 JSON 用 camelCase(tableReferencerefSql)——对 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:363discover_project_path 按上述顺序实现;第 3 步的向上遍历用 for parent in [current, *current.parents],并在 parent == Path.home() or parent == parent.parent 处停下,避免走出用户目录(context.py:383-388)。找不到时不是返回 None,而是抛 SystemExit 附一句可操作的提示(context.py:395-399)——这是全文件一贯的"错误即人话"风格。

PROJECT_FILE = "wren_project.yml"(context.py:16)是发现机制的锚点文件;load_project_config 只是把它 yaml.safe_load 成 dict(context.py:402-407)。

一个小而稳的细节: 回写配置用的是 save_project_config(context.py:424),它按 _PROJECT_FIELD_ORDER(context.py:413)固定字段顺序输出,让 set-profileupgrade 这类会重写文件的命令不会把人排好的字段打乱。代价写在 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:512load_models(v1 → _load_models_v1 at context.py:525,v2 → _load_models_v2 at context.py:539)。同样的双布局套路复用在:

对象入口v1 布局v2+ 布局
模型load_modelsmodels/*.ymlmodels/<name>/metadata.yml (+可选 ref_sql.sql)
视图load_viewsviews.yml 单文件views/<name>/metadata.yml (+可选 sql.yml)
Cubeload_cubescubes/*.ymlcubes/<name>/metadata.yml
关系load_relationshipsrelationships.ymlrelationships.yml(不分版本)

一个值得记的设计:SQL 从 YAML 里拆出来。 v2 布局允许把模型的 ref_sql 单独放 ref_sql.sql、把视图的 statementsql.yml。加载时,独立 SQL 文件的内容会覆盖 metadata.yml 里的内联值(_load_models_v2 at context.py:562-566;_load_views_v2 at context.py:617-621)。好处:一段几十行的 SQL 用 .sql 文件写,能享受编辑器高亮、diff 也干净,不用被 YAML 的缩进和引号折磨。

内部记账字段: 加载时每个对象会被塞一个 _source_dir(或 cube 的 _source_file)记住它来自哪个目录(context.py:559)。这个字段只服务校验时的报错定位(见 §3.4),编译前会被 build_manifestpop 剥掉(context.py:782-787)——不让内部记账泄进引擎。

3.3 非 schema 知识:那些"表结构表达不了"的口径

要解决的小问题: 有些业务知识没法用列和类型表达——"营收一律用 orders_clean 这张去重表""金额字段单位是分不是元""状态码 3 表示已退款"。这类规则得有地方放。

思路: 放进 knowledge/rules/*.md,当成自由文本的业务规则,注入给 agent 当上下文。这就是 README Know 那一拍里"可评审、可 Git 的上下文"的落点。

加载有三层,注意新旧关系:

函数读什么状态
load_knowledge_rulesknowledge/rules/*.md(排序后拼接)v5 正道
load_instructionsinstructions.md(单文件)已废弃(legacy)
load_rules上面两者合并,并回报是否用了 legacy对外统一入口

真实实现: load_knowledge_rules(context.py:702)把 knowledge/rules/ 下所有 .md 排序后拼接,用海象运算符 := 顺带过滤空文件(context.py:707-712)。load_rules(context.py:715)把新规则和 legacy instructions.md 合并,并返回 (content, used_legacy) 二元组——used_legacy 让 CLI 能提示用户"你还在用废弃的 instructions.md,该迁移了"(context.py:729-732)。这个 flag 是基于文件存在性判断的:哪怕 instructions.md 是空的,只要文件在就算命中废弃模式。

知识轴有自己的版本号。 knowledge/knowledge.yml 里的 schema_versionwren_project.yml 里的 MDL schema_version 是两条独立的轴(load_knowledge_config at context.py:735,get_knowledge_schema_version at context.py:744)。设计意图写在常量注释里(context.py:453-459):知识的演进节奏和 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:838):

  • 每个模型有 name、至少一列;每列有 nametype(context.py:932-1029)
  • 模型必须恰好有一个 table_referenceref_sql——两个都有或都没有都报错(context.py:947-964)
  • 关系引用的模型名必须存在(context.py:1136-1156)
  • 模型 / 视图不重名;列不重名(context.py:940-943context.py:1012-1020)
  • primary_key 列必须真的在 columns 里(context.py:1047-1055)
  • 版本相关的门禁:复合主键(list 形式)要求 schema_version >= 4(context.py:1059-1066);dialect 字段要求 >= 3(context.py:1069-1078)

校验不抛异常,而是攒一份清单。 每个问题是一个 ValidationError(context.py:826),带 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:1549)会对没有 properties.description 的模型 / 视图 / 列发警告。原因直接写在警告文案里(context.py:1555-1573):

  • 模型没描述 → "improve memory search and agent comprehension"(拖累 04 的向量检索召回)
  • 视图没描述 → 有描述的视图会被当作 NL-SQL 示例索引进 memory

换句话说:描述不是给人看的注释,它是检索系统的输入。 一列没描述,agent 就更难在成百上千的列里把它召回来。所以 Wren 把"补描述"从"良好习惯"升格成"可校验的治理项"——strict 级别会连列级描述缺失一起查(context.py:1559-1565)。

validate_manifest 的三档 level 是渐进的(context.py:1578):

level查什么用途
error只查视图 SQL 能否 dry-planCI/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)

逐步看:

  1. build_manifest(context.py:768):调用各 load_* 装配成一个 snake_case 的大 dict,填上 catalog(默认 "wren")、schema(默认 "public")、models / views / relationships / cubes。顺带剥掉 _source_dir / _source_file 内部记账字段(context.py:782-787)。注意:instructions 不进 manifest——业务规则是独立通道,不混进 schema JSON(docstring context.py:773)。

  2. build_json(context.py:803):用 _convert_keys 把整棵 dict 递归 snake→camel(context.py:808),再按 schema_version_LAYOUT_VERSION_MAP 盖上 layoutVersion(context.py:810)。layoutVersion 是给引擎看的"这份 JSON 是哪代线格式"。

  3. save_target(context.py:814):把 JSON 写到 target/mdl.json,用 ensure_ascii=False 保留中文等非 ASCII 字符原样(context.py:819)。

_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:144)干的。

它返回一串 ProjectFile(context.py:136,relative_path + content 的 dataclass),而不是直接写盘——把"算出要写哪些文件"和"真去写盘"解耦。真正落盘由 write_project_files(context.py:295)负责。

convert_mdl_to_project 把一坨 JSON 炸开成拆分文件:

输入 JSON 片段产出文件
顶层 name/catalog/schema/dataSourcewren_project.yml
每个 modelmodels/<name>/metadata.yml(+ ref_sql.sql 若有)
每个 viewviews/<name>/metadata.yml(+ sql.yml 若多行)
relationshipsrelationships.yml
_instructionsknowledge/rules/general.md
(固定)knowledge/knowledge.ymlAGENTS.md

炸开时对每个对象做 camel→snake(_convert_keys_to_snake,context.py:190),让落盘的 YAML 是人友好的 snake_case——这是 §2 暗线的反方向。

一个有意思的映射:layoutVersion → schema_version。 导入时输入 JSON 的 layoutVersion(引擎线格式)要落到项目应该用的 schema_version。因为 layoutVersion 3 同时覆盖 v4(复合主键)和 v5,新导入直接对齐当前布局 v5(_LAYOUT_TO_SCHEMA at context.py:160,注释在 context.py:156-159)。

写盘的两道安全闸(write_project_files):

  1. --force 时,任何目标文件已存在就整体拒绝,报"用 --force 覆盖"(context.py:331-339)——不会悄悄覆盖你手写的东西。
  2. 路径逃逸防护:每个待写路径 resolve() 后必须仍在输出目录内,否则 SystemExit(context.py:341-347)。防的是恶意 / 畸形的 relative_path(比如 ../../etc/x)把文件写到项目外。--force 模式只清理一个白名单里的托管路径(context.py:313-320),不碰你项目里的其它文件。

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
5knowledge/ 成为一等项目布局3

_SUPPORTED_SCHEMA_VERSIONS = {1,2,3,4,5}(context.py:446),_LAYOUT_VERSION_MAP 是 schema→引擎 layoutVersion 的映射(context.py:451)。注意 v4 和 v5 都映射到 layoutVersion 3——因为 v5 只是项目布局的演进,不改引擎吃的 JSON 格式(注释 context.py:448-450)。

识别 + 门禁: get_schema_version(context.py:486)读版本号,非整数就报人话错误;require_schema_version(context.py:498)在此之上多一道门禁——版本不在支持集里就叫你"升级 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:1308)先挡掉降级和越界目标(context.py:1320-1325),已在目标则返回空 UpgradeResult(no-op)。然后 for version in range(current, target) 逐级累加每步的产物(context.py:1339-1349)。UpgradeResult(context.py:1263)是纯数据:from/to 版本 + created / deleted / modified 三份文件清单——它让用户在真改盘前能预览 upgrade 会动哪些文件。

每一步是一个 _plan_* / _apply_* 函数对:

  • _plan_v1_to_v2(context.py:1360)/ _apply_v1_to_v2(context.py:1451):最重的一步,扁平文件 → 目录布局的重排。它还挡了一个坑:多个 legacy cube 文件映射到同一目标路径时抛 UpgradeError(context.py:1405-1408)。
  • _plan_v4_to_v5(context.py:1419)/ _apply_v4_to_v5(context.py:1535):创建 knowledge/ 骨架,幂等——只列出还不存在的路径(context.py:1425-1429)。
  • v2→v3、v3→v4 没有文件重排,只重盖 wren_project.yml 的版本号(注释 context.py:1348-1349context.py:1440)。

apply_upgrade(context.py:1433)同样用 for version in range(from, to) replay 每步,最后把 schema_version 写回配置。knowledge/ 骨架的单一事实源是 _knowledge_skeleton_targets(context.py:1278),被 v4→v5 升级和 create_knowledge_skeleton(context.py:1288,项目 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:285-290)。

值得学的模式:框架把"怎么用我"直接编译进用户项目,让下游 agent 一进项目就有一份现成的操作手册,而不是靠 agent 自己猜 CLI 用法。

5.2 描述完整性 = 可校验的治理项

把"每列要有描述"从口头规范升格成 _check_descriptions(context.py:1549)里可报警、可 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:453-459)。当两类内容演进节奏不同,给它们各自的版本号,比塞进一个版本号里省很多迁移的纠结。

5.5 有映射兜底的命名转换

snake↔camel 转换不是纯正则。_camel_to_snake(context.py:114)先查一张已知映射表 _CAMEL_TO_SNAKE_MAP(context.py:94),查不到才退回正则 ([a-z0-9])([A-Z])。因为像 refSqlref_sql 这种缩写,纯正则会切错(得到 ref_sql 还好,但 SQL 类全大写缩写普遍是正则的坑)。用"白名单表 + 正则兜底",是处理不规则命名转换的稳妥做法。


6. 边界与局限(诚实)

  • 不做深层 cube 校验。 validate_project 对 cube 只做结构 / 引用 / 层级名检查;度量环、hierarchy 深校验是 Rust 侧 AnalyzedWrenMDL::analyze 的活(注释 context.py:1170-1172)。YAML 层只做"快速反馈",真正的语义分析在引擎。
  • 写配置会丢 YAML 注释。 save_project_config 明说不做注释 round-trip(context.py:428-430),因为那要引 ruamel.yaml。注释只在 init 模板里有,一旦被 CLI 重写就没了。
  • 只支持升级,不支持降级。 plan_upgrade 明确挡掉 target < current(context.py:1322-1325)。
  • legacy 判定是"存在即命中"。 哪怕 instructions.md 是空文件,load_rules 也会报 used_legacy=True(context.py:729-731)——想去掉警告必须删文件,清空不够。
  • 视图 dry-plan 校验要真引擎。 validate_manifesterror 级的视图 SQL 校验需要起 WrenEngine(context.py:1626-1628),纯结构校验才是零依赖的。

7. 横向对比(同组其它章)

本章讲的是"上下文如何被定义 / 校验 / 版本化"。它是下面几章的上游:

关切在哪讲与本章的关系
Agent 驱动的 CLI 与 skills01wren context * 命令怎么被 agent 调用
模型名 SQL → 受治理物理 SQL03消费本章编译出的 target/mdl.json
记忆与检索04索引本章的 schema + 描述;§3.4 的描述完整性直接决定其召回质量
GenBI 仪表盘与部署05依赖已建好的语义模型

一句话:本章把"知识"建好、校好、编译好,后面几章才有可信的地基去改写 SQL、去检索、去出图。


8. 代码地图(导航索引)

全部符号在 core/wren/src/wren/context.py,除最后一行外。用符号名 grep 比行号抗漂移。

主题文件路径符号名
项目发现(优先级链)core/wren/src/wren/context.pydiscover_project_path
读 / 写项目配置core/wren/src/wren/context.pyload_project_config save_project_config PROJECT_FILE
全局配置core/wren/src/wren/context.pyload_global_config
模型加载(双布局分派)core/wren/src/wren/context.pyload_models _load_models_v1 _load_models_v2
视图 / cube / 关系加载core/wren/src/wren/context.pyload_views load_cubes load_relationships
业务规则(非 schema 知识)core/wren/src/wren/context.pyload_rules load_knowledge_rules load_instructions load_knowledge_config
知识轴版本core/wren/src/wren/context.pyget_knowledge_schema_version _KNOWLEDGE_SUBDIRS
结构校验core/wren/src/wren/context.pyvalidate_project ValidationError
语义 / 描述校验core/wren/src/wren/context.pyvalidate_manifest _check_descriptions _prop_description
编译流水线core/wren/src/wren/context.pybuild_manifest build_json save_target
JSON→拆分 YAMLcore/wren/src/wren/context.pyconvert_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.pyget_schema_version require_schema_version _SUPPORTED_SCHEMA_VERSIONS _LAYOUT_VERSION_MAP
版本迁移(plan/apply)core/wren/src/wren/context.pyplan_upgrade apply_upgrade _plan_v1_to_v2 _plan_v4_to_v5 UpgradeResult
知识骨架core/wren/src/wren/context.pycreate_knowledge_skeleton _knowledge_skeleton_targets
agent 脚手架模板core/wren/src/wren/context.py_AGENTS_MD_TEMPLATE
连接字段登记core/wren/src/wren/model/field_registry.pyDATASOURCE_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:92field_registry.py:181)微调 label / 输入类型。这样新增一个数据源只改 Pydantic 模型,三处 UI 自动跟上。