@geml/dsh-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +69 -0
- package/README.zh.md +60 -0
- package/cordis.patch.yml +27 -0
- package/package.json +19 -0
- package/skills/geml/SKILL.md +167 -0
- package/skills/geml/references/authoring.geml +365 -0
- package/skills/geml-code-graph/SKILL.md +222 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GEML contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTE: This MIT license covers the *code* in this repository (geml-parser/,
|
|
26
|
+
integrations/geml-viewer/, integrations/geml-check-action/, docs/examples/
|
|
27
|
+
tooling). The *specification* documents are licensed separately under
|
|
28
|
+
CC-BY-4.0 — see LICENSE-spec.md.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @geml/dsh-plugin — GEML for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
This plugin brings **Agent-Native** document handling to the harness. Multi-turn
|
|
6
|
+
work drowns in token bloat — whole files read in, whole files written back,
|
|
7
|
+
content growing verbose and drifting from the truth.
|
|
8
|
+
[GEML](https://github.com/geml-spec/geml) exposes a document as **addressable
|
|
9
|
+
blocks** an LLM can reason about and edit precisely: one section in, one section
|
|
10
|
+
back, at a fraction of the tokens, leaving the context window for the actual
|
|
11
|
+
work. A built-in **reference** mechanism keeps a **single source of truth**, so
|
|
12
|
+
facts stop fragmenting across copies and an agent maintains docs at zero
|
|
13
|
+
overhead.
|
|
14
|
+
|
|
15
|
+
The bundle ships three things:
|
|
16
|
+
|
|
17
|
+
- **The GEML MCP server** — one `@deepseek-ai/dsh-mcp-client` row running
|
|
18
|
+
`npx -y @geml/geml mcp --root .`, confined to the session's project
|
|
19
|
+
directory. The model sees `mcp__geml__geml_get`, `mcp__geml__geml_set`,
|
|
20
|
+
`mcp__geml__geml_check` and friends, so it edits one block at a time instead
|
|
21
|
+
of rewriting files.
|
|
22
|
+
- **The authoring skill** (`skills/geml/`) — golden rules, validation loop, and
|
|
23
|
+
a sectioned reference (`references/authoring.geml`) the agent pulls one topic
|
|
24
|
+
at a time.
|
|
25
|
+
- **The code-graph skill** (`skills/geml-code-graph/`) — build, view, update and
|
|
26
|
+
navigate a project's call graph: who calls X, what X calls, impact paths, with
|
|
27
|
+
the graph rendered in the browser.
|
|
28
|
+
|
|
29
|
+
The bundle carries no code of its own — both plugins it configures ship inside
|
|
30
|
+
the dsh installation, and the skills are Markdown. Nothing is built at install
|
|
31
|
+
time, so no `allowBuilds` approval is involved either way.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh plugin --profile <name> add @geml/dsh-plugin
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Verify the layer without booting, then boot:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
dsh --profile <name> --dump-config # shows a "# == @geml/dsh-plugin" layer
|
|
43
|
+
dsh --profile <name>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`dsh plugin --profile <name> remove @geml/dsh-plugin` removes both the
|
|
47
|
+
dependency and the layer.
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Both rows are ordinary configuration: override them by `id` in your profile's
|
|
52
|
+
`cordis.patch.yml`, restating every key the row needs. Point `mcp-geml` at a
|
|
53
|
+
pinned CLI version, for instance:
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
- id: mcp-geml
|
|
57
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
58
|
+
config:
|
|
59
|
+
serverName: geml
|
|
60
|
+
transport: stdio
|
|
61
|
+
command: npx
|
|
62
|
+
args: ['-y', '@geml/geml@1.8.1', 'mcp', '--root', '.']
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A global `geml` on PATH works as well — `command: geml`, dropping the `npx`
|
|
66
|
+
arguments.
|
|
67
|
+
|
|
68
|
+
The CLI and the same skills for Claude Code instead: `npx -y @geml/geml skill
|
|
69
|
+
install`, or the plugin under [`../claude-plugin`](../claude-plugin).
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# @geml/dsh-plugin — DeepSeek Harness 上的 GEML
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
本插件为 harness 提供 **Agent-Native** 的文档处理能力。多轮交互最容易被 Token
|
|
6
|
+
膨胀拖垮——整篇读进来、整篇写回去,内容越滚越臃肿,也越来越偏离事实。
|
|
7
|
+
[GEML](https://github.com/geml-spec/geml) 把文档呈现为**可寻址的块**,让 LLM
|
|
8
|
+
能精准理解与改写:取一个小节、写回一个小节,Token 只需零头,宝贵的上下文窗口
|
|
9
|
+
留给真正的工作。内建的**引用机制**维持**单一数据源(Single Source of
|
|
10
|
+
Truth)**,事实不再散落成互相漂移的副本,Agent 可以零负担地读写与维护。
|
|
11
|
+
|
|
12
|
+
这个 bundle 带三样东西:
|
|
13
|
+
|
|
14
|
+
- **GEML MCP server** —— 一行 `@deepseek-ai/dsh-mcp-client`,运行
|
|
15
|
+
`npx -y @geml/geml mcp --root .`,限定在会话自己的项目目录内。模型看到的是
|
|
16
|
+
`mcp__geml__geml_get`、`mcp__geml__geml_set`、`mcp__geml__geml_check` 等工具,
|
|
17
|
+
于是一次改一个块,而不是重写整个文件。
|
|
18
|
+
- **写作技能**(`skills/geml/`)—— 黄金规则、校验闭环,以及一份分节的参考文档
|
|
19
|
+
(`references/authoring.geml`),Agent 按需取其中一节。
|
|
20
|
+
- **代码图谱技能**(`skills/geml-code-graph/`)—— 构建、查看、更新和浏览项目的
|
|
21
|
+
调用图:谁调用了 X、X 调用了谁、影响路径,并在浏览器里渲染出图。
|
|
22
|
+
|
|
23
|
+
这个 bundle 自身不含任何代码——它配置的两个插件都随 dsh 安装自带,技能则是
|
|
24
|
+
Markdown。安装时不构建任何东西,因此完全不涉及 `allowBuilds` 构建授权。
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
dsh plugin --profile <name> add @geml/dsh-plugin
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
先不启动、只验证这一层,再启动:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
dsh --profile <name> --dump-config # 应能看到 "# == @geml/dsh-plugin" 这一层
|
|
36
|
+
dsh --profile <name>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`dsh plugin --profile <name> remove @geml/dsh-plugin` 会同时移除依赖和这一层。
|
|
40
|
+
|
|
41
|
+
## 配置
|
|
42
|
+
|
|
43
|
+
两行都是普通配置:在你 profile 的 `cordis.patch.yml` 里按 `id` 覆盖即可,注意
|
|
44
|
+
要把该行需要的每个键都重新写全。例如把 `mcp-geml` 钉到某个 CLI 版本:
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
- id: mcp-geml
|
|
48
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
49
|
+
config:
|
|
50
|
+
serverName: geml
|
|
51
|
+
transport: stdio
|
|
52
|
+
command: npx
|
|
53
|
+
args: ['-y', '@geml/geml@1.8.1', 'mcp', '--root', '.']
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
用 PATH 上的全局 `geml` 也可以——把 `command` 改成 `geml`,去掉 `npx` 那几个
|
|
57
|
+
参数。
|
|
58
|
+
|
|
59
|
+
想要 CLI 和同一套技能用在 Claude Code 上:`npx -y @geml/geml skill install`,
|
|
60
|
+
或使用 [`../claude-plugin`](../claude-plugin) 下的插件。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# The layer this bundle contributes. Two rows, no code of its own: both plugins
|
|
2
|
+
# ship inside the dsh installation (`apps/cli` depends on them), so this package
|
|
3
|
+
# carries only configuration and the skill text.
|
|
4
|
+
- insert:
|
|
5
|
+
# The geml MCP server, one stdio connection per session. `--root .` confines
|
|
6
|
+
# it to the session's own project directory. The model sees the tools as
|
|
7
|
+
# `mcp__geml__geml_get`, `mcp__geml__geml_set`, … — server-qualified names.
|
|
8
|
+
- id: mcp-geml
|
|
9
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
10
|
+
config:
|
|
11
|
+
serverName: geml
|
|
12
|
+
transport: stdio
|
|
13
|
+
command: npx
|
|
14
|
+
args: ['-y', '@geml/geml', 'mcp', '--root', '.']
|
|
15
|
+
|
|
16
|
+
# The skills travel with this package rather than being copied into the
|
|
17
|
+
# user's skill root: `baseUrl` is this patch file's own directory, so the
|
|
18
|
+
# root resolves wherever the bundle is installed. An isolated provider
|
|
19
|
+
# (`includeDefaultRoots: false`, own `providerName`) contributes these two
|
|
20
|
+
# skills without shadowing or duplicating the user's own roots.
|
|
21
|
+
- id: skill-geml
|
|
22
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
23
|
+
config:
|
|
24
|
+
providerName: geml
|
|
25
|
+
includeDefaultRoots: false
|
|
26
|
+
customSkillDirs:
|
|
27
|
+
- !!js "process.getBuiltinModule('node:url').fileURLToPath(new URL('skills/', baseUrl))"
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geml/dsh-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Agent-Native document handling for DSH — addressable blocks let an agent read and edit one section instead of the whole file. Ships the geml MCP server and the authoring and code-graph skills.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/geml-spec/geml",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/geml-spec/geml.git",
|
|
10
|
+
"directory": "integrations/dsh-plugin"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["dsh-plugin", "dsh", "deepseek-harness", "geml", "mcp", "documents", "codemap"],
|
|
13
|
+
"files": ["cordis.patch.yml", "skills", "LICENSE"],
|
|
14
|
+
"dsh": {
|
|
15
|
+
"bundle": {
|
|
16
|
+
"patch": "./cordis.patch.yml"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geml
|
|
3
|
+
description: >-
|
|
4
|
+
Address a document by its BLOCKS instead of reading it whole. Use for any long
|
|
5
|
+
Markdown or documentation file — README, spec, guide, design doc, changelog —
|
|
6
|
+
when the job is to find where something is documented, read one section, or
|
|
7
|
+
change one section: `geml list`, `geml find` and `geml get` read Markdown
|
|
8
|
+
directly and hand back the one block that matters, leaving the file the
|
|
9
|
+
Markdown it already was. Skip it when the whole file is short enough to read
|
|
10
|
+
anyway. Use it also to read, author, edit or validate GEML itself — .geml
|
|
11
|
+
files, .gemlhistory sidecars, typed blocks, === fences, geml-chart, converting
|
|
12
|
+
Markdown to GEML — where the output must parse cleanly (zero error
|
|
13
|
+
diagnostics) against the reference parser.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Reading and editing documents blockwise
|
|
17
|
+
|
|
18
|
+
Two jobs. The first is the common one, and it needs none of GEML's syntax.
|
|
19
|
+
|
|
20
|
+
## A document that is NOT GEML — use this as a read layer
|
|
21
|
+
|
|
22
|
+
`geml list`, `geml find` and `geml get` read **Markdown** directly. Use them to
|
|
23
|
+
locate and read one block of a long document. Nothing is converted, nothing is
|
|
24
|
+
written, and the file stays exactly the Markdown it already was.
|
|
25
|
+
|
|
26
|
+
**Only when you would otherwise read the whole file to change part of it.** If
|
|
27
|
+
the file is short, or you already know the exact string to replace, open it the
|
|
28
|
+
ordinary way — the round trip costs more than it saves. What is saved is only
|
|
29
|
+
ever the part you did not have to read: on this project's own docs a `list` map
|
|
30
|
+
runs 5–11% of the file it maps, but on a changelog of many small sections it is
|
|
31
|
+
37%.
|
|
32
|
+
|
|
33
|
+
1. `geml list <file>` — every block: address, kind, line range.
|
|
34
|
+
2. `geml find "<text>" <file>` — which block holds those words, answered as an
|
|
35
|
+
ADDRESS rather than a line number, so it survives the next edit. (On a file
|
|
36
|
+
that is not `.geml` this needs 1.7.5 or newer; older builds report no match
|
|
37
|
+
rather than an error, so if a search you expect to hit comes back empty,
|
|
38
|
+
use `list`.)
|
|
39
|
+
3. `geml get <file> '<address>'` — that block and nothing else.
|
|
40
|
+
4. Edit with the ORDINARY file-editing tool, using the text from step 3 as the
|
|
41
|
+
string to replace.
|
|
42
|
+
|
|
43
|
+
Step 4 is a safety property, not a shortcut taken for speed. `geml set` and
|
|
44
|
+
`geml replace` write through GEML's own semantics; a Markdown document nobody
|
|
45
|
+
asked to convert is edited the ordinary way. If the block text does not match
|
|
46
|
+
the file byte for byte, that edit fails loudly instead of writing something
|
|
47
|
+
wrong.
|
|
48
|
+
|
|
49
|
+
Never convert a document to GEML, never leave a `.gemlhistory` beside one, and
|
|
50
|
+
do not pitch the format: use the tool, report the change you made, and let the
|
|
51
|
+
file stay what it is.
|
|
52
|
+
|
|
53
|
+
If `geml --version` does not answer, none of this is available. Read and edit
|
|
54
|
+
the ordinary way, and do not tell anyone to install anything.
|
|
55
|
+
|
|
56
|
+
## A project moving TO GEML
|
|
57
|
+
|
|
58
|
+
"This project's documents are GEML now" means new documents are authored as
|
|
59
|
+
`.geml` — notes, plans, findings, reports — in one directory (`docs/geml/`
|
|
60
|
+
unless the project says otherwise), one file per topic, with an `index.geml`
|
|
61
|
+
saying what is there and why. It does not mean converting what is already
|
|
62
|
+
written, and nobody has to say "leave the existing files alone" for that to
|
|
63
|
+
hold.
|
|
64
|
+
|
|
65
|
+
**Add, never replace.** Writing a `.geml` version of a document is not licence
|
|
66
|
+
to delete the Markdown it was drawn from — however completely the content was
|
|
67
|
+
carried across, and whatever a "one home per topic" convention seems to imply.
|
|
68
|
+
Deleting a file is a request a person makes, never an inference from a
|
|
69
|
+
convention. When both exist, say in each what it is for and name one of them as
|
|
70
|
+
the place a given fact is maintained: two documents describing a project is
|
|
71
|
+
fine, two documents maintaining the same fact is what drifts.
|
|
72
|
+
|
|
73
|
+
## A GEML document — get the syntax right
|
|
74
|
+
|
|
75
|
+
GEML expresses **every** kind of structured content — code, tables, diagrams,
|
|
76
|
+
math, callouts, metadata — through **one** primitive: the **typed block**
|
|
77
|
+
(`=== <type> {#id .class key=val}` … `===`). Always finish by **validating**: a
|
|
78
|
+
GEML file is correct only when `geml check` reports **no error diagnostics**
|
|
79
|
+
(exit 0).
|
|
80
|
+
|
|
81
|
+
## Golden rules (the things that are easy to get wrong)
|
|
82
|
+
|
|
83
|
+
1. **Fences are runs of `=` (≥3).** A block closes at a `=` run of **exactly
|
|
84
|
+
the opening length**, or — when the block has an `#id` — at the labeled
|
|
85
|
+
fence `=== #id` (any `=` run ≥3 followed by the id; no length counting).
|
|
86
|
+
2. **Nest with longer fences.** A body containing `===` lines needs a
|
|
87
|
+
**longer** outer fence: `====` wraps `===`. Careful: a same-length bare
|
|
88
|
+
`===` in the body closes the block even if you intend a labeled close —
|
|
89
|
+
the labeled close only spares you length-counting, it does NOT protect
|
|
90
|
+
same-length inner fences.
|
|
91
|
+
3. **Headings are ATX `#` only** (`#`…`######`). No setext underlines, no
|
|
92
|
+
`---` breaks, no YAML frontmatter — metadata is a `=== meta` block, and the
|
|
93
|
+
document TITLE lives there (`title = "…"`), not in an H1. A heading may
|
|
94
|
+
carry a stable explicit id: `## Title {#sec}`.
|
|
95
|
+
4. **Give every section a stable `{#id}`** — `## Findings {#findings}` — then
|
|
96
|
+
keep ids unique per document, with **every reference resolving**:
|
|
97
|
+
`[t](#id)`, `[[#id]]`, `[^id]`, `src=`, `data=`, `other.geml#id`. An
|
|
98
|
+
unresolved reference is a build **error**. Naming them is the part that pays
|
|
99
|
+
later: a document with no ids costs what Markdown costs, because there is
|
|
100
|
+
nothing for `geml get` to read or `geml set` to replace short of the file.
|
|
101
|
+
5. **No raw HTML.** Notes → `=== note`, comments → `%%` lines, hidden content
|
|
102
|
+
→ `{hidden}`, addressable prose → `=== text`, verified data → `=== data`
|
|
103
|
+
(json/jsonl; `code` shows text, `data` IS data).
|
|
104
|
+
|
|
105
|
+
## Validate every time
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
geml check file.geml # diagnostics + exit code only; exit 0 = correct
|
|
109
|
+
geml check --json file.geml # machine-readable diagnostics array
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
If `geml` is not on PATH: `npm i -g @geml/geml` (package `@geml/geml`, command
|
|
113
|
+
`geml`), or run without installing via `npx -y @geml/geml check file.geml`.
|
|
114
|
+
Inside the geml-spec repo prefer the local build:
|
|
115
|
+
`node geml-parser/dist/geml.js <args>`. If no parser is reachable, follow the
|
|
116
|
+
golden rules and validate once it is.
|
|
117
|
+
|
|
118
|
+
`geml skill install` sets all of this up user-global, and installs this text
|
|
119
|
+
into whatever other agent tools it detects — a tool's directory has to be there
|
|
120
|
+
already; none is ever created for you. `--dry-run` shows what it would do.
|
|
121
|
+
|
|
122
|
+
## Work blockwise (agent editing)
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
geml list file.geml # CALL THIS FIRST — every block, its address, kind, lines
|
|
126
|
+
geml find "text" file|dir # search block CONTENT -> file<TAB>address (exit 1 = no hit)
|
|
127
|
+
# a NAMED file is searched whatever its extension (.md too);
|
|
128
|
+
# a directory walks *.geml only
|
|
129
|
+
geml get file.geml '#id' # read ONE block (a heading id = its whole section)
|
|
130
|
+
geml set file.geml '#id' --in f # replace ONE block (re-parsed; never writes a broken doc)
|
|
131
|
+
geml history save file.geml -m "…" # snapshot to .gemlhistory after each meaningful edit
|
|
132
|
+
geml revert file.geml '#id' # roll ONE block back (--rev -2 | changed | <rev-id>)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Address a block, never a line range: `#id` · `'## Heading'` (its whole section)
|
|
136
|
+
· `L27-58` (the smallest block holding those lines — how a line number from an
|
|
137
|
+
editor, a linter or a diff hunk becomes an address). `list` and `find` print
|
|
138
|
+
addresses that paste straight into the others, so neither `grep` nor a line
|
|
139
|
+
count is needed to locate anything.
|
|
140
|
+
|
|
141
|
+
The rest is one `geml get` away in the reference below, and stays there because
|
|
142
|
+
it is needed rarely and this page is read every time: the remaining address
|
|
143
|
+
forms in `#cli`, and in `#editing` the three ways to cut a section
|
|
144
|
+
(`--head`/`--intro`/`--body`), the experimental `replace`, and what a write that
|
|
145
|
+
drops blocks does.
|
|
146
|
+
|
|
147
|
+
## Full reference — pull ONE section, not the whole file
|
|
148
|
+
|
|
149
|
+
`references/authoring.geml` (under this skill's base directory) holds the
|
|
150
|
+
detailed reference. Fetch just the section you need:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
geml get <skill-base>/references/authoring.geml '#tables'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| section | covers |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `#typed-block` | block anatomy, attribute object, examples of every registered type |
|
|
159
|
+
| `#tables` | pipe/CSV bodies, `compute=`, `summary=`, printf display, `span=` merges |
|
|
160
|
+
| `#charts` | `geml-chart` diagrams bound to a table via `data=#id` |
|
|
161
|
+
| `#data` | the `data` block — value tree, `json`/`jsonl` formats, blind append, chart binding |
|
|
162
|
+
| `#inline` | inline markup, links/refs/footnotes, task lists, media embeds |
|
|
163
|
+
| `#hidden` | `%%` comments, `{hidden}`, `{{key}}` interpolation, `=== embed` |
|
|
164
|
+
| `#cli` | every CLI verb — get/set/add/delete/rename, `--to` conversion, check |
|
|
165
|
+
| `#editing` | the blockwise editing loop + `.gemlhistory` versioning |
|
|
166
|
+
| `#project-config` | carrying a project's Claude config docs in GEML, quietly |
|
|
167
|
+
| `#checklist` | full pre-flight authoring checklist |
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
=== meta
|
|
2
|
+
title = "GEML authoring reference"
|
|
3
|
+
role = "detail sections behind the geml skill"
|
|
4
|
+
howto = "pull ONE section: geml get <this-file> '#<section-id>' — ids: typed-block, tables, charts, data, inline, hidden, cli, editing, project-config, checklist, reference"
|
|
5
|
+
===
|
|
6
|
+
|
|
7
|
+
%% The golden rules and the validation workflow live in ../SKILL.md (always loaded
|
|
8
|
+
%% with the skill). This file holds the full detail, one addressable section per
|
|
9
|
+
%% topic. Keep the section ids stable — SKILL.md's section map points at them.
|
|
10
|
+
|
|
11
|
+
# Typed block {#typed-block}
|
|
12
|
+
|
|
13
|
+
==== code {#ex-anatomy lang=geml}
|
|
14
|
+
=== <type> {#id .class key=val}
|
|
15
|
+
<body>
|
|
16
|
+
===
|
|
17
|
+
====
|
|
18
|
+
|
|
19
|
+
The **type** decides how the body is read (the *body mode*):
|
|
20
|
+
|
|
21
|
+
- `raw` (verbatim): `code`, `diagram`, `table`, `data`, `math`, `embed`
|
|
22
|
+
- `flow` (parsed prose with inline markup): `note` (callout), `text`
|
|
23
|
+
(addressable prose — an `#id` for a run of plain prose, no callout chrome;
|
|
24
|
+
use sparingly)
|
|
25
|
+
- `data` (one `key=val` per line): `meta`
|
|
26
|
+
|
|
27
|
+
An **unknown type** is a warning (body kept raw) — prefer the registered types.
|
|
28
|
+
|
|
29
|
+
## Attribute object {#attribute-object}
|
|
30
|
+
|
|
31
|
+
Written `{#id .class key=val}` on the opening fence or a heading line:
|
|
32
|
+
|
|
33
|
+
- `#id` — unique anchor for references.
|
|
34
|
+
- `.class` — a *semantic* label (no styling implied).
|
|
35
|
+
- `key=val` — typed: quoted `"…"` = string; `true`/`false` = bool;
|
|
36
|
+
integer/float syntax = number; any other bare word = string. A **bare word
|
|
37
|
+
with no `=` is a boolean flag set to true** (e.g. `hidden`).
|
|
38
|
+
- Order is insignificant; recommended `#id`, then `.class`, then `key=val`.
|
|
39
|
+
|
|
40
|
+
## Examples of each block {#block-examples}
|
|
41
|
+
|
|
42
|
+
==== code {#ex-blocks lang=geml}
|
|
43
|
+
=== meta
|
|
44
|
+
title = "Budget plan"
|
|
45
|
+
version = "1.0-draft"
|
|
46
|
+
===
|
|
47
|
+
|
|
48
|
+
=== code {#hello lang=python}
|
|
49
|
+
print("hi")
|
|
50
|
+
===
|
|
51
|
+
|
|
52
|
+
=== note {.warning}
|
|
53
|
+
Back up before upgrading. (flow body — inline markup works here)
|
|
54
|
+
===
|
|
55
|
+
|
|
56
|
+
=== text {#thesis}
|
|
57
|
+
Addressable prose: plain rendering, but geml get/set #thesis can edit it.
|
|
58
|
+
===
|
|
59
|
+
|
|
60
|
+
=== math {#gauss caption="Gaussian integral"}
|
|
61
|
+
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
|
|
62
|
+
===
|
|
63
|
+
|
|
64
|
+
=== diagram {#flow format=mermaid caption="Review flow"}
|
|
65
|
+
graph LR
|
|
66
|
+
A[Draft] --> B{Review} -->|ok| C[Publish]
|
|
67
|
+
===
|
|
68
|
+
====
|
|
69
|
+
|
|
70
|
+
`diagram` hosts an external DSL (`mermaid`, `graphviz`, `dot`, `d2`,
|
|
71
|
+
`plantuml`, `geml-chart`); the processor never interprets the body. An unknown
|
|
72
|
+
`format` is a warning.
|
|
73
|
+
|
|
74
|
+
# Tables {#tables}
|
|
75
|
+
|
|
76
|
+
Two bodies, one model: the visual (pipe) form, or the data form
|
|
77
|
+
(`format=csv`/`tsv`). Both parse to the same table model.
|
|
78
|
+
|
|
79
|
+
==== code {#ex-tables lang=geml}
|
|
80
|
+
=== table {#budget caption="Annual cost"}
|
|
81
|
+
| Plan | Months | Rate |
|
|
82
|
+
|-------|-------:|-----:|
|
|
83
|
+
| Basic | 1 | 30 |
|
|
84
|
+
===
|
|
85
|
+
|
|
86
|
+
=== table {#fy25 format=csv header=1 compute="FY [%.1f] = Q1 + Q2 + Q3 + Q4" summary="Segment = 'Total'; FY [%.1f] = sum(FY)"}
|
|
87
|
+
Segment, Q1, Q2, Q3, Q4
|
|
88
|
+
Cloud, 8, 10, 12, 14
|
|
89
|
+
===
|
|
90
|
+
====
|
|
91
|
+
|
|
92
|
+
- `delim=";"` — one character replacing the data form's natural delimiter (`,`
|
|
93
|
+
for csv, tab for tsv): European CSV, `|`-delimited exports. Anything but a
|
|
94
|
+
single character is an error, a tab is `format=tsv` (attribute values have no
|
|
95
|
+
escapes), and `delim` without a data `format` is ignored (warning). The data
|
|
96
|
+
form splits and nothing more — it never strips outer `|` like the visual form.
|
|
97
|
+
- `compute="Name = expr; Name2 = expr2"` — per-row formulas over columns (by
|
|
98
|
+
header name, or single letter `A`,`B`,…), operators `+ - * / ( )`.
|
|
99
|
+
Reference an earlier computed column by name. Quote names with spaces:
|
|
100
|
+
`'Unit Price'`.
|
|
101
|
+
- Aggregates `sum|avg|min|max|count` (e.g. `sum(FY)`) — for the `summary=`
|
|
102
|
+
foot row. A bare (non-aggregated) column ref in `summary` is an error.
|
|
103
|
+
- A trailing `[printf]` on a name sets numeric display: `FY [%.1f]`,
|
|
104
|
+
`P [%.1f%%]`.
|
|
105
|
+
- Merge cells with `span="r2c1:2x1"`.
|
|
106
|
+
|
|
107
|
+
# Charts {#charts}
|
|
108
|
+
|
|
109
|
+
Render a table — don't copy it:
|
|
110
|
+
|
|
111
|
+
==== code {#ex-chart lang=geml}
|
|
112
|
+
=== diagram {#rev format=geml-chart data=#fy25 type=bar x=Segment y=FY}
|
|
113
|
+
===
|
|
114
|
+
====
|
|
115
|
+
|
|
116
|
+
`data=#id` must point at a `table` block (single source of truth); the column
|
|
117
|
+
refs (`x`, `y`, …) are checked. `type ∈ {bar,line,area,pie,scatter}`. The body
|
|
118
|
+
is empty (the spec lives in attributes).
|
|
119
|
+
|
|
120
|
+
# Data blocks {#data}
|
|
121
|
+
|
|
122
|
+
`=== data` carries the VALUE TREE (scalars/arrays/objects — JSON's value
|
|
123
|
+
domain) as verified data. The dividing line: `code` shows text the processor
|
|
124
|
+
never interprets; `data` IS data — the body parses under `format=`, and a
|
|
125
|
+
body the engine rejects is a build ERROR naming the line.
|
|
126
|
+
|
|
127
|
+
==== code {#ex-data lang=geml}
|
|
128
|
+
=== data {#cfg}
|
|
129
|
+
{"name": "geml", "port": 8140}
|
|
130
|
+
===
|
|
131
|
+
|
|
132
|
+
=== data {#log format=jsonl}
|
|
133
|
+
{"ts":"09:00","latency":41}
|
|
134
|
+
{"ts":"09:01","latency":58}
|
|
135
|
+
===
|
|
136
|
+
|
|
137
|
+
=== diagram {format=geml-chart data=#log type=line x=ts y=latency}
|
|
138
|
+
===
|
|
139
|
+
====
|
|
140
|
+
|
|
141
|
+
- `format=json` (default): the body is ONE JSON value. `format=jsonl`: one
|
|
142
|
+
JSON value per non-blank line — the record-stream form. Because a document
|
|
143
|
+
is a flat sequence of blocks, appending a complete `data` block at EOF is a
|
|
144
|
+
valid continuation of any document (blind-append, like a jsonl file, with
|
|
145
|
+
ids and verification on top).
|
|
146
|
+
- `src=` loads the content from an external file (`.json`/`.jsonl`; explicit
|
|
147
|
+
`format=` wins over the extension) — exactly ONE of `src=` and a body.
|
|
148
|
+
`http(s)` sources load at render time. The log arrangement: keep the
|
|
149
|
+
records in a plain `.jsonl` any tool can append to and tail — the GEML doc
|
|
150
|
+
is its verified, chartable view. A chart may also name a local file
|
|
151
|
+
directly: `data=log.jsonl`.
|
|
152
|
+
- A source route MAY narrow the file to a line range — `src=log.jsonl#L900-999`,
|
|
153
|
+
1-based and inclusive — which is how a window of a long log is addressed.
|
|
154
|
+
`code` uses the SAME route syntax for the code it shows
|
|
155
|
+
(`src=src/attrs.ts#L14-24`): the route is the source of truth, a range the
|
|
156
|
+
file no longer has is an error (a drifted reference fails the build), and a
|
|
157
|
+
body kept alongside it is a snapshot that warns when it goes stale. Routes
|
|
158
|
+
resolve document-relative, or relative to `--root` when one is given.
|
|
159
|
+
- `yaml`/`toml` are RESERVED names: no engine in the core — body kept raw
|
|
160
|
+
plus a warning, never guessed. csv/tsv belong to `table`, not `data`
|
|
161
|
+
(their delimiter/header dialect parameters only mean something against a
|
|
162
|
+
column model).
|
|
163
|
+
- A chart's `data=#id` accepts a `data` block whose value is a RECORD ARRAY
|
|
164
|
+
(non-empty array of objects): keys project to columns; every column the
|
|
165
|
+
chart references must be present and scalar in every record.
|
|
166
|
+
- `schema=` names a block (`#id`) or GEML document holding a schema —
|
|
167
|
+
reference-checked only today; value validation is a later GEP.
|
|
168
|
+
- The parsed value lives in the model: `geml get '#cfg' --json` returns the
|
|
169
|
+
node with `value`, no re-parsing. `geml fmt` canonicalizes: json at
|
|
170
|
+
two-space indent, jsonl one compact value per line.
|
|
171
|
+
|
|
172
|
+
# Inline markup {#inline}
|
|
173
|
+
|
|
174
|
+
Inside flow blocks only: `*emphasis*` · `**strong**` · `` `code` `` ·
|
|
175
|
+
`~~strike~~` · `$inline math$`.
|
|
176
|
+
|
|
177
|
+
- Link: `[text](https://…)` · internal ref `[text](#id)` · auto-ref `[[#id]]`
|
|
178
|
+
(link text from the target's caption/heading) · footnote `[^id]`.
|
|
179
|
+
- Media embed: `` — kind (image/audio/video) inferred from the
|
|
180
|
+
extension; renders/plays in place (a link navigates, an embed does not).
|
|
181
|
+
- Hard line break: trailing `\`. Escape punctuation with `\`; block syntax at
|
|
182
|
+
line start is escaped the same way (`\===`, `\#`).
|
|
183
|
+
- Lists: `- item` / `1. item`. **Task list**: `- [ ] open` / `- [x] done`.
|
|
184
|
+
|
|
185
|
+
# Hidden, comments, interpolation, embed {#hidden}
|
|
186
|
+
|
|
187
|
+
- **`%%` line** — a hidden, raw, never-rendered note (TODO/review remark).
|
|
188
|
+
Kept in the model (tools can find it) but NOT inline-parsed, so a scratch
|
|
189
|
+
note can't break the build. Line-start only.
|
|
190
|
+
- **`{hidden}` block** — present in the model and **fully reference-checked**,
|
|
191
|
+
but not rendered. Use it for a source table that only feeds a chart:
|
|
192
|
+
`=== table {#fy25 hidden …}`.
|
|
193
|
+
- **`{{key}}`** in flow text is replaced with the matching `=== meta` value;
|
|
194
|
+
an unknown key is a build **error** (single source of truth). Inside a code
|
|
195
|
+
span it stays verbatim — that is how to *show* the syntax.
|
|
196
|
+
- **`=== embed {src=other.geml#id}`** stands for content that lives elsewhere
|
|
197
|
+
and renders it in place; a fragment naming a heading takes the whole
|
|
198
|
+
section, and no fragment takes the whole document. `src=` is
|
|
199
|
+
reference-checked, so a reference-only index document can be validated.
|
|
200
|
+
Cycles are an error; nesting is capped.
|
|
201
|
+
|
|
202
|
+
# CLI {#cli}
|
|
203
|
+
|
|
204
|
+
Validate first — `geml check` exits non-zero on any error, a hard pass/fail
|
|
205
|
+
signal, and prints only diagnostics (cheap on context):
|
|
206
|
+
|
|
207
|
+
=== code {#cli-check lang=sh}
|
|
208
|
+
geml check file.geml # diagnostics + exit code only
|
|
209
|
+
geml check --json file.geml # machine-readable diagnostics array
|
|
210
|
+
geml check --root . file.geml # widen cross-doc reference resolution to a dir
|
|
211
|
+
===
|
|
212
|
+
|
|
213
|
+
All commands accept `-` to read from stdin.
|
|
214
|
+
|
|
215
|
+
=== code {#cli-verbs lang=sh}
|
|
216
|
+
geml file.geml # document-model JSON (default --to json)
|
|
217
|
+
geml list file.geml # CALL FIRST: every block, its address, kind, line range
|
|
218
|
+
geml find "text" file|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit
|
|
219
|
+
# a NAMED file is searched whatever its extension — `list`,
|
|
220
|
+
# `get` and `find` all read Markdown, so this addresses a
|
|
221
|
+
# plain README without converting it; a DIRECTORY walks *.geml
|
|
222
|
+
geml get file.geml # same listing as `list` (the no-selector default)
|
|
223
|
+
geml get file.geml '#id' # print ONE block (raw span; --json = model node)
|
|
224
|
+
geml get file.geml '=== note' # every block of a type; '@a3f9c1d2' = a block with no #id
|
|
225
|
+
geml get file.geml 'L27-58' # position: the smallest block holding those lines
|
|
226
|
+
geml get file.geml '#sec' --intro # a section cut three ways: --head | --intro | --body
|
|
227
|
+
geml set file.geml '#id' --in f # replace ONE block (guarded: re-parsed, never writes broken)
|
|
228
|
+
geml set file.geml '#sec' --intro # replace just the opening; the subsections stay put
|
|
229
|
+
geml replace file.geml OLD NEW # EXPERIMENTAL, may be withdrawn: literal swap, checked and
|
|
230
|
+
# reported; --within '#id' or '=== type' narrows the scope
|
|
231
|
+
geml add file.geml --after '#id' --in f # insert a fragment (keeps its own ids)
|
|
232
|
+
geml delete file.geml '#id' ['#id2'] # remove one or more blocks
|
|
233
|
+
geml rename file.geml '#old' '#new' # rename an id AND every reference to it
|
|
234
|
+
===
|
|
235
|
+
|
|
236
|
+
A heading id addresses its whole SECTION (through the next same-or-higher
|
|
237
|
+
heading); `--head` narrows any id to its head line alone (rename a heading, or
|
|
238
|
+
edit a block's attributes without re-sending its body).
|
|
239
|
+
|
|
240
|
+
An `embed` block has no content of its own, so `get '#e'` returns the FRAME
|
|
241
|
+
(its `src=`). To see what the window looks onto, add `--view`:
|
|
242
|
+
|
|
243
|
+
=== code {#cli-view lang=sh}
|
|
244
|
+
geml get file.geml '#e' --view # the entity block the chain ends at
|
|
245
|
+
geml get file.geml '#e' --view --body # just its body — the usual want
|
|
246
|
+
geml get file.geml '#e' --view --json # its model node, plus `from`
|
|
247
|
+
===
|
|
248
|
+
|
|
249
|
+
`--view` resolves to the ENTITY block: multi-layer chains are followed to the
|
|
250
|
+
end, and on any block that is not an embed it changes nothing. Provenance goes
|
|
251
|
+
to stderr (`view: #e -> part.geml#tip`) because the bytes belong to ANOTHER
|
|
252
|
+
document — their refs and relative paths resolve against that one. It is
|
|
253
|
+
read-only (`set` refuses it), chain reads are confined to `--root` (default: the
|
|
254
|
+
document's own directory), and a non-local target is refused, never fetched. A
|
|
255
|
+
SECTION selector is the identity: piercing an embed inside it would splice two
|
|
256
|
+
documents' bytes together, so address that embed instead. MCP: `geml_get
|
|
257
|
+
{view: true, part: "body"}`.
|
|
258
|
+
|
|
259
|
+
Conversion is ONE entry — `geml <file> --to <format>` — not a verb per format:
|
|
260
|
+
|
|
261
|
+
=== code {#cli-convert lang=sh}
|
|
262
|
+
geml file.geml --to html -o out.html # one self-contained, interactive HTML file
|
|
263
|
+
geml file.geml --to md -o out.md # GitHub-Flavored Markdown (lossy; loss notes on stderr)
|
|
264
|
+
geml input.md --to geml -o out.geml # Markdown -> GEML
|
|
265
|
+
geml file.geml --to geml # canonical re-format (idempotent)
|
|
266
|
+
===
|
|
267
|
+
|
|
268
|
+
Install: `npm i -g @geml/geml` (package `@geml/geml`, command `geml`), or
|
|
269
|
+
one-shot via `npx -y @geml/geml <args>`. From a clone of the geml-spec repo:
|
|
270
|
+
`cd geml-parser && npm install && npm run build && npm link`, or run
|
|
271
|
+
`node geml-parser/dist/geml.js <args>` directly.
|
|
272
|
+
|
|
273
|
+
# Editing and versioning {#editing}
|
|
274
|
+
|
|
275
|
+
When revising a `.geml` over many steps, work **one block at a time** and
|
|
276
|
+
snapshot as you go, rather than re-emitting the whole file:
|
|
277
|
+
|
|
278
|
+
=== code {#editing-loop lang=sh}
|
|
279
|
+
geml get file.geml '#intro' # read just this block (a heading id = its whole section)
|
|
280
|
+
geml set file.geml '#intro' --in - # replace just this span (stdin or --in FILE);
|
|
281
|
+
# the splice is re-parsed and REJECTED if it breaks the doc
|
|
282
|
+
geml history save file.geml -m "…" # snapshot into the .gemlhistory sidecar — do this each step
|
|
283
|
+
geml history get file.geml # revisions, newest first; first column IS the --rev selector
|
|
284
|
+
geml revert file.geml '#intro' # roll ONE block back to the previous revision (= --rev -1)
|
|
285
|
+
geml revert file.geml '#intro' --rev -2 # …two revisions back (also: --rev 0 = tip, --rev <id>)
|
|
286
|
+
geml revert file.geml '#intro' --rev changed # …the block's last ACTUAL change — use this after other
|
|
287
|
+
# blocks were written since; a fixed -N silently no-ops there
|
|
288
|
+
===
|
|
289
|
+
|
|
290
|
+
**Retain every step.** `history` and `revert` can only recover what was saved
|
|
291
|
+
— so after each meaningful edit to a `.geml`, run `geml history save`
|
|
292
|
+
(automatable with a `PostToolUse` hook). Together, `get`/`set` (address one
|
|
293
|
+
block) and `history`/`revert` (version and rewind it) let an agent revise a
|
|
294
|
+
document incrementally and undo any single section.
|
|
295
|
+
|
|
296
|
+
**A section can be cut three ways**, on `get` and `set` alike: `--head` (the
|
|
297
|
+
heading line), `--intro` (what it says before its first subheading — empty when
|
|
298
|
+
one follows immediately, the whole body when none does), `--body` (everything
|
|
299
|
+
under it, so it always contains the intro). `--intro` is how you edit a
|
|
300
|
+
section's opening without pulling its subsections into context, and setting an
|
|
301
|
+
empty one writes an opening where the section had none.
|
|
302
|
+
|
|
303
|
+
**When the exact old text is already known** and nothing needs reading — a
|
|
304
|
+
version string in six places, a renamed term — `geml replace` is the cheap path,
|
|
305
|
+
and the one to prefer over dropping to `sed`: the same two short strings, but
|
|
306
|
+
the result is re-parsed before it lands, the blocks it touched are named back to
|
|
307
|
+
you, and it is in `.gemlhistory` to revert. It swaps a LITERAL, never a pattern,
|
|
308
|
+
and refuses a swap that would rename an id (use `geml rename`, which fixes the
|
|
309
|
+
references too). **It is EXPERIMENTAL and may be withdrawn** — reach for it, but
|
|
310
|
+
do not build anything on it that cannot change.
|
|
311
|
+
|
|
312
|
+
**A write is refused when it would BREAK the document**, never merely because it
|
|
313
|
+
removes something: a replacement that drops blocks is carried out and NAMED on
|
|
314
|
+
stderr — unnamed blocks included — with `geml revert` as the way back. Read,
|
|
315
|
+
edit, write back, and nothing is dropped, because `get` handed those blocks to
|
|
316
|
+
you. Send content that omits them only when removing them is the point.
|
|
317
|
+
|
|
318
|
+
**Where sidecars do NOT belong:** a doc that git already versions — config
|
|
319
|
+
docs especially — usually needs no `.gemlhistory`; do not create one there
|
|
320
|
+
unless the user asks for finer-than-commit history.
|
|
321
|
+
|
|
322
|
+
# Project config in GEML {#project-config}
|
|
323
|
+
|
|
324
|
+
A project's Claude instruction docs can live in GEML with **zero settings
|
|
325
|
+
changes and zero hooks** — quiet by design:
|
|
326
|
+
|
|
327
|
+
1. Put the doc at `.claude/geml/<name>.geml` (e.g. `instructions.geml`), one
|
|
328
|
+
topic per section, each heading with a stable `{#id}` — so a single rule
|
|
329
|
+
can be edited (`geml set`) or removed (`geml delete`) without touching the
|
|
330
|
+
rest.
|
|
331
|
+
2. Reference it from the project's `CLAUDE.md` with one import line:
|
|
332
|
+
`@.claude/geml/instructions.geml` — Claude Code expands imports of any
|
|
333
|
+
file type literally at session start (recursive, max 4 hops).
|
|
334
|
+
3. Leave everything else in `CLAUDE.md` alone — especially tool-managed,
|
|
335
|
+
installer-appended sections (they are updated in place by their
|
|
336
|
+
installers and must stay in Markdown where those tools grep for them).
|
|
337
|
+
4. The imported `.geml` must not contain bare `@path` tokens in prose — the
|
|
338
|
+
import scanner does not recognize GEML code fences, only Markdown ones.
|
|
339
|
+
5. No `.gemlhistory` for these files: git is their history.
|
|
340
|
+
|
|
341
|
+
When asked to create or restructure project instructions, prefer this shape
|
|
342
|
+
without ceremony; never convert files the user did not ask about, and never
|
|
343
|
+
move a tool-managed block out of `CLAUDE.md`.
|
|
344
|
+
|
|
345
|
+
# Authoring checklist {#checklist}
|
|
346
|
+
|
|
347
|
+
- [ ] Every closing fence is a `=` run of exactly the opening length, or the
|
|
348
|
+
labeled fence `=== #id` of an id-carrying block.
|
|
349
|
+
- [ ] Bodies containing `===` runs are wrapped in a LONGER fence (`====`) —
|
|
350
|
+
a same-length bare `===` in the body closes the block early, labeled
|
|
351
|
+
close or not.
|
|
352
|
+
- [ ] Headings are ATX `#`; metadata is a `=== meta` block (no frontmatter);
|
|
353
|
+
the document title is `title = "…"` in meta, not an H1.
|
|
354
|
+
- [ ] All ids unique; all `[t](#id)` / `[[#id]]` / `[[doc.geml#id]]` /
|
|
355
|
+
`[^id]` / `src=` / `data=` references resolve.
|
|
356
|
+
- [ ] `{{key}}` keys exist in `=== meta` (code-span occurrences stay literal).
|
|
357
|
+
- [ ] No raw HTML; comments use `%%`, hidden content uses `{hidden}`.
|
|
358
|
+
- [ ] Validated: `geml check` reports zero error diagnostics (exit 0).
|
|
359
|
+
|
|
360
|
+
# Reference {#reference}
|
|
361
|
+
|
|
362
|
+
Full normative spec, in the geml-spec repo
|
|
363
|
+
(https://github.com/geml-spec/geml): `spec/GEML-spec.md` (English),
|
|
364
|
+
`spec/GEML-spec_CN.md` (中文). History sidecar: `spec/GEML-history-spec.md`.
|
|
365
|
+
The spec is itself written in GEML (dogfood): `spec/in_geml_format/`.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: geml-code-graph
|
|
3
|
+
description: >-
|
|
4
|
+
Build, view, update, and navigate a project's call graph as GEML codemap
|
|
5
|
+
documents. Use when asked to see/update/build a project's code graph or
|
|
6
|
+
codemap (看下/更新下 code-graph), when asked "who calls X" / "what does X
|
|
7
|
+
call" / to trace a call chain or impact path, or whenever a
|
|
8
|
+
.geml-code-graph/ directory with index.geml and _index/name-lookup.json
|
|
9
|
+
exists.
|
|
10
|
+
Detects the project's languages itself — never asks the user; viewing ends
|
|
11
|
+
with the browser OPEN on the graph.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Code-graph navigation (codemap profile)
|
|
15
|
+
|
|
16
|
+
The call graph lives as **text documents, not a database**
|
|
17
|
+
([profile](https://github.com/geml-spec/geml/blob/main/docs/design/specs/codemap/codemap-profile.md)):
|
|
18
|
+
one GEML document per container (module / dir /
|
|
19
|
+
file), each with ONE meta (`module`, `src`, `entry`, `resolution-default`),
|
|
20
|
+
empty-body `code` blocks per method, and up to three CSV edge tables —
|
|
21
|
+
`#calls` (out), `#called-by` (in), `#unresolved` (blind spots). The build's
|
|
22
|
+
`verify` has checked that every edge reference resolves.
|
|
23
|
+
|
|
24
|
+
## The moves
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# 1. resolve a name — where does a symbol live
|
|
28
|
+
node -e "console.log(JSON.stringify(require('./.geml-code-graph/_index/name-lookup.json')['hashtableFind'],null,1))"
|
|
29
|
+
# → [{"anchor":"c:hashtable.c#hashtableFind(…)","doc":"hashtable.c.geml","id":"hashtableFind"}, …]
|
|
30
|
+
# Multiple entries = real ambiguity (e.g. a .c definition and a .h inline) — inspect each.
|
|
31
|
+
|
|
32
|
+
# 2. container overview — the module's surface, one glance
|
|
33
|
+
head -8 .geml-code-graph/hashtable.c.geml # meta: entry = the externally-called methods
|
|
34
|
+
|
|
35
|
+
# 3. open the method block (src= tells you exactly where the code is)
|
|
36
|
+
geml get .geml-code-graph/hashtable.c.geml '#hashtableFind'
|
|
37
|
+
|
|
38
|
+
# 4. forward: what it calls (grep your method's rows; follow doc.geml#id refs)
|
|
39
|
+
geml get .geml-code-graph/hashtable.c.geml '#calls'
|
|
40
|
+
|
|
41
|
+
# 5. reverse: who calls it (aggregated, with file:line sites)
|
|
42
|
+
geml get .geml-code-graph/hashtable.c.geml '#called-by'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A reference is `#id` (same document) or `sibling.geml#id` (that document, that
|
|
46
|
+
block) — `geml get` it the same way. `index.geml` holds the repo-level view:
|
|
47
|
+
app entries in its meta, `#modules` / `#module-edges` aggregate tables.
|
|
48
|
+
|
|
49
|
+
## Reading the tables
|
|
50
|
+
|
|
51
|
+
| Line | Meaning |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `#calls` row, empty confidence | resolved at the document's `resolution-default`, high confidence |
|
|
54
|
+
| `#calls` row `kind=candidate` | dispatch ambiguity: one of several implementations, right after its main `call` row. Treat the SET as the answer, never just the first |
|
|
55
|
+
| `#calls` row confidence `medium`/`low` | the extractor is less sure — say so when reporting |
|
|
56
|
+
| `#unresolved` rows (hidden table) | calls the extractor could NOT resolve — **blind spots, not evidence of absence**; fall back to grep when one matters |
|
|
57
|
+
| `#called-by` absent for a method | no *resolved* callers. Under `resolution-default = heuristic` that means little; under `cpg` it is strong (but pointer/dynamic dispatch still lands in `#unresolved`) |
|
|
58
|
+
|
|
59
|
+
Symbol classes: `.accessor` (bean get/set/is leaves — the graph view hides
|
|
60
|
+
them by default, tables keep them) · `.leaf` (calls nothing, only called — usually skippable when
|
|
61
|
+
tracing logic) · `.test` (test territory) · `.flow-entry` (critical-flow start).
|
|
62
|
+
|
|
63
|
+
## "看下/更新下 X 项目的 code-graph" — the end-to-end move
|
|
64
|
+
|
|
65
|
+
The toolkit ships inside the `@geml/geml` package: `geml codemap …`
|
|
66
|
+
(without a global install: `npx -y @geml/geml codemap …`).
|
|
67
|
+
|
|
68
|
+
### Dispatch first — generation is slow, the conversation must not block on it
|
|
69
|
+
|
|
70
|
+
Indexers take real time (scip: seconds–minutes; Joern on a repo: minutes).
|
|
71
|
+
Pick the executor BEFORE starting:
|
|
72
|
+
|
|
73
|
+
- **Codemap exists, user wants to look** → inline, seconds:
|
|
74
|
+
`serve --background` + open the browser. No subagent.
|
|
75
|
+
- **Update asked and `_index/refresh.json` exists** → no subagent either:
|
|
76
|
+
`geml codemap refresh <dir> --background` (detached process, costs the
|
|
77
|
+
conversation nothing). Open the CURRENT graph immediately — serve renders
|
|
78
|
+
live, so when the refresh lands, F5 shows it; say exactly that.
|
|
79
|
+
- **geml files must be (re)generated agentically** — first build, no recipe
|
|
80
|
+
recorded, adapters change, or a refresh failed → hand the WHOLE generation
|
|
81
|
+
to ONE subagent (Agent tool; `run_in_background: true` so the user can keep
|
|
82
|
+
working). Its prompt must be self-contained: project root; detect the
|
|
83
|
+
languages per the table below (never ask); the exact indexer +
|
|
84
|
+
`geml codemap build --history` + `geml codemap verify` commands; verify
|
|
85
|
+
MUST exit 0; write `_index/refresh.json` with the exact commands used;
|
|
86
|
+
return container/method/entry counts, verify result, and any language
|
|
87
|
+
gaps. The MAIN conversation does the last mile itself when the subagent
|
|
88
|
+
reports: `serve --background`, open the browser (if an older codemap was
|
|
89
|
+
already on screen, telling the user to F5 is the whole move).
|
|
90
|
+
|
|
91
|
+
1. **Have a codemap?** `<proj>/.geml-code-graph/index.geml` exists → skip to
|
|
92
|
+
step 4 (view) or step 3 (update was asked). An older `codemap/`/`graph/`
|
|
93
|
+
tree from before the rename is not special: regenerate into
|
|
94
|
+
`.geml-code-graph/` (one build; carry the `*.gemlhistory` sidecars over
|
|
95
|
+
first if they matter) and remove the old directory.
|
|
96
|
+
2. **Detect the language(s) — NEVER ask the user.** (Steps 2–3 are the
|
|
97
|
+
generation work — per Dispatch above they normally run inside the
|
|
98
|
+
subagent.) Judge from manifests
|
|
99
|
+
first, then source-file counts (`Glob`/`ls`). Multiple languages with
|
|
100
|
+
real code (≥ a handful of files each) → one build with REPEATED
|
|
101
|
+
`--adapter` groups; the codemap merges them (Java+TS validated).
|
|
102
|
+
|
|
103
|
+
| Signal | Indexer → adapter |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `tsconfig.json` / mostly `.ts` `.tsx` `.js` | `npx --yes @sourcegraph/scip-typescript index --output index.scip` (run IN the target repo/subproject) → `--adapter scip --raw index.scip` |
|
|
106
|
+
| React / JSX (`.tsx` `.jsx`) | same scip route, verified tier: `<Child />` render edges, custom-hook calls, and `useReducer(reducer, …)` wiring all resolve high — arrow components (`const Foo = () =>`) included. Indirect dispatch is **absent, not `#unresolved`**: callback-prop calls (`onToggle(…)`), `dispatch()`→reducer case handling, and context-injected functions ride scip locals/members and leave NO edge — grep when one matters. Also invisible: `memo()`/`forwardRef()`-wrapped components (const = call, inner fn is a local) and module-scope `render(<App />)` callers |
|
|
107
|
+
| `Cargo.toml` / `.rs` | `rust-analyzer scip . --output rust.scip` (run IN the crate/workspace root; missing → `rustup component add rust-analyzer` or the rust-analyzer GitHub releases page) → `--adapter scip --raw rust.scip`. Precise tier: rust-analyzer-resolved, cross-file/cross-crate calls included; calls into std/external crates land in `#unresolved` |
|
|
108
|
+
| `pom.xml` / `build.gradle` / `.java` | Joern (locate per **Locating Joern** below; JDK required): `GEML_SRC=<abs-src> GEML_OUT=<abs-raw> GEML_LANG=JAVASRC joern --script <pkg>/codemap/joern-export.sc` → `--adapter joern --raw <raw>`. GEML_LANG takes Joern's `--language` names, UPPERCASE — lowercase `javasrc` fails with "No CPG generator exists" |
|
|
109
|
+
| `.c` / `.h` | same Joern route, `GEML_LANG=NEWC` (valkey-validated) |
|
|
110
|
+
| `.py` / `go.mod` / `.kt` | Joern frontends, `GEML_LANG=PYTHONSRC` etc. (usable tier — SAY SO in your report) |
|
|
111
|
+
| only a code-review-graph `graph.db` | `--db <graph.db>` (heuristic tier — say so) |
|
|
112
|
+
| none of the above | report honestly which languages are unsupported; do not guess |
|
|
113
|
+
|
|
114
|
+
`.vue` / `.svelte` SFCs: covered — use the AUTO build (`geml codemap
|
|
115
|
+
build --root <proj>`), not the manual per-indexer route. It virtualizes
|
|
116
|
+
each SFC project (Volar / svelte2tsx, fetched hermetically via npx) into
|
|
117
|
+
shadow TS with line-map sidecars, runs one scip pass over shadows + the
|
|
118
|
+
project's real TS/JS, and attributes every symbol back to the original
|
|
119
|
+
file and line. Template event handlers surface as edges from a synthetic
|
|
120
|
+
`<Component>.template` node (`@click="save"` → `#App-template, #save`;
|
|
121
|
+
mustapi-validated across three Vue apps, 85/85 SFCs). Honest residuals —
|
|
122
|
+
say them when reporting: component-TAG usage (`<Child/>`) is not a call
|
|
123
|
+
edge; Nuxt auto-imports (unimported `ref`, auto-registered components)
|
|
124
|
+
don't resolve, so those references drop; top-level `<script setup>`
|
|
125
|
+
calls, including `computed(() => …)` bodies, drop exactly like
|
|
126
|
+
module-level calls in plain TS; a failed virtualization falls back to
|
|
127
|
+
plain TS indexing and says so.
|
|
128
|
+
|
|
129
|
+
Vendored source trees explode the job list — next.js's
|
|
130
|
+
`packages/next/src/compiled/` carries ~140 checked-in package.json bundles,
|
|
131
|
+
each becoming its own scip job. Prune them at build time:
|
|
132
|
+
`geml codemap build --root <proj> --exclude "src/compiled/**"` (repeatable;
|
|
133
|
+
the exclusion also keeps their symbols out of the graph).
|
|
134
|
+
|
|
135
|
+
**Locating Joern — never hardcode a path.** Resolve it fresh on each run,
|
|
136
|
+
in this order: (1) `joern` on PATH — if `joern --version` works, use it;
|
|
137
|
+
(2) else read `~/.claude/skills/geml-code-graph/config.json` (`{"joern": "<launcher-or-dir>"}`)
|
|
138
|
+
and pass it as `geml codemap build … --joern <path>` (or export `GEML_JOERN`);
|
|
139
|
+
(3) else ASK the user for the joern-cli location (Windows: the folder unzipped
|
|
140
|
+
from joern-cli.zip; macOS/Linux: the joern-install.sh install dir), WRITE it
|
|
141
|
+
into that JSON file, then reuse it. `<path>` may be the launcher itself or the
|
|
142
|
+
directory holding it (`joern.bat` on Windows, `joern` on unix). Ask at most
|
|
143
|
+
once per machine — after that the JSON answers. Mirrors the CLI's own
|
|
144
|
+
`--joern` / `GEML_JOERN` resolution.
|
|
145
|
+
3. **Build + verify** (also the "更新" path — builds are deterministic,
|
|
146
|
+
only changed documents are rewritten):
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
geml codemap build --adapter scip --raw index.scip --root <proj> \
|
|
150
|
+
--out <proj>/.geml-code-graph --history # --container module|dir|file: match
|
|
151
|
+
# the layout (default dir; flat C repo → file)
|
|
152
|
+
geml codemap verify <proj>/.geml-code-graph # MUST exit 0 before showing anyone
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**First successful build: record the recipe** so `refresh` (and the
|
|
156
|
+
commit hook) can replay it — write `<proj>/.geml-code-graph/_index/refresh.json`
|
|
157
|
+
with the EXACT commands you ran:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{ "root": "..",
|
|
161
|
+
"steps": ["npx --yes @sourcegraph/scip-typescript index --output index.scip",
|
|
162
|
+
"geml codemap build --adapter scip --raw index.scip --root . --out .geml-code-graph --history",
|
|
163
|
+
"geml codemap verify .geml-code-graph"] }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
From then on, "更新下" = `geml codemap refresh <proj>/.geml-code-graph` (skips
|
|
167
|
+
itself when git HEAD hasn't moved; log at `_index/refresh.log`).
|
|
168
|
+
4. **View — finish with the browser OPEN, not with instructions.**
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
geml codemap serve <proj>/.geml-code-graph --background # detached: SURVIVES the agent session;
|
|
172
|
+
# http://localhost:8140, pages render live
|
|
173
|
+
# from .geml — rebuild + F5, never stale.
|
|
174
|
+
# already-running port → reused, not stacked.
|
|
175
|
+
geml codemap serve <proj>/.geml-code-graph --stop # stop it (pid: .geml-code-graph/_index/serve.pid)
|
|
176
|
+
geml codemap render <proj>/.geml-code-graph # serverless alternative: bake .html next to
|
|
177
|
+
# each doc; open file:///…/.geml-code-graph/index.html
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Always `--background` (a viewer must not die with the session). Then open
|
|
181
|
+
it for the user: Windows `start "" <url>` (or `Start-Process <url>`),
|
|
182
|
+
macOS `open <url>`, Linux `xdg-open <url>`. Port taken by something
|
|
183
|
+
else → pick another (`--port`), open that one.
|
|
184
|
+
|
|
185
|
+
`index.html` is the module overview; clicking a module opens its page inside
|
|
186
|
+
the graph area (nested view). Method pages: click = callee chain, ⊕ on an
|
|
187
|
+
entry = full caller chain, breadcrumb walks back up.
|
|
188
|
+
|
|
189
|
+
## Keep it in sync on every commit (optional per-project hook)
|
|
190
|
+
|
|
191
|
+
With the recipe recorded (step 3), a Claude Code PostToolUse hook makes any
|
|
192
|
+
`git commit` Claude runs in that project refresh the codemap in the
|
|
193
|
+
BACKGROUND (never blocks the commit; non-commit commands exit instantly;
|
|
194
|
+
projects without `refresh.json` are silently skipped). Add to the project's
|
|
195
|
+
`.claude/settings.json`:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [
|
|
199
|
+
{ "type": "command", "command": "geml codemap refresh .geml-code-graph --hook --commit" }
|
|
200
|
+
] } ] } }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
(`.geml-code-graph` = the codemap dir relative to the project root; use an absolute
|
|
204
|
+
path if the hook cwd differs.) With `--commit`, the refreshed documents land
|
|
205
|
+
as their own follow-up commit — `chore(codemap): refresh for <sha>`, codemap
|
|
206
|
+
dir only — so the next push carries code + graph together. It is loop-safe
|
|
207
|
+
(the follow-up commit changes no source file, so the refresh it triggers
|
|
208
|
+
skips) and it stands down when HEAD moved during the refresh or a merge is in
|
|
209
|
+
progress. Drop `--commit` to keep the old behavior: refreshed files stay in
|
|
210
|
+
the working tree for you to include in a later commit.
|
|
211
|
+
|
|
212
|
+
Between commits (editing-time sync), `geml codemap serve <dir> --watch`
|
|
213
|
+
re-runs the recipe after 30s of quiet whenever an indexed source file
|
|
214
|
+
changes — pages render live, so a browser reload shows the new graph.
|
|
215
|
+
|
|
216
|
+
Add `--history [-m msg]` to build to snapshot changed documents into
|
|
217
|
+
`.gemlhistory` sidecars — then `geml history get .geml-code-graph/<doc>.geml` shows
|
|
218
|
+
the graph's evolution and `geml revert .geml-code-graph/<doc>.geml '#method' --rev -1`
|
|
219
|
+
rolls one method's edges back. Language maturity tiers and the smoke-test
|
|
220
|
+
gate: [DESIGN-geml-code-graph.md](https://github.com/geml-spec/geml/blob/main/docs/design/specs/codemap/DESIGN-geml-code-graph.md) §3.4. An MCP wrapper with the same
|
|
221
|
+
three moves exists (`geml mcp --root <dir>`, which serves them next to the
|
|
222
|
+
document tools when the root holds a graph); the CLI path works without it.
|