| *English | 中文* |
| 字段 | 取值 |
|---|---|
| 扩展自 | GEML 1.0(见 GEML-spec_CN.md) |
| 版本 | 1.0 |
| 状态 | 稳定(stable) |
| 文件后缀 | .gemlhistory |
本配套规范为 GEML 定义一个自包含的版本化层。文档的 .geml 文件只保存当前版本,
且仅保存当前版本。一个与之同基名、后缀为 .gemlhistory 的伴生文件,以自当前版本
向回的逆向增量记录历史,并辅以全量关键帧快照。历史文件是自包含的:它始终携带
一份由工具维护、镜像已提交当前版的关键帧,因此任意历史修订都可还原、主文件也可回滚到
任意历史修订——全程不依赖活动的 .geml、不依赖任何外部版本控制系统、也不依赖任何在线
服务。两个文件刻意把热路径(频繁读写的当前版)与冷路径(仅在需要时加载的历史)
分开。历史文件由工具生成与校验;消费方(包括 AI 智能体)以纯文本读取它。
关键词 MUST(必须)、MUST NOT(必须不)、MAY(可)、SHOULD(应)
的要求等级沿用核心规范的定义。修订(revision) 指文档的一个被记录的状态,以提交
id(§8)标识;当前版本(current) 指最新修订。活动文件指工作副本
doc.geml;已提交当前版指历史文件中以当前修订记录的内容。
本扩展不向 GEML 增加任何新语法。版本化完全骑在既有的类型块原语(核心规范 §3)、
属性对象(§4)、稳定 id 与引用(§5)之上。不实现本扩展的处理器不受影响:.geml
文件本身仍是一份完整、合法的 GEML 文档,任何普通 GEML 工具都能渲染。
历史层是可选的,其存在仅由一个同基名的 .gemlhistory 文件来标示。
对于文档 doc.geml:
doc.geml —— 当前版本的权威副本,即热路径。它是那些对本扩展一无所知的普通
GEML 工具所读取、编辑、渲染的文件,是当前版本的真源。无论历史增长多长,它始终
小而干净。doc.gemlhistory —— 自包含的历史。它把已提交当前版记录为一份由工具维护的
关键帧(doc.geml 的镜像,每次 commit 时刷新),并为更早的修订记录逆向增量与
周期性关键帧。所有权明确无歧义:活动的 doc.geml 是当前版本的真源;doc.gemlhistory 中的「已提交
当前版关键帧」是工具维护的镜像,绝不手改,二者由哈希检查(§8)对账。
由此带来两个推论:
doc.gemlhistory 丢失或损坏,当前文档依然完整保存在
doc.geml 中,受影响的只是可恢复的历史。.gemlhistory 文档.gemlhistory 文件本身就是一份 GEML 文档。本历史扩展注册四种块类型:
| 类型 | 正文模式 | 角色 |
|---|---|---|
meta |
data | 历史文件头(每行一个 key=val) |
revision |
raw | 每个修订一条:元数据写在属性里,逆向补丁操作写在正文里 |
blob |
raw | 一段原样载荷(某修订下某块的内容),由补丁操作按 id 引用 |
keyframe |
raw | 某修订完整 .geml 内容的原样全量快照 |
当前修订的关键帧始终存在(已提交当前版镜像);其余关键帧周期性出现(见
keyframe-interval)。
由于 keyframe 与 blob 的正文原样内嵌整段 GEML 片段,其开围栏必须比载荷内部最长的
围栏更长(核心规范 §3):内部用 === 的载荷以 ==== 包住,依此类推。
=== meta)| 键 | 含义 |
|---|---|
history-of |
活动文件的基名,如 "doc.geml" |
geml-version |
历史所遵循的 GEML 语言版本 |
current |
当前修订的 id(§8) |
keyframe-interval |
关键帧快照之间推荐的修订间隔数 |
# History of budget.geml
=== meta
history-of = "budget.geml"
geml-version = "1.0"
current = "20260617T103012Z-33ab12cd"
keyframe-interval = 10
===
# 已提交当前版镜像(始终存在):
==== keyframe {id="20260617T103012Z-33ab12cd" hash="sha256:33ab12cd…"}
# 预算方案
=== meta
title = "预算方案"
===
=== table {#budget caption="年成本"}
| 方案 | 人月 | 单价 |
|-------|-----:|-----:|
| 基础版 | 1 | 25 |
===
=== note {#risks}
主要风险是供应商锁定。
===
====
=== revision {id="20260617T103012Z-33ab12cd" parent="20260501T140000Z-22cd34de" author="alice" summary="新增风险 note;修订预算单价" hash="sha256:33ab12cd…"}
delete #risks
replace #budget <- blob:b-22cd34de-budget
===
==== blob {#b-22cd34de-budget lang=geml}
=== table {#budget caption="年成本"}
| 方案 | 人月 | 单价 |
|-------|-----:|-----:|
| 基础版 | 1 | 30 |
===
====
=== revision {id="20260501T140000Z-22cd34de" parent="20260410T091500Z-11ef56ab" author="alice" summary="移除遗留费率说明" hash="sha256:22cd34de…"}
insert <- blob:b-11ef56ab-legacy after #budget
===
==== blob {#b-11ef56ab-legacy lang=geml}
=== note {#legacy}
旧费率口径,保留备查。
===
====
=== revision {id="20260410T091500Z-11ef56ab" author="alice" summary="初稿" hash="sha256:11ef56ab…"}
===
根修订是一条无逆向补丁正文、且无 parent 的 revision:它没有前驱。若某
revision 的载荷会迫使围栏深层嵌套,则该载荷必须以独立的顶层 blob 携带,按
blob:<id> 引用。(允许用更长围栏内联载荷,但不推荐——正确的围栏长度嵌套容易出错。)
块排列(建议)。 物理顺序不影响正确性——处理器按 type 与修订 id 建索引取块,
与位置无关。但出于可读性与流式效率,.gemlhistory 文件应(SHOULD)采用最新在前
的布局:meta,然后是已提交当前版 keyframe,再是从当前回溯到根的 revision 块,
且每个 blob 紧跟在引用它的 revision 之后。这使文件自上而下的顺序与逆向补丁的回放
方向(§6)一致,把最常读的条目(当前版镜像与最近的若干变更)放在最前,并让只需近期
修订的读者尽早停止。meta 可(MAY)携带一份区间关键帧 id 的索引,以便无需扫描即可
定位到更老的入口点。
逆向补丁通过块身份(区别于修订 id,§8)寻址块:
#id,该 id 即其身份。.gemlhistory 文件,绝不回写活动的 .geml。块 id 在语言层仍是可选的;本扩展不强制任何块带 id。两条性质使强制 id 没有必要:
工具应(SHOULD)在「易被引用或易被修改、且需要跨版本稳定身份」的块上记录显式
#id(表格、图形、note、重要小节)。注意: 标题的自动 id 是其文本的函数,因此重命名
标题会改变其 id,差分会把该变更视为「删除 + 新增」而非「重命名」。要跨重命名稳定追踪,
需显式 id。
revision 正文是一串面向行的操作,把某修订的内容变换为其 parent 的内容。
块键(block-key) 对带 id 块为 #<id>,对 id-less 块为工具派生的键令牌。
锚点(anchor) 为 at-start、at-end、after <块键>、before <块键> 之一。
| 操作 | 撤销(较新修订中的) | 效果(朝向 parent) |
|---|---|---|
delete <块键> |
一个被新增的块 | 移除该块 |
replace <块键> <- blob:<id> |
一个被修改的块 | 把该块内容置为 parent 的载荷 |
insert <- blob:<id> <锚点> |
一个被删除的块 | 在锚点处以 parent 修订内容重新插入该块 |
move <块键> <锚点> |
一个被移动的块 | 重新定位该块 |
一条 revision 内的操作按书写顺序套用。每个 blob:<id> 引用必须解析到同一
.gemlhistory 文件内的 blob 块;无法解析的引用是构建错误,与核心规范的引用
校验规则(§5)一致。
把文档还原(查看)到目标修订 R(只读;不修改任何文件):
keyframe 块提供更早的入口点。因此还原不依赖活动的 doc.geml,
即便活动文件存在未提交改动或缺失也照常工作。parent 链逐个修订向回套用逆向补丁,直到抵达 R。hash(§8)。关键帧把任意目标修订的逆向步数封顶,并把单条补丁损坏的影响限制在一个关键帧区段内。 每对相邻修订的逆向补丁都被保留,以保证任一步都可用;关键帧是额外的,充当有界、可校验 的入口点。
回滚把活动文件重写为某历史修订并从那里继续。历史是线性的:无分支,无合并。
由于历史文件自包含(§2),目标修订的还原不依赖活动文件的当前状态。
未提交改动策略。 回滚会覆盖活动文件。若 doc.geml 存在未提交改动(§8),处理器
必须不(MUST NOT)隐式丢弃它们:除非调用方显式同意丢弃,否则必须拒绝回滚——
交互式下为确认提示,非交互(脚本或 Agent)下为显式 force 选项。处理器应(SHOULD)
提示改用 commit 来保留当前改动。
回滚到修订 R:
doc.geml。current:丢弃链上比 R 更新的所有 revision 与
keyframe,以及仅被这些被丢弃修订引用的 blob 块;把已提交当前版关键帧刷新为
R;置 meta.current 为 R 的 id。回滚是破坏式的:R 之后的修订(含原当前修订)被永久丢弃且不可恢复。之后的编辑 作为带全新 id(新时间戳)的新修订提交;id 绝不复用,因此回滚后的修订永远不会与被 丢弃的 tip 混淆。(工具可(MAY)在截断前把被丢弃的 tip 另存一份快照作为可选保险; 这不是必须的。)
hash 为该版本完整 .geml 内容的精确 UTF-8 字节的 SHA-256,以
十六进制书写并以 sha256: 为前缀。每条 revision 记录其所代表版本的 hash;已提交
当前版关键帧记录当前修订的 hash。revision 以 newline(lf | crlf)记录其版本做
哈希时的换行风格,校验按该编码重现该版本的字节。<时间戳>-<短码>:<时间戳> 为提交时刻的 UTC 基本 ISO-8601
(YYYYMMDDTHHMMSSZ),<短码> 为该版本内容 hash 的前 8 位十六进制(即 sha256:
前缀之后的 8 位)。因此 id 可按时间排序、实践中唯一。还原校验对照的始终是完整
hash,绝非 8 位短码。parent 是前一修订的 id;根修订无 parent。工具接受任意
无歧义的 id 前缀(如短码或时间戳)作为选择符。区分两种严重度不同的情况:
parent 链断裂(某修订的 parent 不是其前一修订的 id,或链未抵达
根)、blob: 引用悬空、或还原结果哈希与记录不符——这些表明历史本身已损坏,必须
报为错误。hash(doc.geml) 与 current 记录的 hash 不一致,只表示
活动文件自上次提交后被编辑过。这是正常的编辑状态,不是损坏:必须报为警告,
且必须不阻塞只读操作(view、verify)或任意修订的还原。注(非规范): 由于短码取自版本的内容哈希、而非父绑定的提交哈希,id 链不具备
密码学意义上的防篡改性。对文档历史而言这是可接受的;若实现需要防篡改,可保持同样的
时间戳-<短码> id 格式、改为从 sha256(parent ‖ content ‖ metadata) 派生 <短码>
即可,其余不变。
合规历史处理器必须:
doc.geml 视为当前版本的唯一真源,渲染它时从不要求 doc.gemlhistory。doc.gemlhistory 作为合规 GEML 文档解析(核心规范 §3–§5)。parent 链断裂、blob: 引用悬空、或还原结果哈希不符。hash(doc.geml) 与 current 的不一致报为未提交改动警告,且绝不因此阻塞
只读操作。历史文件由工具生成与校验,而非手工编写。记录一次修订(commit)会刷新已提交当前版
关键帧、写入逆向补丁与相关 blob、并记录新的哈希与 id。逆向补丁需要精确的块内容抽取、
围栏长度记账与哈希——这些手工产出极易出错(对 AI 智能体亦然)。推荐的分工:
doc.geml。doc.gemlhistory(它是纯文本、按块寻址、每条修订都带
人类可读的 summary),以理解文档如何演进、为何演进——无需 git、无需任何在线服务。commit)、还原修订(show/view)、校验完整性(verify)或回滚
(restore)。由于历史以同基名的纯文本伴生文件随文档一同流转,这些信息可离线获取、在复制与转发后 依然完整、且自我描述——这些性质是带外的版本控制与在线文档历史所不具备的。