GEML 图解 · profile · geml-history/v1(Stable) · English

GEML 图解 · geml-history

.geml 文件只放当前版本;同名的 .gemlhistory 边车放历史:从当前版本往回的反向补丁,加上完整快照。边车自己就是一份 GEML 文档,靠 profile = "geml-history/v1" 放行三个块类型;它自带一份「已提交当前版」的镜像,所以重建任何旧版本都不依赖活文件、不依赖 git、不依赖网络。这一页把 profile 的每条规则和 geml history 四个动词的实测放在一起。

看板

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

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

规范已定 12实测 2
规则出处状态
1应用层 profile,核心一字未改;不认识它的处理器完全合规。存在由边车文件表明,词汇由边车自己的 meta 声明;不许从 .gemlhistory 扩展名或 history-of 键推断(§8.6 规则 2)。§1 · §1.1规范已定
2声明放行三个类型:history-revision(键 id parent author summary hash newline)、history-keyframe(id hash)、history-blob(lang)。放行只给名字,body 仍是 raw,与未声明时同一模型。§1.1规范已定
3doc.geml 是当前版本的唯一真相,热路径;doc.gemlhistory 是自包含的冷路径,永远带一份已提交当前版的 keyframe 镜像,工具维护、不手改,两者靠哈希对账。§2规范已定
4边车丢了,当前文档完好;活文件丢了,任何版本仍可从边车重建。两个方向都不互相依赖。§2规范已定
5头部 meta 键:history-of、geml-version、current、keyframe-interval。keyframe 和 blob 的正文原样嵌整段 GEML,所以围栏必须比载荷里最长的围栏更长。§3 · §3.1规范已定
6块身份:有 #id 用 id;没有就用内容哈希加结构位置派生的键,算法实现定义,簿记只在边车里、绝不写回活文件。不强制任何块带 id。§4规范已定
7反向补丁四种操作:delete、replace … <- blob:、insert <- blob: <anchor>、move;按写的顺序应用;blob: 引用必须解析到同文件里的 history-blob,否则 error。§5规范已定
8重建:取目标版本或更新的最近 keyframe,沿 parent 链反向打补丁到目标,结果哈希必须等于记录的 hash。不依赖活文件。§6规范已定
9回滚线性、破坏性:截掉目标之后的所有版本,current 改为目标;活文件有未提交改动时必须拒绝,除非调用者明确同意(交互确认或 --force)。§7规范已定
10hash 是整份 .geml UTF-8 字节的 SHA-256,前缀 sha256:,含换行风格(newline=lf|crlf)。版本 id 是 <UTC 时间戳>-<hash 前 8 位>,按时间排序;任何不含糊的前缀可作选择器。§8规范已定
11两种轻重:链断、blob 悬空、重建哈希不匹配是损坏 → error;活文件哈希与 current 不同只是未提交 → warning,不得阻止只读操作。§8 · §9规范已定
12工具生成的每个边车必须在 meta 写 profile = "geml-history/v1";agent 可以读边车,不该手写补丁、blob、id、hash,用工具的 save / get / restore / verify。§9 · §10规范已定
13CLI:save 追加一版(与 tip 相同则 no-op);get 无参列出全部、带 rev 打印那一版全文;restore <rev> [--force];verify 重建并重哈希整条链。rev 写法:0 tip、-N 往回 N、或不含糊的 id 前缀。geml history --help实测
14geml revert <file> #id [--rev sel] 是核心动词,用边车把一个块回到过去某版:拼回、复活或删除。项目的 PostToolUse hook 对 spec/ 下的 .geml 自动 save。geml --help实测
两个文件

热路径与冷路径

编辑器、渲染器、agent 只碰 .geml;历史只在需要时加载。

一次 save 做的事 规范已定
doc.geml当前版本 · 唯一真相
→ save →
keyframe已提交当前版的镜像,刷新
history-revisionid · parent · hash · 反向补丁
history-blob被替换块的旧内容
→
doc.gemlhistory自包含 · 工具维护
为什么反向
最新版是最常读的,所以镜像放最新版,补丁往回走:从 tip 重建上一版只要打一个补丁,不必从头重放。keyframe 每隔 keyframe-interval 个版本再放一个,把任何目标的反向步数和一条坏补丁的影响范围都限制在一段之内。
为什么自包含
§2:活文件有未提交改动、甚至不在了,边车里任何版本照样能重建;反过来边车丢了当前文档一点不受影响。两个文件谁都不欠谁。
边车

一份真实的 .gemlhistory

两次 save 之后的文件,一字不改。它本身通过 geml check。

头部、镜像 keyframe、一条反向补丁、一个 blob、根版本 规范已定
doc.gemlhistory · 工具生成
# History of doc.geml

=== meta
profile           = "geml-history/v1"
history-of        = "doc.geml"
geml-version      = "1.0"
current           = "20260904T080743Z-40a503a6"
keyframe-interval = 10
===

