Independence comparison · two markup languages · measured 2026-10-06 · 中文 · all illustrated pages · GEML home

GEML and Carve: one built for typesetting, one for agents

Carve .crvGEML .geml

Both languages were born in 2026 and both set out to go one step past Markdown, in different directions: Carve optimizes for typesetting and presentation, for richer expression; GEML optimizes for agents, for stronger control over every element, and works more like a container: one set of addresses and standard actions holds and manipulates text forms inside the document and out. This page lines up their 82 syntactic constructs and every capability, marks where each design comes from, and runs the same inputs through both official parsers. The two share a spelling in only 13 places, all inherited from older public sources. Everything else is its own system, with different goals, trade-offs and capabilities.

1313 shared spellings, all from common sources

ATX headings, [t](url), ![](src), `code`, [^id], - [ ], {#id .class k=v}, %% and a few more, from Markdown (2004), PHP Markdown Extra, GFM, Pandoc, Obsidian/Mermaid and CommonMark.

29 · 11Each language's own constructs stay its own

Carve alone has 29: /italic/ *bold* _underline_ =highlight=, |= header cells, ^ captions, ::: containers, </#id>, CriticMarkup and more. GEML alone has 11: the typed-block primitive, data-form tables, view, data, inner-unit coordinates, labeled closes and more. The same line of text parses completely differently in each (probe P7).

Two directionsOne built for typesetting, one for agents

Carve succeeds Djot and spends its effort on inline expression, table layout, captions and numbering, and many output formats. GEML spends its effort on addresses, block-level reads and writes, guarded writes, block history and reference checking, so an agent can change any element of a document precisely and safely.

OppositeKey design decisions point opposite ways

Carve rejected {{name}} variables; GEML puts {{key}} in the core. Carve leaves an unresolvable reference to the host; GEML makes it a build error. Carve passes =html through by default; GEML has no HTML channel at all.

09-30When each project learned of the other

Carve's repository holds no record of GEML. GEML first noted Carve on 2026-09-30, in a survey of similar projects; by then every core part of GEML had been live for one to three months.

229Profiles and views were redesigned step by step

Profiles went from hard-wired in the core spec to an application-layer mechanism on 08-31, with 229 commits across six profiles; view was split out of a table's computing attributes into a block of its own. See section 5.

1 · Design direction

One built for typesetting, one for agents

Carve wants a document to look better; GEML wants an agent to change any element of a document precisely and safely. The two columns show where each puts most of its effort; the table shows the trade-offs that follow, each row taken from the project's own documents or first commit.

CarveBuilt for typesetting and presentation

Its effort goes into how well a document can look.

  • Inline expressivenessUnderline, highlight, super- and subscript, bold italic, semantic spans (kbd, abbr, time), language tags, mentions and tags.
  • Typographic detailSmart punctuation (dashes, ellipses, arrows, ©), no-break spaces, verse line blocks, continuation markers.
  • Table layoutMerged cells, horizontal and vertical alignment, continuation rows, header and footer row counts.
  • Captions and numberingCaptions for figures, tables, listings and equations, Figure # numbering, composite figure panels.
  • Editorial marksCriticMarkup insertions, deletions, substitutions and comments.
  • Many outputsHTML, Markdown, plain text and ANSI terminal output, each with a report of what it dropped.

GEMLBuilt for agents that manipulate elements

Its effort goes into how precisely and safely a program can change one element of a document.

  • Everything has an addressExplicit ids, derived heading ids, derived addresses for prose between blocks, content addresses @<hex>, inner-unit coordinates #t[2]["Q1"], line selectors L27-58.
  • Read and write by addressget / set / add / delete / rename / revert touch only the target block; changing one cell left 399 of 400 bytes untouched (P9).
  • Guarded writesEvery write rechecks the whole document and is refused if the result would not parse; #id@<hex> stops a write based on stale content; rename updates every reference.
  • Undo what brokeA .gemlhistory revision chain, single-block revert, a chain that can be rehashed and verified.
  • References are a contractEight kinds of reference are checked at build time, so an agent that edits one block learns at once whether it broke another (P1, P3–P6).
  • Data that derivestable, view, data and the charts bound to them: change one number and the totals and the chart follow.
  • An interface for agents11 read/write MCP tools and an agent skill; reading one block costs one block of context (measured 133 B of 400 B).

GEML works more like a container

One set of addresses and standard actions reaches the text forms inside a document and the files, data, codebases and other documents outside it.

Inside the document · forms it holds
  • === code any programming language, kept verbatim
  • === math TeX
  • === diagram external DSLs such as mermaid and vega-lite
  • === table pipe grid, CSV, TSV
  • === data JSON, JSONL (YAML, TOML, EDN reserved)
  • === meta typed key-value pairs
  • === note, === text flow prose
  • An unknown type: a warning, with the body kept verbatim
A GEML document

One fence, === type {attrs}; a type registry decides whether a body is read as raw, flow or key-value.

One set of standard actions
listfindgetsetadddeleterenamerevertcheckhistoryto
One set of addresses
#id## Heading=== type@<hex>L27-58#t[2]["Q1"]{lang=py}--within

The command line, MCP (each tool name is its command path, since 07-29) and every profile's verbs share this one set.

Outside the document · references that reach out
  • code src=f.py#L10-20 a span of a source file; an out-of-range span is an error
  • table src=data.csv a local or http(s) data file
  • data src=log.jsonl#L1-500 a span of a JSON, JSONL or YAML file
  • data= chart data from CSV, JSON, or a table in another document
  • embed src=doc.geml#id, [[doc.geml#id]] blocks in other documents; get --view reads straight through an embed
  • Profiles reach further: codemap writes a whole codebase as documents, media manages audio and video assets, translator makes a document in another language a projection

Backward compatible with Markdown: inline syntax follows Markdown; .md files can be listed, searched, read and written with no conversion, and what is written stays Markdown (10-02); when needed, geml notes.md converts one to GEML in a single command.

Carve's model is one markup document rendered to many outputs. Its include processor takes only Carve or plain text and is expanded by the host outside the parser, which does no file I/O; its command-line actions work on whole documents (render, fmt, lint, diff, merge, migrate, flatten), and block-level read and replace arrived in its MCP server on 10-05.

AspectCarveGEML
Problem it solvesA successor to Djot that removes Markdown's twenty years of ambiguity and is easy for writers to learn and remember (docs/technical-rationale.md)Let people and AI agents co-write one document: plain text for people, addressable, verifiable and versioned for machines (first draft §1, "addressable everything")
Stated ancestryDjot, Org-mode (/italic/), Creole (|=), AsciiDoc (admonitions), CriticMarkup (Init commit README)Markdown/CommonMark, AsciiDoc, reStructuredText, Org-mode, MyST, Mermaid/Graphviz, TOML (2026-06-17 draft preface and commit message); neither Djot nor Carve is among them. Attribute syntax credited to Pandoc as its ancestor (COMPARISON.md, 06-18); the website's "Influences" section (10-01) adds djot
Relation to MarkdownDeliberately changes meanings: *x* is bold, _x_ is underline; a .crv file must not go to a Markdown parserBackward compatible with Markdown: keeps its inline syntax, and the CLI reads and writes blocks in a .md file directly, with no conversion, writing Markdown back
Block modelA dedicated syntax per kind of content: ``` code, $$` math, ::: containers, |= tables, ^ captions, --- frontmatterOne typed-block primitive, === type {attrs}, carries code, math, tables, diagrams, data, notes and metadata; a type registry decides how the body is read
External contentThe include processor takes only Carve or plain text, expanded by the host outside the parser (includes.md)src= reaches source-file line spans, CSV, JSON/JSONL/YAML and blocks in other documents; local sources are checked at build time
Unit of actionThe whole document: render, fmt, lint, diff, merge, migrate, flattenThe element an address selects: a block, a section, prose, a line span, a cell; one set of verbs across the command line, MCP and profiles
Unresolvable referenceKept as written and left for the host to resolve (extension-contract.md §12.4)A build error; the document does not conform (spec §5.2, §8)
Variables / interpolationListed under dismissed syntax: it mixes markup with templating (dismissed-syntax.md){{key}} is core syntax, valued from === meta; a misspelled key is an error
Raw HTMLBare HTML is literal text; ```=html passthrough is on by default in the JS and Rust enginesNo HTML channel at all; documents are data, never code (spec §9.1)
Block identityAuthor-written {#id} plus session-scoped node ids, which have no spelling in the source and end with the session (ast-node-identity.md)An #id written in the source, plus prose addresses derived from neighbouring blocks; the same key serves reads, writes, checks and history
Metadata--- YAML frontmatter, kept raw=== meta key-value blocks with typed values (string, number, boolean); several merge; #meta is addressable
HistoryNo stored history; compares two files (carve diff) and merges three ways (carve merge)A .gemlhistory sidecar: an append-only revision chain, hash-verified, with single-block revert
2 · Syntax

Every construct, as each language spells it

Carve per its spec repository at HEAD bb09baea (2026-10-06): docs/cheatsheet.md and resources/grammar.ebnf. GEML per spec/GEML-spec.md, Appendix B and the sections it cites. Use the buttons to filter.

Shared heritagesame spelling in both, from an older public source
Same form, different meaning
Same need, different design
Carve only
GEML only
ConstructCarveGEMLVerdictNotes / common source
Inline
Italic/x/*x*Different designCarve takes it from Org-mode; GEML keeps Markdown
Bold*x***x**Same form, different meaning*x* is bold in Carve and italic in GEML (probe P7)
Bold italic/*x*/no dedicated formCarve onlyCarve's own
Underline_x_none; _x_ stays literalCarve onlyProbe P7
Strikethrough~x~~~x~~Different designGEML uses the GFM spelling; a single tilde is not syntax in GEML
Highlight=x=noneCarve only==x== in Init; single character since 06-11
Superscript / subscript{^x^} {,x,}noneCarve onlyBraced forced form from Djot
Intraword forced delimitersH{,2,}O etc.noneCarve onlyDjot style
Code span`x``x`Shared heritageMarkdown (2004)
Inline literal!`x`noneCarve onlyAdded to Carve 07-21
Inline math$`x`$x$Different designCarve moved to Djot's form on 05-21; GEML uses the LaTeX/Pandoc $…$
Raw inline output`x`{=html}none; the spec forbids itCarve onlyFrom Djot / Pandoc RawInline
Link[t](url)[t](url)Shared heritageMarkdown (2004)
Reference link[t][ref] + [ref]: urlnoneCarve onlyMarkdown has it; GEML left it out
In-document anchor link[t](#id)[t](#id)Shared heritageMarkdown spelling in both; only GEML checks that the target exists (probe P1)
Cross-reference with automatic text</#id>[[#id]]Different designThe concept is LaTeX \ref and AsciiDoc <<id>>; GEML's first draft cites AsciiDoc for it
Cross-document referencea plain link [t](a.crv#id), unchecked[t](a.geml#id) [[a.geml#id]], checkedDifferent designProbe P3: Carve accepts a missing target; GEML reports it
Wiki-style link[Heading text][]; optional extension [[Heading]][[#id]], naming an id rather than heading textDifferent design[[ ]] comes from MediaWiki / Obsidian / Logseq
Image / media![alt](src) image![alt](src) image, audio or video, inferred from the extensionShared heritageMarkdown; GEML widened it to in-place media (06-17)
Inline content projection{{ path #id }} include processor, outside the core![[#id]] core syntax, projecting a text blockDifferent design![[ ]] is Obsidian's embed spelling
Footnote reference[^l], defined by [^l]: …; inline ^[…][^id], pointing at any block with that idShared heritagePHP Markdown Extra (2005); the definitions differ
Metadata interpolationdismissed{{key}}, escaped as \{{GEML onlyCarve rejects it in dismissed-syntax.md; GEML added it 06-18, and an undefined key is an error (probe P5)
The {{ … }} pairfile include {{ path #id @lines:N-M }}metadata interpolation {{key}}Same form, different meaningCarve requires inner spaces, so {{path}} is plain text; GEML's {{key}} has the same shape as Mustache or Hugo templates
Inline span and attributes[t]{.c}, /x/{.c}only links and media take attributesCarve onlyDjot's span
Semantic spans[Tab]{kbd} {abbr=…} {time=…}noneCarve onlyCarve's own
Language tag{:fr} {:}noneCarve onlyBCP 47
Inline extension:name[x]{a}noneCarve onlyResembles remark-directive
Symbol:rocket:noneCarve onlyDjot symbol
Mention / tag@user #tagnoneCarve onlySocial-platform convention
Editorial marks{+ +} {- -} {~a~>b~} {#note#}noneCarve onlyCriticMarkup
Smart typography-- ... --> (c)no substitutionCarve onlyDjot / SmartyPants
Escape\ + ASCII punctuation\ + ASCII punctuationShared heritageCommonMark (2014)
Hard line break\ at line end\ at line endShared heritageCommonMark
No-break space\␠ mid-linenoneCarve onlyDjot
Blocks
Heading#…#######…######Shared heritageMarkdown ATX headings; neither has setext
Explicit heading idon the line above: {#id}⏎# Titleat the line end: # Title {#id}Different designCarve dropped the trailing form on 06-04; GEML settled on the line end on 06-17 (Pandoc / kramdown style)
Automatic heading idkeeps case: id="Intro"normative derivation: lower-case, NFD, strip diacritics, whitespace to -Different designProbe P1 renders <section id="Intro">; GEML spec §4 gives a six-step derivation
Prose addressesnone#prev-between-next, #container-before-nextGEML onlyProse between two blocks gets a unique address from both ends; inserting a block makes the old address stop resolving
Paragraph / bullet list- *- *Shared heritageMarkdown
Ordered list1. 1) a. i. and auto-numbered .1.Different designCarve's is richer
Task list[ ] [x] plus [-] [>] [?] [_][ ] [x]Shared heritageGitHub (2013); Carve's extra states come from Org TODO
Block quote>, ::: >, ^ attributionnoneCarve onlyGEML's syntax inventory has no block quote
Thematic break--- *** ___left out on purposeCarve onlyGEML's 06-17 fence rationale: - clashes with breaks, frontmatter, setext and lists, so the fence uses =
Code block```lang "Header" [Label]=== code {lang=… src=…}Different designIn GEML a backtick run is only a shield and never opens a code block
Raw block```=html, passed through by defaultnoneCarve onlyProbe P8: <script> comes out unchanged
Container / admonition::: note "Title" … :::=== note … ===Different design::: is the Pandoc / MyST / Djot fence; GEML's first draft used it for seven minutes and switched to === at 07:17
Fence closing and nestingexact colon count; inner fences add a colonexact equals count; outer fences are longerDifferent designEqual-length closing is the common fence rule (CommonMark code fences)
Unknown type::: word → <div class="word">=== word → warning, body keptDifferent designGEML's first draft (§3) already made it a forward-compatible warning
One typed-block primitivenone=== type {attrs} + a type registry (raw / flow / key-value)GEML onlyCode, math, tables, views, data, diagrams, embeds, notes, text and metadata share one fence
Line block (verse)::: |noneCarve onlyreST / Djot
Definition list:: term / : definitionnoneCarve only
Continuation markera lone + linenoneCarve onlyCarve's own
Display math$$`…`=== mathDifferent design
Comment line%%, plus %%% blocks, trailing %%, {% %}line-start %% onlyShared heritageObsidian %%…%% and Mermaid %% comment lines (GEML hosts Mermaid). GEML added it 06-18 to replace the forbidden <!-- -->
Hidden but checked blocknone{hidden}GEML onlyA source table that only feeds a chart: in the model, reference-checked, not rendered
Metadata--- YAML frontmatter (---toml ---json)=== meta key-value, typed, mergeable, addressable as #metaDifferent designGEML explicitly has no --- frontmatter
Caption^ text on the next line, auto-numbered Figure #caption="…" attributeDifferent designCarve's is stronger: numbering, listings, equation captions
Composite figure::: figure, panels "Figure 2a"noneCarve only
Attribute object contents{#id .class key=val}{#id .class key=val}Shared heritageLong established in PHP Markdown Extra, kramdown, Pandoc and MyST; in GEML's 06-17 draft EBNF
Where attributes attachthe line above a block, floating past invisible blocksin the fence line === t {…}, at a heading's line endDifferent designGEML lets a long attribute object continue over lines with \
Bare attribute wordword → word=""word → boolean true; values are string, number or booleanDifferent designAn open question in GEML's first draft: borrow TOML's typing discipline
Abbreviation definition*[HTML]: …noneCarve onlyPHP Markdown Extra
Include / embed{{ path #id @lines:N-M @shift:N }}=== embed {src=doc.geml#id}; code src=f#L10-20Different designCarve has more parameters; in GEML an out-of-range line span is an error (probe P4)
Placement directives::: toc ::: footnotes etc.noneCarve only
Tabs / citations::: tab, [@key] (optional)noneCarve only
Tables
Table shapea bare pipe table, |= marks header cellsa GFM pipe table inside === tableDifferent design|= comes from Creole; pipe tables from PHP Markdown Extra / GFM
Alignmentglued < ~ >, vertical ^ ~ v, inherit ?GFM delimiter row :--:Different designCarve's is stronger
Merged cells^ up, < leftnoneCarve only
Continuation rows / header rows+ rows, {header-rows=N}noneCarve only
Data-form bodynoneformat=csv|tsv header= delim=GEML only"Two table forms" in the 06-17 draft
External data sourcenonesrc=data.csv or http(s)GEML only
Derived viewnone=== view {src=#t compute=… summary=…}GEML onlyComputed columns, a summary row, printf display formats (probe P6)
Data blocknone=== data {format=json|jsonl schema=…}GEML onlyA record array can feed a chart directly
Graphics
Hosting an external diagram DSL```mermaid etc., optional feature (06-21)=== diagram {format=mermaid} (06-17 draft)Different designGitLab, GitHub and Obsidian had mermaid code blocks first; GEML's draft rule is "host a DSL, never invent one"
Charts```chart Chart.js JSON, numbers written into the bodyformat=geml-chart data=#table x= y=, checked by columnDifferent designOnly GEML's charts bind to data in the document (probe P6)
Code graphnoneformat=geml-code-graphGEML onlyGEP-0002 / 0003 (07-01)
Addressing
Labeled closing fencenone; colons are counted=== #idGEML onlyAdded 06-29: the closing line names the block it closes, so nothing has to be counted
Inner-unit coordinatesnone#fy[2] #fy[2]["Q1"] #fy["Q1"] #fy[summary]GEML onlyGEP-0011 (09-03): reference one row, cell or column of a table, across documents too, and have it checked
Address for content without an idAST path (JSON Pointer), session-scoped node idcontent address @<hex> (first 8 hex of SHA-256), derived prose addressesDifferent designGEML's @<hex> arrived 08-04 and finds a block by its content; Carve's paths move when the structure changes
Selecting blocks by type / attributecarve_select_ast_nodes (MCP, 09-09)'=== code {lang=py}' {.warn} --within (09-30)Different designGEML's commit message cites mq's query language
Characters and files
Input normalizationstrip one BOM, unify line endings, U+0000→U+FFFDthe same (§0, 07-25)Shared heritageCommonMark §2, "Insecure characters"; GEML added it after a bug where a BOM turned # Title into a paragraph
Extension / media type.crv, clipboard text/x-carve.geml, text/vnd.geml (IANA registration pending)Different design
3 · Same-input runs

One intent, each official parser run once

Carve: @markup-carve/carve@0.1.9 from npm (carve lint and render). GEML: geml 1.12.3 (geml check). Output is quoted verbatim.

P1 · A link to a block that does not exist

Both link forms point at a missing #budget.

Carve

See [the budget](#budget) and </#budget>.
$ carve lint p1.crv
p1.crv:3:31 broken-crossref — Cross-reference </#budget>
has no matching heading id; it renders as the literal
text "</#budget>".
Reports the cross-reference only; the plain link […](#budget) passes, and render exits 0

GEML

See [the budget](#budget) and [[#budget]].
$ geml check p1.geml
error: unresolved reference `#budget` (line 3)
error: unresolved reference `#budget` (line 3)
2 error(s)
Both reported, exit 1

P2 · Duplicate id

Two headings share an id. Both languages catch this one.

Carve

p2.crv:5:1 duplicate-heading-id — Duplicate heading id "a"
Reported by lint; render still emits two id="a"

GEML

error: duplicate id `#a` (first defined at line 1) (line 3)
Error, exit 1

P3 · A cross-document reference to a missing target

The other document exists, but has no #nope.

Carve

See [x](other.crv#nope).
$ carve lint p3.crv      → no output, exit 0
$ carve p3.crv
<p>See <a href="other.crv#nope">x</a>.</p>
Not detected

GEML

See [x](other.geml#nope).
error: unresolved reference `other.geml#nope` (line 3)
1 error(s)
Error

P4 · A line range of a code file that has since shrunk

code.txt has 10 lines; the document cites lines 50–60.

Carve

{{ code.txt @lines:50-60 }}
$ carve lint p4.crv      → no output, exit 0
$ carve p4.crv           → warning on stderr, exit 0
p4.crv:3:1 include-lines-out-of-range - Include line
range for "code.txt" starts past end of file.
<p>{{ code.txt <span class="mention"><strong>@lines
</strong></span>:50-60 }}</p>
A warning only; the output falls back to literal text, with @lines read as a mention

GEML

=== code {src=code.txt#L50-60}
===
error: code source `code.txt#L50-60`: the file has
10 line(s), so lines 50-60 no longer exist — the range
is stale (line 3)
Error, naming the stale range

P5 · Metadata interpolation with one misspelled key

product is right; prodct is a typo.

Carve

---
product: Acme
---
Welcome to {{product}}, also {{prodct}}.
<p>Welcome to {{product}}, also {{prodct}}.</p>
Not a feature (dismissed by design); both stay literal

GEML

=== meta
product = "Acme"
===
Welcome to {{product}}, also {{prodct}}.
error: unknown metadata reference `{{prodct}}` (line 5)
The misspelled key is caught

P6 · Charts and table data

The table has Q1 and Q2. One chart plots the first-half total; another names a Q3 column that does not exist.

Carve

|= Region |= Q1 |= Q2 |
| North | 10 | 12 |
| South |  7 |  9 |
^ Sales by region

``` chart
{"type":"bar","data":{"labels":["North","South"],
 "datasets":[{"label":"Q3","data":[99,99]}]}}
```
$ carve lint p6.crv      → no output, exit 0
The chart's numbers are typed into its JSON, unconnected to the table; a wrong column or number goes unnoticed

GEML

=== table {#sales}
| Region | Q1 | Q2 |
…
===
=== view {#sales-h1 src=#sales compute="H1 = Q1 + Q2"
  summary="Region = 'Total'; …; H1 = sum(H1)"}
===
=== diagram {format=geml-chart data=#sales-h1 x=Region y=H1}
===
=== diagram {format=geml-chart data=#sales x=Region y=Q3}
===
error: chart: column `Q3` not found in table (line 16)
#sales-h1, as computed:
Region | Q1 | Q2 | H1
North  | 10 | 12 | 22
South  |  7 |  9 | 16
Total  | 17 | 21 | 38
The chart binds to the table and its columns are checked; the view derives the computed column and the summary row

P7 · The same line of Markdown

One ordinary line of Markdown, given to both. This is Carve's most distinctive part.

Carve

Plain *emphasis*, **strong** and _under_ with ~~strike~~.
<p>Plain <strong>emphasis</strong>, **strong** and
<u>under</u> with ~~strike~~.</p>
*x* becomes bold, _x_ underline; ** and ~~ are not parsed

GEML

Plain *emphasis*, **strong** and _under_ with ~~strike~~.
<p>Plain <em>emphasis</em>, <strong>strong</strong> and
_under_ with <del>strike</del>.</p>
Same as Markdown

P8 · Raw HTML

Both escape inline HTML as text; they differ on a dedicated passthrough channel.

Carve

```=html
<script>alert(1)</script>
```
<script>alert(1)</script>
Passed through by default (untrusted input needs --safe)

GEML

(the language has no HTML passthrough construct)
Spec §9.1: documents are data, never code

P9 · Change one cell, record a revision, then revert just that block

GEML's core workflow. The Carve side lists its three nearest tools.

Carve

$ carve diff before.crv after.crv
changed  text at line 5 (value: "10" -> "15")
1 structural change
Compares two existing files (08-01) and merges three ways (08-11)
The CLI has no command to read or write one block by id.
carve-mcp's carve_get_block / carve_replace_source
(block read and byte-range replace) were committed 2026-10-05.
No stored document history of any kind.
No revision chain, no single-block revert

GEML

$ geml get doc.geml '#sales'          → that block only
$ geml history save doc.geml
saved 20261006T020617Z-d1701a38
$ … | geml set doc.geml '#sales' --body --in - -o doc.geml
wrote doc.geml — 1 B changed, 399 B of 400 B untouched
$ geml history save doc.geml
$ geml doc.geml --to json              → the view recomputes
North | 15 | 12 | 27
$ geml revert doc.geml '#sales'
reverted #sales to 20261006T020617Z-d1701a38
$ diff before.geml doc.geml            → byte-identical
Addressed reads and writes since 07-01; the history sidecar since 06-17
4 · Capabilities

Tooling and capabilities: who has what, since when

Dates are first appearances: a repository's server-side creation date, or the first commit carrying the feature.

CapabilityCarveGEMLVerdict
Build-time reference checkinglint checks </#id> and duplicate ids; unresolved references are left to the host by design; no cross-document checksRequired by the spec: links, auto-references, cross-document references, footnotes, chart data=, code src line ranges, {{key}} and schema= are all checked (06-17 draft §8)GEML stronger
Read one block by idinclude fragment #id (heading sections normative 09-14, any block 10-05); carve_get_block (10-05)geml get #id (07-01); a heading means its section; line selectors L27-58; prose addresses; reads .md directlyGEML earlier, broader
Write one block by idbyte-offset source patches (09-07); carve_replace_source (10-05)set / add / delete / rename / revert (from 07-01); a write whose result would not parse is refused; rename updates every reference; content address @<hex> (08-04); #id plus @<hex> as a write guard (09-30)GEML earlier, broader
Document historyNo stored history; carve diff (08-01), three-way carve merge (08-11), reversible AST patches.gemlhistory (06-17): append-only revision chain, SHA-256 checks, verify rehashes the chain, single-block revert; no three-way mergeEach has strengths
Data layer in the documentnoneData-form tables, external CSV, view computed columns and summaries, data JSON/JSONL, charts bound by columnGEML only
The word "profile"Content-restriction presets: full / article / comment / minimal, for untrusted input (06-16)Application-layer vocabularies: form, media, style, translator, codemap, history, each with its own spec and conformance testsSame name, different thing
Translation projectionnone (UI labels only)The Chinese spec is a geml-translator/v1 projection: 17 embeds point at English blocks and follow their changesGEML only
Code dependency graphnonecodemap: a call graph written as GEML documents whose edges are checked references (run on Flink: 95,958 nodes)GEML only
Agent skillcarve-skill (07-15)In-repo skill (06-18); geml skill installGEML earlier
MCP servercarve-mcp (09-05): parse, lint, migrate, AST patches; block read/replace on 10-05geml mcp (07-25): 11 read/write tools, every write validated first; 4 more tools for a code graphGEML earlier
ImplementationsTypeScript, PHP (forked from djot-php) and Rust, byte-identical (05-13); WASM (06-01); Go / Python / Ruby bindingsTypeScript (06-17); a second implementation in Rust + WASM (10-03)Carve stronger
Conformance suite2,223 .crv/.html pairs (first batch 05-13)Language-neutral suite (06-28); the 06-17 draft already says "after CommonMark / toml-test"Common practice
EditorsVS Code, tree-sitter, LSP, Zed (06-01); IntelliJ, Vim, Emacs, Helix, Sublime, Obsidian (08-21) and moreVS Code, Obsidian (06-30); a Chrome viewer; a Logseq bridgeCarve broader
Format bridgesImports Markdown, HTML, Djot, BBCode; outputs HTML, Markdown, plain text, ANSI, AST; loss reports; pandoc-carveMarkdown → GEML; outputs HTML, Markdown, JSON, canonical GEMLCarve broader
Framework integrationsWordPress, Laravel, Symfony, Hugo, Jekyll, Astro, MkDocs, Docusaurus and others across 60+ repositoriesThe website playgroundCarve broader
Security modelURL denylist, nesting cap, amplification budget, include sandbox; =html on by default§9: no HTML channel, resource limits, one-level confined cross-document resolution, URL scheme allowlistDifferent stance
Formatter / lintcarve fmt (06-26), about 45 lint rules--to geml canonical output (06-29), Appendix A stable diagnostic codesCommon practice
5 · Design evolution

GEML's own redesigns

GEML's structure was not fixed in one stroke. Each of the three redesigns below was driven by GEML's own problems: whether a second implementation could agree with the reference one, and whether a processor that does not know a new construct would show the wrong content.

Profiles: from hard-wired into the core to application-layer vocabularies

  1. .gemlhistory as a "companion specification" beside the coreIts three block types were declared nowhere, so seven sidecars reported 333 "unknown block type" warnings that nothing could remove.
  2. The code graph wired into the core renderergeml-code-graph sat on one if-chain beside the specification's own geml-chart and mermaid; codemap, media and style commands each had an else-if; and an unwritten registry in the reference implementation suppressed diagnostics.
  3. GEP-0008 first proposes form as a core block type
  4. §8.6, the profile mechanism (GEP-0009)A document declares vocabularies through the profile meta key; a vocabulary licenses names and must not change the document model; inference from a file extension is banned; history becomes the first profile. The conformance suite proves it with a vocabulary name nothing recognizes: a second implementation with no profile mechanism reproduces every case.
  5. form, translator and media become profilesform becomes the form-* family of geml-form/v1; translator on 09-07, media on 09-16.
  6. Profile governanceNaming, state, checking, conformance tests.
  7. The core no longer knows any vocabulary by nameRendering, commands and meta keys became table lookups, with runtime registration. 1,683 of render.ts's 2,894 lines belonged to the code graph; once moved out, the file is 1,125 lines and a page with no diagram is 46% smaller.

Carve's profiles have been something else since Init: djot-php's content-restriction presets (full / article / comment / minimal), for filtering untrusted input.

View: from table attributes to a block of its own

  1. The first draft puts compute= on a table"Two table forms plus portable computation".
  2. A summary row and printf display formats are added
  3. The GEP-0012 draft still puts where / order / limit / select on a table
  4. A separate view block instead (parser 1.10.0)The reason was weighed attribute by attribute: a processor that does not know where= or select= shows the rows and columns the author excluded, which is a false render rather than a degraded one; an ignored view has no body and shows nothing. compute=, summary= and a src= naming a block all moved to view; a table holds facts only and reports unknown-attribute if it carries them. The spec's own diagnostics went from 6 errors to 4.

Output: from a dedicated block to general embedding

  1. === output stores a code block's captured resultNever executed, only stored, optionally bound to its code block with of=#id.
  2. output withdrawn in favour of === embed {src=doc.geml#id}Any block can be embedded, and src= and data= become one rule.

Carve's tables do not compute, and its chart data lives in JSON; its include processor {{ path #id }} has had its current spelling since Init and became normative on 09-14.

ProfileWhat it doesFirst commitCommitsSpec and code lines today
geml-history/v1Block-level version sidecar: revisions, keyframes, blobs06-17402,680
geml-codemap/v1A code call graph written as checkable GEML documents07-011479,705
geml-style/v1Stylesheets that select into documents and never modify them08-30247,382
geml-form/v1Form fields addressable inside their block08-30141,592
geml-translator/v1A translation as a projection of its source, following its changes09-01172,035
geml-media/v1Audio and video assets, edits and generation lineage09-15366,685
The profile mechanism itself§8.6, registration and governance08-30271,062

Counted over each profile's spec, design-document and source paths: 229 distinct commits, about a quarter of all 940 non-merge commits. Profile work inside the core parser is not counted, so these are lower bounds.

6 · Strengths

What each does that the other does not, or does better

Only items backed by a run above or by spec text. Side by side, the two lists describe two different goals.

GEMLOriginal or stronger

  1. One fence for all structured content=== type {attrs} plus a type registry that reads a body as raw, flow or key-value; the first design goal of the 06-17 draft.
  2. References are a contractEight kinds of reference are checked at build time, and a broken one is an error. In probes P1, P3, P4, P5 and P6, Carve lets the case through.
  3. Every block has an address, and every address is writableExplicit ids, derived heading ids, derived addresses for prose between blocks; get / set / add / delete / rename / revert; each write is checked to parse. Carve's comparable tools arrived three months later and cover read and replace.
  4. Block-level historyA .gemlhistory hash chain that can be verified, with single-block revert (probe P9). Carve stores no document history.
  5. A data layer inside the documentData-form and externally sourced tables, derived views, computed columns and summary rows, data blocks, charts checked by column. Carve's charts embed fixed JSON.
  6. Metadata as typed, checked values=== meta and {{key}}, the direction Carve explicitly dismissed.
  7. Application-layer vocabularies and projectionsform, media, style, translator and codemap extend the core; the Chinese spec is itself a translation projection of the English one.
  8. Backward compatible with Markdowngeml list / find / get / set work on Markdown files directly and write Markdown back; one command converts a file to GEML when needed.

CarveOriginal or stronger

  1. Rich inline writingItalic, bold, underline, highlight, super- and subscript, CriticMarkup, smart typography, mentions and tags, semantic spans.
  2. Table layoutMerged cells, two-axis alignment, continuation rows, header and footer row counts.
  3. Captions and numbering^ captions for figures, tables, listings and equations, Figure # numbering, composite figure panels.
  4. Three byte-identical implementationsJS, PHP and Rust, plus Go, Python and Ruby bindings, against 2,223 test pairs.
  5. Ecosystem reachA dozen editors, dozens of framework and static-site integrations, PDF and LaTeX output.
  6. Format bridges with loss reportsImports HTML, Markdown, Djot and BBCode; reports what an output format dropped.
  7. Content restriction for untrusted inputProfile presets, link policies, a safe mode.
  8. Three-way mergecarve merge combines two independent AST edits.
7 · Timeline

When the key features appeared

Carve went public first (05-13); GEML's first commit is 06-17. Each line develops along its own route.

8 · Sources

Every finding can be checked

Where to check

  • GEML's first public commit geml-spec/geml@ace97761, 2026-06-17 07:10 UTC; its message says the draft was distilled from a survey of HTML, Markdown, TOML, AsciiDoc, Org, rST and MyST. The repository was created server-side at 06-17 12:35 UTC.
  • GEML's earlier draft xiongjy2104/must-skills, branch geml-spec-draft: df2fc31 (the UTM draft; its preface names the formats it combines), 619d33b (07:17, ::: to === with a fence rationale), 93296fd (07:34, renamed GEML).
  • Carve markup-carve/carve at HEAD bb09baea; first commit 341c1f98 "Init."; block read and replace in markup-carve/carve-mcp@35ac412 and @a7bca51 (both 2026-10-05).
  • Server-side times: npm records @geml/geml 1.0.0 as published 2026-06-29 11:01 UTC, and the package already contains .gemlhistory, geml-chart, compute, {{key}}, === meta, hidden, [[#id]] and cross-document broken-link errors. @markup-carve/carve was first published 07-14.
  • Mutual search: no branch history or file in Carve mentions geml, General Expressive or gemlhistory. GEML's current tree does not mention carve; historically it appears only in one 09-30 survey of similar projects (c78b28d0, moved to the website on 10-01), which calls Carve the closest project in features. Neither project's public issues or discussions mention the other.
  • Versions run: @markup-carve/carve@0.1.9 (npm), geml 1.12.3. The probe files and commands are as shown on this page.

Scope notes

  • Carve went public before GEML: its GitHub repository was created 05-13, and GEML's first commit is 06-17.
  • Carve's 03-30 Init date comes from the commit's own timestamp; GitHub's server-side creation date is 05-13. GEML's commit times are the same kind of evidence, corroborated server-side by the repository creation time and the npm publish time.
  • Carve's npm release (0.1.9) may trail its spec repository; "an include fragment may name any block", written into the spec on 10-05, does not yet work in that release.
  • Both now read and write single blocks: GEML since 07-01, keyed by author-written #id and content addresses; Carve since 10-05 in its MCP server, keyed by selectors, byte ranges and a whole-source sha256. Similar aims, different keys.

When each project learned of the other

  • No branch history or file in Carve records GEML.
  • GEML first recorded Carve in a 09-30 survey of similar projects that also covered Djot, VersaDok, Pandoc, MyST, Markdoc, mq and others. By then typed blocks, reference checks, block reads and writes, history, the data layer and MCP had been live for one to three months.
  • The survey listed two things Carve has and GEML lacks: ids on inline elements, and checking an expected hash before a write. Inline ids have not been added to GEML.
  • Later that day GEML combined #id with the content address @<hex> it had had since 08-04 into a write guard (4ddb3eed); the commit message cites mq's query language. A write with an expected value works like HTTP If-Match.