The .geml file holds only the current version; a same-named .gemlhistory sidecar holds the history: reverse patches walking back from the current version, plus full snapshots. The sidecar is itself a GEML document, admitting three block types through profile = "geml-history/v1"; it carries a mirror of the committed current version, so reconstructing any old version depends on neither the live file, nor git, nor the network. This page puts each rule of the profile next to the four geml history verbs, run for real.
Settled fixed by the profile document (it calls itself Stable). Observed not prescribed, this is what the tool does today. No implementation gap found on this page.
| Rule | Source | Status | |
|---|---|---|---|
| 1 | An application-layer profile; the core is untouched, and a processor that does not know it is fully conforming. Presence is signalled by the sidecar file, vocabulary by the sidecar's own meta; never inferred from the .gemlhistory extension or the history-of key (§8.6 rule 2). | §1 · §1.1 | Settled |
| 2 | The declaration admits three types: history-revision (keys id parent author summary hash newline), history-keyframe (id hash), history-blob (lang). Admission is names only: bodies stay raw, the same model as without the declaration. | §1.1 | Settled |
| 3 | doc.geml is the single source of truth for the current version, the hot path; doc.gemlhistory is the self-contained cold path, always carrying a keyframe mirror of the committed current version, maintained by tools and not hand-edited; the two reconcile by hash. | §2 | Settled |
| 4 | Lose the sidecar and the current document is intact; lose the live file and any version is still rebuildable from the sidecar. Neither direction depends on the other. | §2 | Settled |
| 5 | Header meta keys: history-of, geml-version, current, keyframe-interval. Keyframe and blob bodies embed whole GEML verbatim, so their fences must be longer than the longest fence in the payload. | §3 · §3.1 | Settled |
| 6 | Block identity: an #id when present; otherwise a key derived from content hash plus structural position, algorithm implementation-defined, bookkeeping only in the sidecar and never written back into the live file. No block is forced to carry an id. | §4 | Settled |
| 7 | Reverse patches have four operations: delete, replace … <- blob:, insert <- blob: <anchor>, move; applied in written order; a blob: reference must resolve to a history-blob in the same file, else error. | §5 | Settled |
| 8 | Reconstruction: take the nearest keyframe at or newer than the target, apply reverse patches along the parent chain down to the target, and the result's hash must equal the recorded hash. Independent of the live file. | §6 | Settled |
| 9 | Rollback is linear and destructive: every version after the target is truncated and current becomes the target; with uncommitted changes in the live file it must refuse unless the caller consents explicitly (interactive confirmation or --force). | §7 | Settled |
| 10 | hash is SHA-256 over the whole .geml's UTF-8 bytes, prefixed sha256:, newline style included (newline=lf|crlf). A revision id is <UTC timestamp>-<first 8 of hash>, sorting by time; any unambiguous prefix works as a selector. | §8 | Settled |
| 11 | Two severities: a broken chain, a dangling blob, or a reconstruction hash mismatch is corruption → error; the live file's hash differing from current is merely uncommitted → warning, and must not block read-only operations. | §8 · §9 | Settled |
| 12 | Every tool-generated sidecar must write profile = "geml-history/v1" in its meta; agents may read the sidecar but should not hand-write patches, blobs, ids or hashes; use the tool's save / get / restore / verify. | §9 · §10 | Settled |
| 13 | CLI: save appends a revision (no-op when identical to the tip); get without arguments lists all, with a rev prints that version in full; restore <rev> [--force]; verify reconstructs and rehashes the whole chain. rev spellings: 0 tip, -N back N, or an unambiguous id prefix. | geml history --help | Observed |
| 14 | geml revert <file> #id [--rev sel] is a core verb that uses the sidecar to return one block to a past version: splice back, resurrect, or delete. The project's PostToolUse hook auto-saves .geml files under spec/. | geml --help | Observed |
Editors, renderers and agents touch only the .geml; history loads only when asked for.
keyframe-interval revisions, bounding both the number of reverse steps to any target and the blast radius of one bad patch to a single segment.The file after two saves, unedited. It passes geml check itself.
# 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 === ==== %% four dashes around three: the payload contains === === history-revision {id="20260904T080743Z-40a503a6" parent="20260904T080742Z-5a3b3ac6" summary="edit intro" hash="sha256:40a5…" newline="lf"} replace #intro <- blob:b1 %% current → previous: swap #intro back to its old content === ==== history-blob {#b1 lang=geml} === text {#intro} First version of the intro. === ==== === history-revision {id="20260904T080742Z-5a3b3ac6" summary="first" hash="sha256:5a3b…" newline="lf"} === %% the root: no parent, no patch
$ 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 declared: the three types raise no unknown-block-type
| Block | Body | Holds |
|---|---|---|
| meta | key-value | history-of, geml-version, current, keyframe-interval |
| history-keyframe | raw | the complete .geml of one version; the current one is always present |
| history-revision | raw | attributes are metadata, the body is the operations that turn this version into its parent |
| history-blob | raw | one block's text at some version, referenced by blob:<id> |
20260904T080743Z-40a503a6 is a UTC timestamp plus the first 8 characters of the content hash, sorting by time naturally; verification uses the full hash, not those 8. The second save came 5 seconds after the first, and the timestamps differ by one second.===, so they open with ====. That is exactly the core's §3 fence discipline; the sidecar invents nothing.history- prefix because §8.5 wants extension names hyphenated; bare revision / keyframe / blob would sit on names the specification reserves.All run for real in a temp directory; with uncommitted changes verify only warns and never blocks, which is §8's two-severities rule.
$ 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 live file changed, not saved warning: uncommitted changes: hash(doc.geml) differs from current verify: OK (1 revisions reconstructed & hashed) exit 0 — a read-only operation is not blocked $ geml history save doc.geml -m "edit intro" saved 20260904T080743Z-40a503a6 $ geml history get doc.geml the first column is the selector 0 20260904T080743Z-40a503a6 - edit intro -1 20260904T080742Z-5a3b3ac6 - first $ geml history verify doc.geml verify: OK (2 revisions reconstructed & hashed)
| Verb | Does | Source |
|---|---|---|
| save [-m] | refreshes the mirror keyframe, writes the reverse patch and blobs, records the new hash and id; no-op when identical to the tip | §10 |
| get [<rev>] | no argument: all versions, newest first; with a rev: that version's full text (reconstructed, hash checked) | §6 |
| verify | reconstructs every version along the chain and rehashes; broken chain, dangling blob, hash mismatch → error; live file merely uncommitted → warning | §8 · §9 |
| restore <rev> [--force] | overwrites the live file and truncates history; refuses with uncommitted changes unless --force discards them | §7 |
| revert #id [--rev] | core verb: returns one block to some version, everything else untouched | geml --help |
--force.Run in the short-path temp directory %TEMP%\geml-hist-probe, because the session scratchpad's path exceeds the Win32 working-directory limit and node would not start there. Current in-repo build, 1.9.2.
| Step | Result | Maps to |
|---|---|---|
| save → set → verify → save → get → verify | all exit 0, one warning while uncommitted | verbs, board 11, 13 |
| geml check doc.gemlhistory | 0 diagnostics | sidecar, board 2, 12 |
| geml list doc.gemlhistory | meta, keyframe, two revisions, one blob | sidecar |
| revert #intro --rev -2 (only 2 revisions) | error offset out of range, live file untouched | out of range leaves the file alone |