Skip to main content

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.

version: 0.1.0 license: MIT

InstallationHow it worksThe CLIScopeRun an exampleRoadmap


[!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 (sync verb with .lg2m.lock baseline). The CLI ships check, validate, list, init, gen, and sync. Install from PyPI or a GitHub tag — see Installation. The annotated example checks clean under lg2m check; the plain native example (below) runs today. The design lives in docs/design.md and the competitive landscape in PRIOR-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:

  1. Topology (introspection). The real nodes, edges, conditional flags, path_map targets, and the state schema with its reducers, read from compiled.get_graph(xray=True). This is the source of truth for graph shape.
  2. Annotations. @node, @predicate, lg2m.router, @state_model, and @data_model link each symbol to the diagram and the Markdown. They record metadata and return their target unchanged, so they do not alter runtime behavior.
  3. The Markdown contract. A purely topological Mermaid stateDiagram-v2, plus tables, hidden fences, and notes for the facts a diagram cannot draw (reducers, Command destinations, Send width).

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:

  1. Done. IR, config loader, and the Markdown / Mermaid parsers (no framework import).
  2. Done. Annotations and the router, the diff engine, and the reports; reconciliation runs against a fake introspector and fixtures.
  3. Done. The LangGraph introspector behind the [langgraph] extra, and a runnable check.
  4. Done. The Typer CLI (check, validate, list, init, gen).
  5. Done. gen --from-doc / --from-code with round-trip golden tests (LangGraph emission).
  6. Done. sync verb: prose write-back with a .lg2m.lock baseline-hash store, 3-way merge, conflict detection, --prefer code|doc resolution, 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.


lg2m: one graph, one diagram, no drift.

DesignPrior artExamples

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

langgraph_to_from_mermaid-0.1.0.tar.gz (360.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langgraph_to_from_mermaid-0.1.0-py3-none-any.whl (66.8 kB view details)

Uploaded Python 3

File details

Details for the file langgraph_to_from_mermaid-0.1.0.tar.gz.

File metadata

File hashes

Hashes for langgraph_to_from_mermaid-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f388b17ef677480d4c6560eee1b3ce105a9fdf234a25f0df70d74fa5845b957a
MD5 e0b04d9a82c9f60ac976a88dc0fb8aaa
BLAKE2b-256 c2f1fa438af8fa9aef7e9a734ba8ac6f5ec5db60fe2057291996b1a0211d97ac

See more details on using hashes here.

File details

Details for the file langgraph_to_from_mermaid-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for langgraph_to_from_mermaid-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b5625453399f970441457b8f5ee22326022c2a695830d54277859e14af562ea
MD5 dd23f7768a474c728a0d0cec57b0e046
BLAKE2b-256 f5d4d19c85e0a6c3478cd9c926b2c1f73b2d094e13b1ef8ae1b296012746cfff

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page