Bidirectional, drift-detecting sync between a LangGraph/LangChain graph's topology and a Mermaid stateDiagram-v2 contract.
Project description
langgraph to-from mermaid (lg2m)
Keep your LangGraph or LangChain graph and its Mermaid diagram in sync, both ways, and fail the build when they drift.
Installation • How it works • The CLI • Scope • Run an example • Roadmap
[!NOTE] v0.1.0. The package is built and green end-to-end: the intermediate representation, the config loader, the Markdown / Mermaid parsers, the annotation decorators and
router, the diff and report engine, the real LangGraph introspector, the Typer CLI, code/contract generation (scaffold/+gen), and prose sync (syncverb with.lg2m.lockbaseline). The CLI shipscheck,validate,list,init,gen, andsync. Install from PyPI or a GitHub tag — see Installation. The annotated example checks clean underlg2m check; the plain native example (below) runs today. The design lives indocs/design.mdand the competitive landscape inPRIOR-ART.md.
lg2m (langgraph_to_from_mermaid) is a Python package and CLI that
treats a Mermaid stateDiagram-v2, written in Markdown, as a checkable contract
for a LangGraph or LangChain graph. This high fidelity app reads the real compiled
graph for topology truth, reads code annotations that link each symbol to the
diagram, and reports drift in either direction with file and line locations, or
scaffolds one side from the other.
A diagram drifts from the code the moment either changes. Tools that draw a graph
from code are one-way; builders that generate code from a diagram throw the
diagram away afterward. lg2m keeps the two honest over time and makes
disagreement a build failure. See PRIOR-ART.md for where it sits
among existing tools.
Installation
From PyPI:
pip install langgraph-to-from-mermaid
Add the [langgraph] extra to enable lg2m check and the real introspector:
pip install "langgraph-to-from-mermaid[langgraph]"
From a GitHub release tag (no PyPI needed):
pip install "git+https://github.com/psenger/langgraph_to_from_mermaid.git@vX.Y.Z"
# with the langgraph extra:
pip install "langgraph-to-from-mermaid[langgraph] @ git+https://github.com/psenger/langgraph_to_from_mermaid.git@vX.Y.Z"
Replace vX.Y.Z with the tag you want (e.g. v0.1.0). Tags are listed on the
GitHub Releases page.
From a clone (for development or to track main):
git clone https://github.com/psenger/langgraph_to_from_mermaid.git
cd langgraph_to_from_mermaid
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" # foundation layer only
# pip install -e ".[langgraph,dev]" # add the introspector
Once installed, the lg2m command is on your PATH.
How it works
lg2m check reconciles three sources and exits non-zero when they disagree:
- Topology (introspection). The real nodes, edges, conditional flags,
path_maptargets, and the state schema with its reducers, read fromcompiled.get_graph(xray=True). This is the source of truth for graph shape. - Annotations.
@node,@predicate,lg2m.router,@state_model, and@data_modellink each symbol to the diagram and the Markdown. They record metadata and return their target unchanged, so they do not alter runtime behavior. - The Markdown contract. A purely topological Mermaid
stateDiagram-v2, plus tables, hidden fences, and notes for the facts a diagram cannot draw (reducers,Commanddestinations,Sendwidth).
Routing cannot drift
Conditional routing is the part most likely to rot, so lg2m owns it. You declare
a fan-out as an ordered mapping of named predicates ending in a required [else]
default; lg2m generates the router and the path_map from that one mapping.
from lg2m import predicate, router, ELSE
@predicate("should_escalate")
def should_escalate(state) -> bool: # a whole leaf condition; you write this
f = state["flags"]
return (f.get("urgent") or f.get("vip")) and not f.get("resolved")
# the fan-out as an ordered mapping; lg2m builds the selector and owns the path_map
route_after_classify = router("classify_intent", [
("should_escalate", "escalate_to_human"),
("should_auto_resolve", "auto_resolve"),
(ELSE, "investigate"), # required default
])
Because the diagram labels, the runtime selector, and the path_map all come from
the same mapping, they cannot disagree. The only independently authored surface
left is the Markdown, which is exactly what check reconciles.
What the contract looks like
The diagram is plain Mermaid that renders on GitHub. The conditional edges are
labelled with the predicate names, and the default branch is labelled [else]:
stateDiagram-v2
[*] --> ingest_ticket
state fork_enrich <<fork>>
state join_enrich <<join>>
ingest_ticket --> fork_enrich
fork_enrich --> fetch_history
fork_enrich --> lookup_account
fetch_history --> join_enrich
lookup_account --> join_enrich
join_enrich --> classify_intent
classify_intent --> escalate_to_human: should_escalate
classify_intent --> auto_resolve: should_auto_resolve
classify_intent --> investigate: [else]
state investigate {
[*] --> gather_logs
gather_logs --> analyze
analyze --> [*]
}
investigate --> map_items
map_items --> process_item
process_item --> reduce_items
reduce_items --> compose_reply
auto_resolve --> compose_reply
escalate_to_human --> compose_reply
compose_reply --> [*]
A clean run reports each source as in agreement (illustrative output; run
lg2m check for the real report):
lg2m check
graph: support_pipeline (support_pipeline.graph:build_graph -> CompiledStateGraph)
introspected: 11 nodes, 15 edges (5 conditional), state PipelineState (8 fields)
nodes .......... 11/11 OK (@node ids == graph nodes == diagram states)
routing ........ OK (router mapping == path_map == get_graph labels == diagram)
reducers ....... 4/4 OK (add_messages, operator.add x2, extend_unique)
diagram ........ OK
diagnostics .... none
0 drift items. exit 0
The full annotated source and contract are in
examples/support_pipeline/.
The CLI
The CLI ships today (see docs/design.md Section 11). Configuration lives in an
lg2m.toml that maps a graph id to its entry point and Markdown contract.
| command | what it does |
|---|---|
init |
scaffold a starting lg2m.toml |
list |
list the configured graphs |
validate |
each side parses, the entry point imports, every fan-out has an [else] |
check |
reconcile topology, annotations, and the diagram; non-zero on drift |
gen --from-doc |
scaffold annotated code from the Markdown contract |
gen --from-code |
scaffold the Markdown contract from code plus annotations |
sync |
write prose back across the code/doc boundary using a .lg2m.lock baseline |
Exit codes: 0 clean, 1 drift or structural error, 2 usage or config error.
gen emits LangGraph today; LangChain emission is on the roadmap (the routing model
compiles to both). gen writes only where asked: --out writes files and refuses to
overwrite, and without it the output is a stdout dry-run. sync supports --prefer code|doc conflict resolution and --dry-run.
LangGraph and LangChain scope
Full bidirectional fidelity is for LangGraph. For LangChain, lg2m covers
the LCEL-expressible slice (linear chains plus RunnableBranch). The routing model
is portable to both frameworks; the wider graph (parallel fan-out with reducers,
Send map-reduce, Command(goto), subgraphs) has no LCEL equivalent and is
LangGraph-only. The examples include a per-construct breakdown of
what LangChain can and cannot express.
Try the runnable example
The plain native example runs today. It is the same graph in both frameworks, with deterministic node bodies (no LLM calls, no API keys), so runs are reproducible.
cd examples/support_pipeline_native
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python langgraph_app.py # the full graph
python langchain_app.py # the slice LCEL can express
python introspect.py # the topology lg2m would read via get_graph(xray=True)
examples/support_pipeline/ is the same graph with
lg2m applied (annotations plus the Mermaid and Markdown contract); run lg2m check
against it for a clean reconcile.
Status and roadmap
v0.1.0 — all layers shipped and green. The build order from docs/design.md
Section 13 is complete:
- Done. IR, config loader, and the Markdown / Mermaid parsers (no framework import).
- Done. Annotations and the
router, the diff engine, and the reports; reconciliation runs against a fake introspector and fixtures. - Done. The LangGraph introspector behind the
[langgraph]extra, and a runnablecheck. - Done. The Typer CLI (
check,validate,list,init,gen). - Done.
gen --from-doc/--from-codewith round-trip golden tests (LangGraph emission). - Done.
syncverb: prose write-back with a.lg2m.lockbaseline-hash store, 3-way merge, conflict detection,--prefer code|docresolution, and--dry-run.
Next: LangChain code emission, full subgraph / Send / Command round-trip fidelity, and a
version-matrix CI across LangGraph / langchain-core releases.
Repository layout
.
docs/design.md the design
PRIOR-ART.md competitive landscape and the novelty claim
src/lg2m/ the package: IR, parsers, annotations, router, introspector,
diff, report, CLI, and scaffold/ (gen)
tests/ the pytest suite (framework-free, plus @langgraph-gated)
docs/prose-sync.md design notes that preceded the sync/ implementation
examples/
support_pipeline_native/ runnable: the graph in LangGraph + LangChain, before lg2m
support_pipeline/ the same graph annotated, with its Mermaid/Markdown contract (checks clean)
Prior art
lg2m's novelty is the combination of bidirectional topology sync,
drift-as-a-build-contract, and LangGraph awareness. Every individual capability
exists in isolation elsewhere; the full landscape and what would falsify the claim
are in PRIOR-ART.md.
Contributing
The design is settled in docs/design.md, and the "Next" items above are the natural
first-issues list. Start by opening a bug or feature issue, then cut a branch from main
and open a pull request back to main. The framework-free pieces (LangChain emission in
scaffold/, the prose sync verb) are a good place to start. See
CONTRIBUTING.md for the full workflow, local setup, and the release process.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langgraph_to_from_mermaid-0.1.0.tar.gz.
File metadata
- Download URL: langgraph_to_from_mermaid-0.1.0.tar.gz
- Upload date:
- Size: 360.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f388b17ef677480d4c6560eee1b3ce105a9fdf234a25f0df70d74fa5845b957a
|
|
| MD5 |
e0b04d9a82c9f60ac976a88dc0fb8aaa
|
|
| BLAKE2b-256 |
c2f1fa438af8fa9aef7e9a734ba8ac6f5ec5db60fe2057291996b1a0211d97ac
|
File details
Details for the file langgraph_to_from_mermaid-0.1.0-py3-none-any.whl.
File metadata
- Download URL: langgraph_to_from_mermaid-0.1.0-py3-none-any.whl
- Upload date:
- Size: 66.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b5625453399f970441457b8f5ee22326022c2a695830d54277859e14af562ea
|
|
| MD5 |
dd23f7768a474c728a0d0cec57b0e046
|
|
| BLAKE2b-256 |
f5d4d19c85e0a6c3478cd9c926b2c1f73b2d094e13b1ef8ae1b296012746cfff
|