GEML 图解 · profile · geml-codemap/v1(2026-07-03 定稿) · English

GEML 图解 · geml-codemap

codemap 把一个代码库的调用图写成一组 GEML 文档:每个容器一份,方法是空体 code 块,边是 CSV 表,入口写在 meta 里。它是应用层 profile:核心规范一字未动,只靠声明 profile = "geml-codemap/v1" 放行 code 上的三个属性键。这一页用 playground/codemap/ 里真实生成的文档做例子,把文件布局、meta 键、方法块、边表、校验分工和 agent 的消费方式逐条对到 profile 文档上。

看板

13 条规则,各自的出处和状态

规范已定 profile 文档写死的。实测 文档不规定、工具今天这样做。实现偏差本页未发现。

规范已定 12实测 1
规则出处状态
1应用层 profile,不是标准的一部分。声明放行 code 块上的 anchor、name、entry-via 三个键(profiles.ts)。必需:没有声明的老图每个 code 块都 warning,重建即修。§2 · §8.6规范已定
2布局:.geml-code-graph/ 下 index.geml(仓库元数据 + 模块汇总表)、每容器一份 <container>.geml、_index/name-lookup.json、_index/cross-stack.json、_build/(中间产物,agent 不读)。foldings.geml 调构建期命名,style.geml 调显示:首次构建播种,之后不覆盖。§1规范已定
3容器文档名 = 显示路径 sanitize(/→--,其它非 [A-Za-z0-9_.-]→-),冲突加 -2。build 默认跳过 .gitignore 的文件,--exclude 再排除,被排除符号的边一起消失。§1规范已定
4每文档恰一个 meta:profile(全部)、module(容器:显示路径,剥掉构建根和公共前缀)、src(真实相对路径)、entry(被容器外调用的方法,空格分隔,verify 检查)、resolution-default cpg / heuristic、index 上 repo commit container、可选 graph-depth。§2规范已定
5方法块:code {#id src=path#Lstart-end anchor="…"},空体。src= 是普通属性,agent 顺着它打开源码;anchor= 是引擎级稳定身份;name= 只在 id 被 sanitize 改动时写。§3规范已定
6id = 方法短名 sanitize;同文档同名冲突时每个成员追加 -<sha256(anchor) 前 6 位>,仍冲突再升到 8、10…。改名 = 新 id = 引用悬空 = verify error,这是特性不是缺陷。§3规范已定
7符号级类:.leaf(零出边含未解析、且被调用)、.accessor(bean 式 get/set/is 叶子,渲染器默认隐藏)、.test、.flow-entry。entry 从不在块上,只在 meta。§3规范已定
8边表(空表不发出):#calls(from,to,kind,confidence)、#called-by(from,to,kind,site)、#unresolved(from,to,hidden)、#api-calls / #api-served-by(跨栈 http 边)、#ref-by 保留。引用写法 #id 或 doc.geml#id;纯文本格不含逗号和换行,方括号换成圆括号,因为格按行内解析。§4规范已定
9跨栈链接:前后端按 METHOD + 归一化路径匹配成 http 边;启发式,所以放独立表、永不混进已验证的 #calls;endpoint 是边标签不是节点;探测器按语言可插拔;_index/cross-stack.json 记审计。§4.1规范已定
10分工:geml check 管结构、id 唯一、原生引用;CSV 格和 meta 值对标准不透明,有意如此。codemap verify 逐格解析 #calls / #called-by 的 from/to 和 meta entry,悬空 = exit 1;跨栈表宽松:#id 必须解析,file:line 端允许在图外。§5规范已定
11渲染:生成的文档是纯数据,不含 diagram 块;认出 codemap 文档的渲染器给分层方法流视图(根 = meta entry,深度 = graph-depth);嵌进别的文档用 diagram {format=geml-code-graph src=…},只有 src 一个属性。§6 · GEP-0003规范已定
12版本与信任:build --history 把变更文档提交进各自 .gemlhistory;revert doc '#method' --rev -1 回滚单个方法。resolution-default 说边从哪来,confidence 列和 candidate 行是解析器拒绝替你猜的地方,#unresolved 是盲区不是「没有」。§7 · §8规范已定
13CLI:geml codemap build | verify | render | serve | refresh | find,dir 缺省 ./.geml-code-graph,codegraph / code-graph 是别名。playground 里 35 份文档 verify 全过;find 需要 _index/name-lookup.json,playground 没带。geml codemap --help实测
布局

一个 codemap 目录里有什么

左边是 profile §1 的布局,右边是 playground 里真实 index.geml 的开头。

目录与入口文档 规范已定
.geml-code-graph/
index.geml 入口:仓库元数据 + 模块汇总表 <container>.geml 每容器一份(module | dir | file 粒度) _index/ name-lookup.json name → {anchor, doc, id},agent 第一跳 cross-stack.json 跨栈审计:endpoint、动词分歧、无人调用的路由 foldings.geml 调构建期模块命名 —— 你编辑,构建不覆盖 style.geml 调显示(geml-style/v1 样式表)—— 同上 _build/ 索引器原始输出、symbols/edges.jsonl;可再生、可 gitignore
geml codemap
$ geml codemap verify playground/codemap
verify: 35/35 documents pass geml check; profile references: all resolve
$ geml codemap find playground/codemap renderChart
no name-lookup at …\_index\name-lookup.json — build the codemap first
playground/codemap/index.geml · 真实文件开头
=== meta
profile = "geml-codemap/v1"
repo = geml
commit = fa40d89
container = file
entry = geml-viewer--content.js.geml#main
resolution-default = cpg
===

# Code map — geml

=== table {#modules format=csv}
module,                        doc,                                 methods, entries, tests
geml-parser/geml.ts,           geml-parser--geml.ts.geml,           72,      23,      0
geml-parser/cli.ts,            geml-parser--cli.ts.geml,            63,      0,       0
geml-parser/render.ts,         geml-parser--render.ts.geml,         50,      6,       0
…                              %% 27 行,每行一个容器文档
===
命名
§1:容器文档名是显示路径 sanitize 后的结果,geml-parser/chart.ts → geml-parser--chart.ts.geml。显示路径的算法在 §2 module:剥掉构建源码根(src/main/java、裸 src),再剥模块内最长公共段前缀;测试代码折进顶层 test/。
两个调优面
foldings.geml 和 style.geml 是你编辑的文件,不是构建产物:首次构建播种,之后永不重写。style.geml 是 geml-style/v1 样式表,缺了或读不了就回到渲染器内置默认,和有它之前的行为一样。
文档

一份容器文档:meta、方法块、边表

geml-parser--chart.ts.geml,构建器生成、未改一字。两个方法,一张出边表,一张入边表。

方法是空体 code 块,边是 CSV 表 规范已定
GEML · 真实文件
=== meta
profile = "geml-codemap/v1"
module = geml-parser/chart.ts             %% 显示路径
src = geml-parser/src/chart.ts            %% 真实路径,用来定位源码
entry = #buildChart                        %% 被容器外调用的方法;verify 检查
resolution-default = cpg
===

# geml-parser/chart.ts

=== code {#str .leaf src=geml-parser/src/chart.ts#L51-53
          anchor="scip-typescript npm @geml/geml 1.9.1 src/`chart.ts`/str()."}
===                                        %% 空体:源码在 src= 那一跳

=== code {#buildChart src=geml-parser/src/chart.ts#L55-140
          anchor="scip-typescript npm @geml/geml 1.9.1 src/`chart.ts`/buildChart()."}
===

=== table {#calls format=csv}
from,        to,   kind, confidence
#buildChart, #str, call,                  %% 空 confidence = high
…
===

=== table {#called-by format=csv}
from,                                    to,          kind, site
#buildChart,                             #str,        call, geml-parser/src/chart.ts:61
geml-parser--geml.ts.geml#resolveCharts, #buildChart, call, geml-parser/src/geml.ts:1797
===                                        %% 跨文档引用:另一份容器文档里的方法

=== table {#unresolved format=csv hidden}   %% 盲区表,hidden
每一部分是什么
部件规则出处
meta恰一个;entry 是唯一写在 meta 而不在块上的事实§2 · §3
code {#id …}空体;id 是短名 sanitize;src= 普通路由属性;anchor= 引擎身份,被放行的键§3
.leaf零出边且被调用;渲染器调暗。.accessor 默认隐藏,.test 可过滤§3
#calls出边;kind 为 call 或 candidate(虚分派候选,紧跟主 call 行);空 confidence = high§4
#called-by入边,由生成器跨全图汇总;site 是 file:line 纯文本§4
#unresolved盲区,hidden;to 原样纯文本、不检查§4
doc.geml#id跨文档引用同 §5.2;verify 逐格解析§4 · §5
分工
§5:geml check 只看结构、id、原生引用;CSV 格里的 #buildChart 对标准来说是文本,标准不长 codemap 形状的洞。codemap verify 才逐格解析 from/to 和 meta entry,悬空就 exit 1,红了说明图陈旧或部分更新,先重建再信导航。
纯文本格
§4:site 和 unresolved 的 to 不含逗号和换行(生成器换成空格),方括号换圆括号,因为表格单元格按行内解析,f[i](&x) 会被读成链接。
anchor
这里的 anchor 是 scip 的符号串(含反引号、点号),装在双引号里正好,属性值不解析行内。
消费

agent 怎么用它,以及该信多少

§8 的 cheat-sheet,加上跨栈链接和信任语义。

四条命令走完一次导航 规范已定
shell · §8
$ node -e "console.log(JSON.stringify(require('./.geml-code-graph/_index/name-lookup.json')['buildChart']))"
{"anchor":"…","doc":"geml-parser--chart.ts.geml","id":"buildChart"}   名字 → 文档 + id
$ geml get .geml-code-graph/geml-parser--chart.ts.geml '#buildChart'   方法块;src= 一跳到源码
$ geml get .geml-code-graph/geml-parser--chart.ts.geml '#calls'        出边;顺着 doc.geml#id 往下走
$ geml get .geml-code-graph/geml-parser--chart.ts.geml '#called-by'    谁调我,带 site
$ head -8 .geml-code-graph/geml-parser--chart.ts.geml                 meta:入口面一眼看完
信任语义 · §8
看到意思
resolution-default = cpg边来自编译器级精确解析;heuristic 是语法级
confidence 空high。有值时是解析器不替你猜的地方
kind = candidate虚分派 / 多实现候选,紧跟主 call 行,继承其 confidence
#unresolved 有行盲区,不是「没有调用」
heuristic 下 #called-by 为空不等于没人调
#api-calls · #api-served-by跨栈 http 边,匹配出来的,不与 #calls 混;method-mismatch 标记动词分歧
跨栈
§4.1:前后端两棵不相交的调用树,靠 HTTP 字符串隔着网络「调用」;profile 按 METHOD + 归一化路径把它们接成 http 边。启发式所以单独成表,每行带匹配置信度;endpoint 是边上的标签而不是节点;框架知识只在按语言的探测器里。_index/cross-stack.json 列 endpoint、动词分歧(合同漂移信号)、无人调用的路由、匹配不到路由的前端调用。
版本
§7:build --history 让每份变更文档进自己的 .gemlhistory,geml history get 看图怎么演化,geml revert doc '#method' --rev -1 只回滚一个方法。见第 8 页。
渲染
§6:codemap 文档自己不含 diagram 块,是纯数据;把图嵌到任何文档用 diagram {format=geml-code-graph src=index.geml},只有 src。见第 4 页。
依据

实测

用仓库内当前构建,1.9.2。示例文件来自 playground/codemap/,由 geml codemap build 生成。

命令 / 文件结果对应
geml codemap verify playground/codemap35/35 pass profile references: all resolve看板 10、13
geml codemap find playground/codemap renderChartno name-lookup:playground 未带 _index/name-lookup.json看板 13
index.geml · geml-parser--chart.ts.gemlmeta 键、空体 code、#calls / #called-by / #unresolved 与 profile 一致布局、文档
geml check profiles.ts 注册geml-codemap/v1 放行 code 上 anchor / name / entry-via看板 1