@geml/dsh-plugin 1.0.2 → 1.0.4

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.
@@ -0,0 +1,116 @@
1
+ # Writing to a Markdown file with `geml set`
2
+
3
+ `geml set` and `geml add` edit one block of a `.md` in place. Nothing is
4
+ converted: the file stays the Markdown it was, and a reader who does not have
5
+ `geml` sees no trace of it.
6
+
7
+ Everything below was measured against real documents, not read off the help
8
+ text. Two of the rules are **silent** when broken — no error, exit code 0 — and
9
+ they are the reason to read this before the first write rather than after.
10
+
11
+ ## What holds
12
+
13
+ **A write is surgical.** After `geml set <file> '#id' --body --in -`, the YAML
14
+ frontmatter and every block other than `#id` are byte-for-byte what they were.
15
+ The verb splices; it does not re-serialize the file.
16
+
17
+ **The body lands verbatim.** Whatever you write goes in as typed — a
18
+ `> [!tip]` callout, a `[[wikilink]]`, a `` ```dataview `` fence, a table. No
19
+ escaping, no reflowing, no normalization of your Markdown to anyone else's
20
+ taste.
21
+
22
+ **A broken result is refused.** The file is re-parsed before the write lands;
23
+ if the result would not parse, nothing is written.
24
+
25
+ **Heading addresses are stable.** `## Entities` is `#entities` on every run and
26
+ every platform, and it survives anything happening above it. That is the whole
27
+ reason to address a file this way instead of by line number.
28
+
29
+ One exception, and it is GitHub's too: **a repeated heading is numbered by
30
+ position.** The second `## Added` is `#added-1`, the third `#added-2` — the same
31
+ anchors GitHub gives them — so a Keep-a-Changelog file is writable section by
32
+ section. But a new `## Added` inserted above one shifts it: `#added-1` becomes
33
+ `#added-2`. Take a repeated heading's address from a fresh `geml list`, never
34
+ from memory.
35
+
36
+ **The file is read as Markdown, not as GEML.** Where the two disagree, a `.md`
37
+ gets Markdown's reading:
38
+
39
+ - `[[Note]]`, `[[Note#Heading]]`, `[[Note#Heading|alias]]`, `[[Note#^block]]`
40
+ and `![[Note#Heading]]` are Obsidian links. The note is found by name
41
+ anywhere under the resolution root, `.md` implied; one that does not exist
42
+ yet is a **warning**, never a refusal — a vault plans notes that way.
43
+ Within the page, `[[#Heading Text]]` may name a heading by its text.
44
+ - `[^label]` is a GFM footnote. With a `[^label]:` line it is a footnote;
45
+ without one it is plain text — so `[^0-9]` in a sentence about a regex is
46
+ nothing to worry about.
47
+ - `{{title}}` is text: a template engine's placeholder, not a reference.
48
+ - `~~~` fences and indented code blocks are code, like ``` ones. Nothing inside
49
+ is a link, a heading or a footnote.
50
+
51
+ The resolution root is the file's own directory unless you pass `--root`. A
52
+ note in a subfolder that links across its vault needs `--root <vault>`, and
53
+ `geml check` says so when it sees the vault's `.obsidian/` above you. It never
54
+ looks above the root on its own.
55
+
56
+ ## The three that bite
57
+
58
+ ### 1. Never `set` the frontmatter block — it destroys the frontmatter
59
+
60
+ Frontmatter is not a typed block in Markdown; it is an anonymous prose block,
61
+ and its **closing `---` is part of that block's body**. Replace the body and the
62
+ closer is gone, leaving an opener with nothing to close it — no properties at
63
+ all, for every tool that reads them.
64
+
65
+ Change frontmatter with an ordinary editor. This is silent: exit code 0.
66
+
67
+ ### 2. `--body` is for a heading. On a prose block it APPENDS
68
+
69
+ A heading's block is a heading line plus a body. A prose block is body all the
70
+ way down, so it has no separate body to set — `get '#x' --body` on one comes
71
+ back empty, and `set --body` writes into that emptiness, which lands **after**
72
+ the prose already there. Nothing is removed.
73
+
74
+ | target | `set '#id'` | `set '#id' --body` |
75
+ |---|---|---|
76
+ | a heading block | refused: *content is prose, not a block — use --body* | replaces the section body, keeps the heading line |
77
+ | a prose block | replaces it | **appends to it, silently** |
78
+
79
+ The rule is the opposite of what one habit would give you, and picking wrong
80
+ fails loudly one way and quietly the other. `geml list` prints the kind in its
81
+ second column — read it before choosing. This is the other silent one.
82
+
83
+ ### 3. `--in <file>` is not "read this text"
84
+
85
+ `--in F` means *take block `#id` from file F*. Raw text goes in on **stdin**:
86
+
87
+ ```sh
88
+ printf '…' | geml set page.md '#id' --body --in - # right
89
+ geml set page.md '#id' --body --in fragment.txt # looks for #id INSIDE fragment.txt
90
+ ```
91
+
92
+ Loud, at least: when F has no such block the refusal names the stdin form.
93
+
94
+ ## Smaller things worth knowing
95
+
96
+ - `find` is a **literal substring**, not a pattern. `Hot.Cache` does not match
97
+ "Hot Cache". Case-insensitive unless `--case`.
98
+ - `find --head` shows **one line per block**. A block with thirteen matches
99
+ reports one; `find` locates, `get` reads.
100
+ - `set --body` replaces a trailing `---` rule too, if the section ends with one.
101
+ - The blank line after a heading is not re-inserted. Cosmetic; renderers do not
102
+ care, a diff does.
103
+ - A directory walk skips hidden directories. A tree that hides sources in
104
+ `.raw/` must name that directory: `geml find 'x' notes .raw`.
105
+ - `@…` addresses are content hashes and change when the content does. Fine to
106
+ read from a fresh `list`; never store one, and never write to one.
107
+
108
+ Before a first write to a page, `geml check <file>` — exit 0 means every block is
109
+ writable; warnings (a note not yet written) never block one.
110
+
111
+ ## Do not convert the file
112
+
113
+ `geml <page>.md --to geml` and back is lossy for anything beyond plain
114
+ Markdown — frontmatter lists collapse, `[[…]]` comes back escaped, thematic
115
+ breaks are dropped. The point of addressing a Markdown file is that it **stays
116
+ Markdown**.
@@ -1,222 +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 exists (`geml mcp --root <dir>`, which serves the four
221
- read-only `geml_codemap_*` tools next to the document tools when the root holds
222
- a graph); the CLI path works without it.
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/spec/profiles/geml-codemap/geml-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/geml-codemap/DESIGN-geml-code-graph.md) §3.4. An MCP wrapper exists (`geml mcp --root <dir>`, which serves the four
221
+ read-only `geml_codemap_*` tools next to the document tools when the root holds
222
+ a graph); the CLI path works without it.