样式表是一份普通的 .geml:声明 profile = "geml-style/v1",装三种全空体的块,用 CSS 味的选择器选进内容文档而不改它,把块绑到宿主注册的组件名上。没有脚本,歧义是构建错误,不靠源顺序。它的一致性面是 geml style check --json 吐出的视图模型。这一页用一份干净的样式表和一份故意写坏的样式表,把 13 条诊断逐条跑出来。
规范已定 profile 文档写死的。实测 文档不规定、工具今天这样做。实现偏差本页未发现。注意 §0.1:只有 codemap 显示旋钮用到的子集承诺稳定。
| 规则 | 出处 | 状态 | |
|---|---|---|---|
| 1 | 样式表是普通 .geml,三种块全空体:信息都在属性对象里,因为未注册类型的正文是 raw、核心不解析,属性对象却对所有类型都解析。规则选进文档而不是模板包住文档,因为内容常是机器生成不可改的。无脚本:component / handler 只是名字,宿主给实现。 | §0 · §2 | 规范已定 |
| 2 | profile 是空格分隔列表,多个 profile 取词汇并集;校验只问「名字允许吗」,不问含义。没声明时每条 style-rule 一个 unknown-block-type warning(实测 5 条),这正是训练人忽略 warning 的东西。 | §1 | 规范已定 |
| 3 | style-rule:match= 必填;component= handler= show= filter= screen=(空格分隔)是 profile 自己消费的全部键;其余键原样透传给组件,所以 rule 上没有未知属性检查。 | §2.1 | 规范已定 |
| 4 | style-state:match=(生产者选择器)和 on= 必填,on 是封闭词汇,目前只有 select;type= block-ref | scalar 不校验;value-from= 带方向;init-value=。未知键 warning。多个生产者写一个 state 是随时间赋值,不是冲突。 | §2.2 | 规范已定 |
| 5 | style-screen:slots= 必填,逗号分隔、有序,每个是选择器或 $state;layout= 不校验(宿主的事);没有 route=,路由归宿主框架;未知键 warning。 | §2.3 | 规范已定 |
| 6 | 选择器 = §4 自己的词汇:type、.class、#id、[key]、[key=value],加唯一的后代组合子(空格)和逗号分支。拒绝的 CSS 被点名而不是静默不匹配:> + ~、伪类、*、子串匹配 → selector-unsupported error。扫描分区:伪类只在括号外找,子串运算符只在括号内找,因为属性值里合法地有 :。 | §3 | 规范已定 |
| 7 | 冲突按属性合并;两条规则给同一块设同一属性时,条件集严格超集者胜,否则 ambiguous-rule error。没有 specificity 算术、没有 !important、没有源顺序回退:靠顺序的样式表会被 geml set / add --before 悄悄改掉。只对语料里真实共现的块判。唯一的例外见规则 16:跨层按层号裁决,而层不是源顺序。 | §4 · §4.1 | 规范已定 |
| 8 | 绑定管线 interaction → state → view,单向三段,state 从不读 state,没有图所以没有环,目录里没有 binding-cycle。三个消费算子:show="$s"、filter="k=$s"、title="$s.caption";由运行时执行而不是组件;检查时只验每个 $name 有 state 声明。 | §5 | 规范已定 |
| 9 | 分隔符只有一条规则:名字列表用空格,选择器列表用逗号,因为空格是后代组合子。 | §6 | 规范已定 |
| 10 | 封闭词汇(运行时自己解释,如 on=)未知成员 → error;开放注册表(宿主注册,如 component= handler=)未知成员 → warning + inert;且 unknown-component / unknown-handler 只在调用者用 --components= / --handlers= 声明注册表时才检查。 | §7 | 规范已定 |
| 11 | 诊断目录 13 条,属于 profile 自己不进 Appendix A:7 个 error(selector-unsupported、ambiguous-rule、unknown-state、unknown-screen、unknown-value-source、unknown-interaction、style-missing-attribute)、6 个 warning(unmatched-rule、unmatched-producer、unknown-component、unknown-handler、style-unknown-attribute、style-embed-not-expanded)。 | §8 | 规范已定 |
| 12 | geml style check <sheet> <corpus…> [--json] [--components=] [--handlers=];exit 0 干净或只有 warning,1 有 error,2 用法错。--json 是视图模型,一致性面:states、screens、bindings、diagnostics;binding 带 doc(id 只在文档内唯一);bindings 按 screen;slots 已解析成块列表,不给原始选择器。 | §9 · §10 | 规范已定 |
| 13 | 稳定范围:承诺稳定的只有 profile 声明、style-rule、match=、属性透传,加上样式入口的路径与 default-style——codemap 的显示旋钮用到的这些「逃出去了」:每次 build 往用户仓库播种 _index/style.geml 和指向它的 _index/index.geml,而渲染器只经那个入口找样式表。#sitemap 那一列表存在但真实 map 里还没人用,所以仍可动。style-state、style-screen、show / filter / handler / screen、on / value-from / init-value / type / layout 都是「已规定、已检查、未使用」,随第一个真实用例移动。v1 有意没有:脚本、URL、路由、设计令牌之外的主题、正文。 | §0.1 · §12 | 规范已定 |
| 14 | 实测:干净样式表 0 诊断;声明 --components=edge-list 后 method-card 报 unknown-component warning;写坏的样式表 7 error 3 warning,与目录逐条对上。 | geml style check | 实测 |
| 15 | 样式入口:任何根只有 <root>/_index/index.geml 一个固定路径,宿主只探它;名字用来找,meta.profile 用来认,路径上放了别的东西就当「没有入口」。两个键说明加载哪几份:default-style(meta 键)和 #sitemap(表,document/template 两列,精确匹配、无 glob、无级联)。两者都是一次隐式 embed,所以环检测、深度上限、诊断全部沿用 embed 那一套;embed {src=index.geml} 因此等于「给我本根的默认,不管它叫什么」。 | §1.1 | 规范已定 |
| 16 | 层:入口把样式表排成 default-style(0)→ #sitemap 命中的那份(1)→ 入口自己的规则(2)。跨层按层号裁决、上层胜;层内规则 7 一字不改。层号是声明出来的顺序,不是从选择器算的分数——CSS @layer 的模型,不是 specificity。显式 embed 不开新层。排除源序的理由依然成立:同层内无序,#sitemap 精确匹配所以改行序不改结果,层数由那两个键固定。代价明写:「裁决与规则来自哪个文件无关」从此只对层内成立。 | §4.1 | 规范已定 |
| 17 | 实测:经入口的两层 0 诊断,#hero 被两条规则同时命中而 component 取上层的值、#facts 仍由默认层管;同两条规则挤进一层立刻 ambiguous-rule error;default-style 指不到时 style-embed-not-expanded warning、exit 0、本文件规则照常装载。 | geml style check | 实测 |
| 18 | 第二个用例(2026-09-10,外壳归位):选择器可以以行内部件收尾(link image code-span strong emphasis,只能是最后一步、前面要有块步);内含词里有块上的 axis,以及看源码的 view / editable;when= 认五个内建条件——@hover @focus @invalid @disabled @checked;合并后没有 component= 接的参数报 style-unknown-attribute。判据仍是 §12.3「换个块还是不是这个意思」。 | 设计 2026-09-10 §4 | 规范已定 |
| 19 | 实测:GitHub blob 复刻页的外壳全部改成 text 块里的列表 + 行内链接后,geml style check 0 error 0 warning;viewer 组件 7 → 2(tree、segments),宿主 CSS 页面段色值 22 → 0,样式表私有键 10 → 0,页面 CSS 的 59 处色值全部来自样式表。 | geml style check · render | 实测 |
内容文档是仓库测试夹具里的小 codemap:两个方法、一张 #calls 表。
--json 规范已定=== meta profile = "geml-style/v1" === === style-state {#sel type=block-ref match="table#calls" on=select value-from=to} === === style-rule {#edges match="table#calls" component=edge-list selectable} === %% selectable 不是 profile 的键:原样透传给组件 === style-rule {#methods match="code[anchor]" component=method-card} === === style-rule {#leaves match="code.leaf[anchor]" collapsed badge="leaf"} === %% 条件集是 #methods 的严格超集:叶子上两条都生效,不冲突 === style-screen {#overview layout=split slots="table#calls, $sel"} === %% 逗号分隔;$sel 槽渲染 state 指向的块
=== meta profile = "geml-codemap/v1" module = "geml-parser/core" === === code {#renderHtml anchor="ts:render-html.ts#renderHtml(Document,RenderOptions)"} === === code {#esc .leaf anchor="ts:render.ts#esc(string)"} === === table {#calls format=csv} from,to,kind,confidence renderHtml,esc,call,high ===
{
"states": [ { "id": "sel", "type": "block-ref", "on": "select", "valueFrom": "to" } ],
"screens": [ {
"id": "overview", "layout": "split",
"slots": [
{ "kind": "blocks", "selector": "table#calls",
"blocks": [ { "doc": "content.geml", "block": "#calls" } ] }, ← 已解析
{ "kind": "state", "state": "sel" }
] } ],
"bindings": [
{ "doc": "content.geml", "block": "#renderHtml", "rules": ["methods"],
"params": { "component": "method-card" } },
{ "doc": "content.geml", "block": "#esc", "rules": ["methods", "leaves"],
"params": { "component": "method-card", "collapsed": true, "badge": "leaf" } },
{ "doc": "content.geml", "block": "#calls", "rules": ["edges"],
"params": { "selectable": true, "component": "edge-list" } }
],
"diagnostics": []
}
$ geml style check good.style.geml content.geml --components=edge-list
warning: unknown-component: component `method-card` is not registered — renders inert (#methods)
#esc 被 #methods(code[anchor])和 #leaves(code.leaf[anchor])同时选中,但两条设的属性不同、且后者条件是前者的严格超集,所以 params 是两者之和。同属性、条件不可比才是 ambiguous-rule。doc,因为 §4 只保证 id 在文档内唯一,一份样式表管一个目录时两份文档都可以有 #budget。slots 已经解析成块列表,宿主不必在运行时重做选择器匹配。--components= 时 unknown-component 根本不检查,因为永远不会响的诊断比没有更糟;给了 edge-list 之后 method-card 就报出来,inert 回退。七个 error 三个 warning,对着 §8 的目录逐条数。
=== style-rule {#a match="code[anchor]" component=card-a} === === style-rule {#b match="code.leaf" component=card-b} === %% 同设 component,条件集不可比:#esc 上撞车 === style-rule {#c match="table > code" component=x} === %% 子组合子:块模型只有包含没有相邻 === style-rule {#d match="table#nowhere" component=y show="$ghost"} === %% 没人声明的 state;选择器也匹配不到任何块 === style-state {#s match="table#calls" on=hover value-from=nope} === %% on 封闭词汇只有 select;nope 不是 #calls 的列 === style-screen {#scr slots="table#calls" route="/x"} === %% 没有 route=:路由归宿主 === style-rule {#e match="code" screen="missing-screen"} ===
error: selector-unsupported: `>` is not supported (supported: type, .class, #id, [attr], [attr=val], descendant) (#c) error: unknown-interaction: `on=hover` is not an interaction this profile defines (known: select) (#s) warning: style-unknown-attribute: unknown attribute `route` for `style-screen` (#scr) error: unknown-screen: rule `#e`: `screen=missing-screen` names no `style-screen` block (#e) error: ambiguous-rule: `#a` and `#b` both set `component` on `content.geml#esc` — neither is more specific; write a rule matching the union of both selectors (#b) error: ambiguous-rule: … in screen `#scr` … (#b) warning: unmatched-rule: rule `#d` matched no block in the corpus (#d) warning: unmatched-rule: rule `#e` matched no block in the corpus (#e) error: unknown-state: `$ghost` is not declared by any `style-state` block (#d) error: unknown-value-source: state `#s`: `value-from=nope` is not a column of `content.geml#calls` (has: from, to, kind, confidence) (#s) 7 error(s), 3 warning(s) exit 1
#scr 屏幕里,因为 bindings 按 screen 各算一份。> 若被静默忽略,作者会以为它生效了。单份样式表可以直接交给工具。一整个目录要说明「我这堆文档怎么渲染」,就需要一个约定位置:<root>/_index/index.geml。它用两个键排出层——§4.1,profile 里唯一按来源裁决的地方。
default-style 命中与否都加载;#sitemap 命中的那份叠在上面 规范已定=== meta profile = "geml-style/v1" default-style = "base.geml" === %% 层 0:命中与否都加载 === table {#sitemap} | document | template | |---|---| | page.geml | hero.geml | === %% 层 1:精确匹配,没 glob 没级联
=== style-rule {#d-notes match="note" component=section} === === style-rule {#d-tables match="table" component=card-grid} === %% 按类型定的默认层
=== style-rule {#hero match="#hero" component=hero} === %% 按具体块定的覆盖层
=== note {#hero} GEML is an Agent-Native base document format. === === table {#facts} | k | v | |---|---| | spec | v1 | ===
{
"bindings": [
{
"doc": "page.geml",
"block": "#hero",
"rules": [
"d-notes",
"hero"
],
"params": {
"component": "hero"
}
},
{
"doc": "page.geml",
"block": "#facts",
"rules": [
"d-tables"
],
"params": {
"component": "card-grid"
}
}
],
"diagnostics": []
}
入口就是那份模板 —— 两个键都是一次隐式 embed,所以这条命令不必先人肉查表再传另一个文件。宿主也因此一行解析逻辑都不用写。
#facts 只被层 0 管到,照样渲染成 card-grid。「命中就换一份」的实现会让它一条绑定都没有。rules 里#hero 被 d-notes(层 0,match="note")和 hero(层 1,match="#hero")同时命中,两个 id 都记在绑定里;决定 component 值的是层号。绑定保留全部命中规则,是为了让人看得出这个属性为什么是这个值。#hero 赢不是因为 id 选择器「更值钱」——CSS 的那套算术 §4 明确拒绝过。它赢是因为 #sitemap 把它放进了更上面那一层。这是 CSS @layer 的模型:层号来自入口那两个键,选择器一个字都不参与。#sitemap 精确匹配所以改行序不改结果,层数由那两个键固定。agent 的按块编辑(geml set、geml add --before)因此仍然不会静默改变渲染——那正是 §4 排除源序要守的东西。ambiguous-rule 实测=== style-rule {#d-notes match="note" component=section} === === style-rule {#hero match="#hero" component=hero} === %% {type=note} 与 {id=hero} 互不包含
error: ambiguous-rule: `#d-notes` and `#hero` both set `component` on `page.geml#hero` — neither is more specific; write a rule matching the union of both selectors (#hero) 1 error(s), 0 warning(s) exit 1
这就是层存在的理由,不是它的边角情形:「默认层按类型 + 覆盖层按具体块」是这个 profile 最常见的写法,而这两种选择器的条件集永远互不包含。没有层的时候,playground 那五份首页文档全部报错。
match="note#hero"),或者把它挪到更上面那一层。default-style 指不到,说出来而不是静默少一层 规范已定=== meta profile = "geml-style/v1" default-style = "gone.geml" === === style-rule {#only match="table" component=card-grid} === %% 本文件的规则照常装载
warning: style-embed-not-expanded: `embed` of `gone.geml` contributed no rules: cannot resolve `gone.geml` (#default-style) 0 error(s), 1 warning(s) exit 0
诊断的 id 是 #default-style —— 那是合成的 embed 块的 id。隐式 embed 和写出来的 embed 在下游无从区分,所以报错路径也是同一条。
style-unknown-attribute 同一性质:我们忽略了作者写下的东西,该说出来。消息带原因——读不到、锚点不存在、成环、或者调用方没给文档解析钩子。§11 的例子。它只用了稳定子集:一条 style-rule、match=、透传。
=== meta profile = "geml-style/v1" title = "codemap graph style" === === style-rule {#graph match="diagram[format=geml-code-graph]" \ fold=1 depth=6 hide-accessors=true \ palette="#e3f2fd #e8f5e9 …"} === %% palette 是名字列表:空格分隔(§6)
foldings.geml 调构建期的模块命名,style.geml 调显示。首次构建播种,之后永不重写。见第 9 页。filter= 从未对真实噪声跑过,handler= 没有真实宿主,on= 封闭词汇只有一个成员因为只接了一种交互。它们「已规定、已检查、未经实战」,所以 geml style check 在 --help 里标 EXPERIMENTAL:今天正确,明年拼写不保证。在短路径临时目录里跑,内容文档取自 geml-parser/test/fixtures/style/codemap-content.geml。用仓库内当前构建,1.10.1。
| 探针 | 结果 | 对应 |
|---|---|---|
| good.style.geml + content.geml | 0 诊断,--json 视图模型如上 | 干净、看板 12 |
| 同上 --components=edge-list | 1 warning unknown-component method-card | 看板 10 |
| bad.style.geml + content.geml | 7 error 3 warning | 写坏、看板 11 |
| _index/index.geml(两层)+ page.geml | 0 诊断,#hero→hero、#facts→card-grid | 入口 |
| flat.geml(同两条规则挤一层)+ page.geml | 1 error ambiguous-rule | 入口 |
| _index/broken.geml(default-style 指不到) | 1 warning style-embed-not-expanded | 入口 |
| 删掉 profile 声明后 geml check | 5 warning unknown-block-type | 看板 2 |