Docs / guides / markdown-context-graph · Edit on GitHub

Markdown Context Graph

Introduction

rgctl indexes .md and .mdx files into a documentation context graph alongside your code. Headings become navigable sections, internal links become REFERENCES edges, code fences become searchable blocks, and frontmatter keys become queryable variables — all in the same graph.snapshot.bin that powers gql, metrics, and export.

This guide walks through discover, GQL, Obsidian/OKF export, and doc-scoped semantic search. The primary example is the English docs from kubernetes/website (content/en, on the order of 17k heading sections). A small in-tree fixture covers every markdown construct and doc→code linking.

Use Cases

  • Large documentation sites. Index an entire docs tree as heading modules with CONTAINS hierarchy and REFERENCES cross-links.
  • Obsidian vault browsing. Export one note per heading section, with folder layout mirroring doc paths and wikilinks from internal links.
  • Agent-first doc navigation. Query heading structure, cross-links, and community membership with GQL instead of reading every file.
  • Natural-language section search. Build a doc-scoped semantic index over heading bodies and code blocks (semantic index --scope docs).
  • Doc + code linking. Discover markdown and Java (or other languages) together; walk doc → file → class in one query.
  • Architecture decision records. Index ADRs with heading hierarchy, file links, and section-level REFERENCES edges.

Example Projects

This guide uses two checkouts — each for a different part of the walkthrough, not as alternatives to the same task.

  1. kubernetes/website (example/k8s-website/) — Steps 1–4: discover, Obsidian export, doc semantic search, communities, and OKF on a real documentation site. Sparse-checkout content/en before you start.

  2. markdown-context fixture (tests/fixtures/markdown-context/) — Step 5 and the construct showcase: every supported markdown syntax, plus CheckoutService.java for doc→code GQL. Always in-tree; no clone.

# kubernetes/website — sparse clone of English docs (same dest as ./scripts/fetch-profile-repos.sh)
mkdir -p example
git clone --depth 1 --filter=blob:none --sparse https://github.com/kubernetes/website.git example/.tmp-k8s-website
(cd example/.tmp-k8s-website && git sparse-checkout set content/en)
rm -rf example/k8s-website
mv example/.tmp-k8s-website/content/en example/k8s-website
rm -rf example/.tmp-k8s-website

export REPO="$(pwd)/example/k8s-website"
export REPO_FIXTURE="$(pwd)/tests/fixtures/markdown-context"
export RGCTL_NO_DAEMON=1   # write artifacts to $REPO/.rgctl/ (see Installation)

Maintainers who already fetch profile corpora can skip the clone: ./scripts/fetch-profile-repos.sh then export REPO="$(pwd)/example/k8s-website".

Prerequisites: rgctl on your PATH. For large exports (17k+ Obsidian notes), use a release binary — download the latest release or build from source (cargo build --release --bin rgctl). See Installation.

Artifacts are written to {repo}/.rgctl/ next to the checkout. If you still have a legacy daemon cache under ~/.rgctl/cache/, run rgctl migrate-cache. See Installation — Migrating from daemon cache.

What rgctl indexes

rgctl parses markdown with official tree-sitter-md (block + inline grammars). The table below maps author syntax to graph nodes and edges.