# Committed-current mirror (always present):
==== history-keyframe {id="20260904T080743Z-40a503a6" hash="sha256:40a5…b4f4"}
=== meta
title = "history probe"
===
# Plan {#plan}
=== text {#intro}
Second version of the intro.
===
=== table {#t format=csv}
a,b
1,2
===
====                                  %% 四杠包三杠:载荷里有 ===

=== history-revision {id="20260904T080743Z-40a503a6" parent="20260904T080742Z-5a3b3ac6"
                     summary="edit intro" hash="sha256:40a5…" newline="lf"}
replace #intro <- blob:b1                %% 从当前版回到上一版:把 #intro 换回旧内容
===

==== history-blob {#b1 lang=geml}
=== text {#intro}
First version of the intro.
===
====

=== history-revision {id="20260904T080742Z-5a3b3ac6" summary="first" hash="sha256:5a3b…" newline="lf"}
===                                    %% 根版本:没有 parent,没有补丁
geml list · geml check
$ geml list doc.gemlhistory
#history-of-docgeml                       heading           h1    L1-10
=== meta                                  meta              anon  L3-9
#committed-current-mirror-always-present  heading           h1    L11-39
=== history-keyframe                      history-keyframe  anon  L12-25
=== history-revision@4214327e             history-revision  anon  L27-29
#b1                                       history-blob            L31-35
=== history-revision@e36c84ca             history-revision  anon  L37-38

$ geml check doc.gemlhistory
ok: no diagnostics     ← profile 声明了,三个类型不报 unknown-block-type
块体装什么
meta键值history-of、geml-version、current、keyframe-interval
history-keyframeraw某一版的完整 .geml,当前版的那份永远在
history-revisionraw属性是元数据,正文是把这一版变成 parent 的操作
history-blobraw一个块在某版的原文,被 blob:<id> 引用
id
§8:20260904T080743Z-40a503a6 是 UTC 时间戳加内容哈希前 8 位,天然按时间排序;校验用的是完整 hash,不是这 8 位。第一次 save 后 5 秒的第二次 save,时间戳差 1 秒。
围栏
§3:keyframe 和 blob 原样嵌 GEML,载荷里有 ===,所以它们开 ====。这正是核心 §3 的围栏纪律,边车没有发明新东西。
放行
§1.1:三个类型名带 history- 前缀,因为 §8.5 要求扩展名带连字符;裸的 revision / keyframe / blob 会占用规范保留的名字。
动词

save · get · verify · restore,加核心的 revert

全部在临时目录里实跑;未提交改动时 verify 只 warning 不阻止,是 §8 那条「两种轻重」。

一轮完整操作 规范已定 CLI 实测
shell
$ geml history save doc.geml -m "first"
saved 20260904T080742Z-5a3b3ac6

$ printf 'Second version of the intro.\n' | geml set doc.geml '#intro' --in - --body
wrote doc.geml

$ geml history verify doc.geml          活文件已改、未 save
warning: uncommitted changes: hash(doc.geml) differs from current
verify: OK (1 revisions reconstructed & hashed)     exit 0 —— 只读操作不被阻止

$ geml history save doc.geml -m "edit intro"
saved 20260904T080743Z-40a503a6

$ geml history get doc.geml             第一列就是选择器
0       20260904T080743Z-40a503a6  -  edit intro
-1      20260904T080742Z-5a3b3ac6  -  first

$ geml history verify doc.geml
verify: OK (2 revisions reconstructed & hashed)
规则对照
动词做什么出处
save [-m]刷新镜像 keyframe,写反向补丁和 blob,记新 hash 与 id;与 tip 相同则 no-op§10
get [<rev>]无参:全部版本,最新在上;带 rev:那一版的完整文本(重建后核对 hash)§6
verify沿链重建每一版并重哈希;断链、blob 悬空、哈希不符 → error;仅活文件未提交 → warning§8 · §9
restore <rev> [--force]覆盖活文件并截断历史;有未提交改动时拒绝,--force 才丢弃§7
revert #id [--rev]核心动词:只把一个块回到某版,其余不动geml --help
rev 写法:0 = tip · -N = 往回 N 版 · 不含糊的 id 前缀
两种轻重
§8:未提交改动是正常的编辑状态,不是损坏,所以是 warning,且不能阻止 get / verify / 重建。链断、blob 引用悬空、重建哈希不符才是 error。上面 verify 在改动未保存时照样 OK,就是这条。
回滚
§7:restore 是破坏性的线性截断,目标之后的版本永久丢弃;id 永不复用,所以回滚后新提交不会和被丢弃的 tip 混淆。有未提交改动必须拒绝,除非 --force。
agent
§10:agent 随便读边车(纯文本、按块、每版带 summary),但不要手写补丁、blob、id、hash。本仓库对 spec/ 下 .geml 的 save 由 PostToolUse hook 自动完成,配置文档除外。
依据

探针

在短路径临时目录 %TEMP%\geml-hist-probe 里跑,因为会话 scratchpad 的路径长度超过 Win32 工作目录上限,node 起不来。用仓库内当前构建,1.9.2。

步骤结果对应
save → set → verify → save → get → verify全部 exit 0,未提交时一条 warning动词、看板 11、13
geml check doc.gemlhistory0 诊断边车、看板 2、12
geml list doc.gemlhistorymeta、keyframe、两条 revision、一个 blob边车
revert #intro --rev -2(只有 2 版)error offset out of range,活文件不动越界不改文件