Author writesGraphProperties / edges
# Heading / ## … (ATX):Modulekind=heading, level, slug, QN {path}#{slug}
Setext heading (=== / --- underline):ModuleSame as ATX (kind=heading)
Nested headings:Module + CONTAINSParent heading → child heading
Section prose under a headingon heading nodebody_text, body_hash; large → body_ref
```lang … ``` fenced code:Modulekind=code_block, language, body_text
Indented code (4 spaces):Modulekind=code_block (no language)
[text](/rgctl/docs/guides/file/) file link:Import + REFERENCESEdge to :File; to_type_hint=file
[text](/rgctl/docs/guides/file/#section) heading link:Import + REFERENCESEdge to :Module heading; to_type_hint=module
[jump](#slug) same-file fragment:Import + REFERENCESEdge to heading in same file
Reference-style links ([x][ref] + [ref]: url):Import + REFERENCESFull, collapsed, and shortcut forms
https://… / mailto: links:Import onlyLink symbol with url; no REFERENCES edge
![alt](img.png) imagesNot indexed
[[wikilink]]Not indexed
YAML frontmatter (---):Variablekind=frontmatter, flattened keys (metadata.author), value
TOML frontmatter (+++):VariableSame flattening as YAML
Links inside table cells:Import + REFERENCESParsed from pipe_table_cell inline trees
.mdx filessame as .mdStructure only — JSX in fences is not executed

Qualified names: {file_path}#{slug} where slug is ASCII-slugified heading text. Duplicate titles in one file get -2, -3, … suffixes.

Fragments are literal: [link](/rgctl/docs/guides/adr/#payments) targets adr.md#payments, not a slugified variant. Prefer slug fragments (#checkout-flow) over visible titles (#Checkout Flow).

For the full node model and GQL catalog, see markdown-context.md.

Step-by-Step

1. Discover documentation (kubernetes/website)

If you built from source, ensure target/release/rgctl is on your PATH (see Add to PATH).

export REPO="$(pwd)/example/k8s-website"
export RGCTL_NO_DAEMON=1

rgctl -r "$REPO" discover . -l markdown

Output:

[>] rgctl discover
[✓] rgctl discover finished in 1.6s

What happened:

  • rgctl parsed the sparse checkout of kubernetes/website content/en — thousands of .md files, 17,244 heading modules (:Module with kind=heading), zero :Function nodes.
  • The graph snapshot was written to $REPO/.rgctl/graph.snapshot.bin.
  • Section bodies larger than 32 KiB inline were stored in $REPO/.rgctl/content_store.bin (Blake3-keyed).

Confirm the heading count (read "count" from the JSON envelope — property projection in RETURN is not supported):

rgctl -r "$REPO" -f json gql \
  "MATCH (n:Module) WHERE n.kind = 'heading' RETURN n LIMIT 1"

The "count" field reflects the full match set (17,244 at time of writing; drifts with upstream). Add LIMIT only when you want sample rows in rows.

Fixture equivalent (same command, tiny graph — useful before iterating on GQL):

export REPO="$REPO_FIXTURE"
rgctl -r "$REPO" discover . -l markdown

Markdown is included in default discover. Use -l markdown when you want only documentation.

2. Export to Obsidian

Turn every heading section into an interlinked vault. Export reads the graph and content_store.bin — it does not re-parse source files.

export REPO="$(pwd)/example/k8s-website"
export RGCTL_NO_DAEMON=1

rgctl -r "$REPO" export \
  --export-format obsidian \
  --export-output "$REPO/vault" \
  --query all

Output:

[>] rgctl export
Exported 17244 notes (2971 wikilinks) -> …/example/k8s-website/vault
[✓] rgctl export finished in 7.0s

What happened:

  • rgctl exported 17,244 notes — one per heading module. Note count equals heading module count.
  • Folder layout mirrors doc paths (e.g. docs/concepts/…/feature.md#overview → nested vault paths). Long blog titles get truncated slugs with a stable hash suffix.
  • Outgoing REFERENCES edges became 2,971 wikilinks ([[path]], no .md suffix).
  • Each note's YAML frontmatter includes qualified_name and level for trace-back to GQL.

Open the vault: Obsidian → Open folder as vault → select $REPO/vault.

Fixture equivalent:

export REPO="$REPO_FIXTURE"
rgctl -r "$REPO" export --export-format obsidian --export-output "$REPO/vault" --query all
Exported 16 notes (7 wikilinks) -> …/markdown-context/vault

Re-export after doc edits: discover, then export. Obsidian export is read-only — vault edits are not synced back to the graph.

3. Query, communities, and semantic search

Cross-links across the corpus:

export REPO="$(pwd)/example/k8s-website"
export RGCTL_NO_DAEMON=1

rgctl -r "$REPO" -f json gql \
  "MATCH (h:Module)-[:REFERENCES]->(t) WHERE h.kind = 'heading' RETURN h, t LIMIT 20"

Communities (GQL) — community assignment is computed at discover and exposed through GQL as a virtual overlay (not stored in graph.snapshot.bin). List communities, then filter heading modules by community_id:

rgctl -r "$REPO" -f json gql --macro-name all_communities unused

# Pick a community_id from the output (example below — yours will differ)
rgctl -r "$REPO" -f json gql \
  "MATCH (n:Module) WHERE n.kind = 'heading' AND n.community_id = '60994' RETURN n LIMIT 20"

On markdown-only graphs, community detection uses REFERENCES cross-links and heading CONTAINS trees. See Graph Query Language and Community Detection.

PageRank (metrics, not GQL) — centrality scores are not GQL node properties. Use the metrics command after discover:

rgctl -r "$REPO" -f json metrics --pagerank

The top array lists node UUIDs and scores. For named hotspots, combine with GQL on structure (REFERENCES, CONTAINS) or communities above.

Doc-scoped semantic index (separate from discover; uses semantic_index.bin):

rgctl -r "$REPO" semantic index --scope docs --embedder hash

Output:

[>] rgctl semantic index
Indexed 24608 functions (sign-hash-v1, 256 dims) → …/k8s-website/.rgctl/semantic_index.bin
  incremental: 0 reused, 24608 embedded, 0 removed
[✓] rgctl semantic index finished in 2.0s

The CLI still prints functions — the count is index entries (heading + code-block modules when --scope docs).

rgctl -r "$REPO" -f json semantic query "pod scheduling" --scope docs --limit 10

What happened:

  • --scope docs embedded :Module nodes with kind=heading and kind=code_block.
  • Embeddings use inline body_text or full UTF-8 from content_store.bin when body_ref is set.
  • Query --scope docs does not filter hits — build the index with the scope you need. Re-run semantic index after large doc edits.

4. Export OKF JSON

export REPO="$(pwd)/example/k8s-website"
export RGCTL_NO_DAEMON=1

rgctl -r "$REPO" export \
  --export-format okf \
  --export-output "$REPO/okf.json" \
  --query all

Output:

[>] rgctl export
Exported 17244 OKF entities -> …/example/k8s-website/okf.json
[✓] rgctl export finished in 466ms

Use --query all for doc exports (filter queries target code-graph subsets). The fixture run is identical with REPO="$REPO_FIXTURE" (16 entities).

5. Feature corpus (markdown-context fixture)

Switch to the in-tree fixture for every markdown construct and doc→code queries. Layout:

markdown-context/
  README.md              ← YAML frontmatter
  docs/
    guide.md             ← headings, links, fenced code
    adr.md               ← tables, cross-links
    overview.mdx         ← `.mdx`
  src/
    CheckoutService.java ← doc→class anchor
export REPO="$REPO_FIXTURE"
rgctl -r "$REPO" discover . -l markdown,java   # docs + code

GQL on the fixture (LIKE = prefix/suffix globs only):

# Checkout-related headings
rgctl -r "$REPO" -f json gql \
  "MATCH (n:Module) WHERE n.kind = 'heading' AND n.name LIKE 'Checkout*' RETURN n LIMIT 10"

# Heading tree
rgctl -r "$REPO" -f json gql \
  "MATCH (h:Module)-[:CONTAINS*1..3]->(n:Module) \
   WHERE h.kind = 'heading' AND h.name LIKE 'Checkout*' AND n.kind = 'heading' \
   RETURN h, n"

# Cross-doc link (guide → payments ADR)
rgctl -r "$REPO" -f json gql \
  "MATCH (h:Module)-[:REFERENCES]->(t:Module) \
   WHERE h.kind = 'heading' AND t.name = 'Payments' RETURN h, t"

# Doc → Java class (needs markdown,java discover)
rgctl -r "$REPO" -f json gql \
  "MATCH (h:Module)-[:REFERENCES]->(f:File)-[:CONTAINS]->(c:Class) \
   WHERE h.name LIKE 'Checkout*' AND f.name LIKE '*CheckoutService.java' \
   RETURN h, f, c"

# Section prose — compact GQL returns bindings; use Obsidian export or semantic query for full text
rgctl -r "$REPO" -f json gql \
  "MATCH (n:Module) WHERE n.kind = 'heading' AND n.name = 'Checkout Flow' RETURN n LIMIT 1"

Markdown showcase (fixture)

After discover on $REPO_FIXTURE, these checks confirm each supported construct.

Headings and hierarchy

docs/guide.md — ATX headings (# Checkout Flow, ## Cart, ### Validation rules). Nested CONTAINS: Checkout Flow → Cart → Validation rules.

rgctl -r "$REPO_FIXTURE" -f json gql \
  "MATCH (a:Module)-[:CONTAINS]->(b:Module) \
   WHERE a.kind = 'heading' AND b.kind = 'heading' RETURN a, b LIMIT 20"

Internal links

docs/guide.md#checkout-flow links to ./adr.md#payments, ./adr.md, and ../src/CheckoutService.java. External URLs (Stripe API) get link symbols but no REFERENCES edge.

rgctl -r "$REPO_FIXTURE" -f json gql \
  "MATCH (h:Module)-[:REFERENCES]->(t) \
   WHERE h.qualified_name LIKE '*#checkout-flow' RETURN h, t"

Fenced and indented code

```java
cart.validate();
```
rgctl -r "$REPO_FIXTURE" -f json gql \
  "MATCH (n:Module) WHERE n.kind = 'code_block' RETURN n LIMIT 5"

Frontmatter, tables, MDX

  • README.md — YAML frontmatter → :Variable with flattened keys (metadata.author)
  • docs/adr.md — pipe table with internal link in a cell
  • docs/overview.mdx — same plugin as .md; JSX in fences is not executed
rgctl -r "$REPO_FIXTURE" -f json gql \
  "MATCH (v:Variable) WHERE v.kind = 'frontmatter' RETURN v LIMIT 10"

Author linking cheat sheet

Author writesResolves toIndexed?
./adr.mdFile docs/adr.mdYes — REFERENCES:File
./adr.md#paymentsModule docs/adr.md#paymentsYes — REFERENCES:Module
#checkout-flowSame-file heading slugYes
#Checkout FlowLiteral fragment (often a stub)Avoid — use slug
../src/Foo.javaFile nodeYes (if file is in discover set)
https://…Link symbol only; no edge
![alt](img.png)Ignored
[[Wiki]]Ignored

If a linked file is not in the discover set, the REFERENCES edge is dropped.

What is not supported

FeatureNotes
CFG / PDG / slice / inspect / cpg flows on .mdNo CFG grammar; commands reject markup paths
MDX component executionStructure only
Images and wikilinksSkipped
External URL graph edgeshttps:// not REFERENCES
blast-radius on doc nodesCalls-only; use GQL for docs
Obsidian → source syncExport is one-way

Profile gates (maintainers)

Cold discover and warm Obsidian export on kubernetes/website are gated in ignored tests (example/k8s-website in the repo's internal profile layout). These use ceiling baselines (not typical laptop timings):

cargo build --release --bin rgctl
cargo test --release --test cold_profile_gates k8s_website_markdown_cold_discover_within_baseline -- --ignored --nocapture
cargo test --release --test cold_profile_gates k8s_website_obsidian_export_to_vault -- --ignored --nocapture

Ceilings: cold discover 3.0s wall, warm Obsidian export 30.0s wall (+10% tolerance). Details: markdown-context.md § Cold profile.

Export formats for documentation

FormatOutputBest for
obsidianDirectory of .md notesHuman browsing in Obsidian
okfSingle JSON entity bundleOKF / knowledge-platform tooling

Code-graph formats (json, graphml, graphviz, mermaid) also include doc nodes when present. See Exporting Graphs.

Benefits

  • Real-site corpus. kubernetes/website content/en is the profile fixture — 17k+ heading modules with real cross-links.
  • One graph for docs and code. ADRs, guides, and services share graph.snapshot.bin when discovered together.
  • Low-token agent queries. GQL returns compact bindings (name, qualified_name, file) without opening whole files.
  • Human-friendly export. Obsidian vaults mirror heading hierarchy with wikilinks.
  • Large-corpus bodies. content_store.bin holds section text beyond the 32 KiB inline cap.

Related Guides