langgraph-lint 0.1.0__tar.gz

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,102 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # uv's managed interpreters are "externally managed" and reject --system, so
9
+ # every job installs into an explicit virtualenv that later steps reuse.
10
+ env:
11
+ UV_PROJECT_ENVIRONMENT: .venv
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12", "3.13"]
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: astral-sh/setup-uv@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Activate the uv-managed virtualenv
27
+ run: |
28
+ uv venv .venv --allow-existing
29
+ echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
30
+ echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
31
+
32
+ # The base install must stay dependency-free, so prove the package and
33
+ # its CLI work before any optional extra -- including langgraph itself --
34
+ # is present.
35
+ - name: Install base package only
36
+ run: uv pip install -e .
37
+ - name: Verify base install pulls in no LangChain or LangGraph
38
+ run: |
39
+ python -c "import langgraph_lint; print(langgraph_lint.__version__)"
40
+ langgraph-lint --version
41
+ python - <<'PY'
42
+ for name in ("langchain_core", "langgraph"):
43
+ try:
44
+ __import__(name)
45
+ except ImportError:
46
+ print(f"confirmed: base install pulls in no {name}")
47
+ else:
48
+ raise SystemExit(f"base install must not depend on {name}")
49
+ PY
50
+
51
+ # Smoke test the CLI end to end against a graph with a real, deliberate
52
+ # LG003 bug, using only the base install plus langgraph itself (which a
53
+ # real user would already have -- this simulates their actual setup).
54
+ - name: Install langgraph for the smoke test
55
+ run: uv pip install langgraph
56
+ - name: CLI finds the deliberate bug in the fixture graph
57
+ run: |
58
+ cd tests/fixtures
59
+ if langgraph-lint buggy_graph:app --min-severity high; then
60
+ echo "expected langgraph-lint to find the LG003 bug and exit 1" >&2
61
+ exit 1
62
+ fi
63
+ echo "confirmed: CLI found the bug and exited non-zero"
64
+ - name: CLI is silent on a clean graph
65
+ run: |
66
+ cd tests/fixtures
67
+ langgraph-lint clean_graph:app
68
+
69
+ - name: Install dev and agent extras
70
+ run: uv pip install -e ".[dev,agent]"
71
+ - name: Lint
72
+ run: ruff check .
73
+ - name: Type check
74
+ run: mypy src
75
+ - name: Test
76
+ run: pytest -q
77
+
78
+ build:
79
+ runs-on: ubuntu-latest
80
+ steps:
81
+ - uses: actions/checkout@v4
82
+ - uses: astral-sh/setup-uv@v5
83
+ with:
84
+ python-version: "3.12"
85
+ - name: Activate the uv-managed virtualenv
86
+ run: |
87
+ uv venv .venv --allow-existing
88
+ echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
89
+ echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
90
+ - run: uv pip install build twine
91
+ - run: python -m build
92
+ - name: Validate distribution metadata
93
+ run: twine check --strict dist/*
94
+ - name: Install the built wheel in a clean environment
95
+ run: |
96
+ uv venv /tmp/fresh
97
+ uv pip install --python /tmp/fresh/bin/python dist/*.whl
98
+ /tmp/fresh/bin/langgraph-lint --version
99
+ - uses: actions/upload-artifact@v4
100
+ with:
101
+ name: dist
102
+ path: dist/
@@ -0,0 +1,71 @@
1
+ name: Publish
2
+
3
+ # Trusted Publishing (OIDC) -- no API token is ever stored in GitHub secrets.
4
+ # One-time setup at https://pypi.org/manage/account/publishing/ :
5
+ # PyPI project: langgraph-lint | owner: mathewOracle | repo: langgraph-lint
6
+ # workflow: publish.yml | environment: pypi
7
+ # Repeat at https://test.pypi.org/manage/account/publishing/ for TestPyPI.
8
+
9
+ on:
10
+ release:
11
+ types: [published]
12
+ workflow_dispatch:
13
+ inputs:
14
+ target:
15
+ description: Where to publish
16
+ required: true
17
+ default: testpypi
18
+ type: choice
19
+ options: [testpypi, pypi]
20
+
21
+ jobs:
22
+ build:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: astral-sh/setup-uv@v5
27
+ with:
28
+ python-version: "3.12"
29
+ # uv's managed interpreter rejects --system, so build inside a venv.
30
+ - name: Activate the uv-managed virtualenv
31
+ run: |
32
+ uv venv .venv --allow-existing
33
+ echo "$PWD/.venv/bin" >> "$GITHUB_PATH"
34
+ echo "VIRTUAL_ENV=$PWD/.venv" >> "$GITHUB_ENV"
35
+ - run: uv pip install build twine
36
+ - run: python -m build
37
+ - run: twine check --strict dist/*
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ testpypi:
44
+ needs: build
45
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
46
+ runs-on: ubuntu-latest
47
+ environment: testpypi
48
+ permissions:
49
+ id-token: write
50
+ steps:
51
+ - uses: actions/download-artifact@v4
52
+ with:
53
+ name: dist
54
+ path: dist/
55
+ - uses: pypa/gh-action-pypi-publish@release/v1
56
+ with:
57
+ repository-url: https://test.pypi.org/legacy/
58
+
59
+ pypi:
60
+ needs: build
61
+ if: github.event_name == 'release' || inputs.target == 'pypi'
62
+ runs-on: ubuntu-latest
63
+ environment: pypi
64
+ permissions:
65
+ id-token: write
66
+ steps:
67
+ - uses: actions/download-artifact@v4
68
+ with:
69
+ name: dist
70
+ path: dist/
71
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ env/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .DS_Store
16
+ .idea/
17
+ .vscode/
18
+ *.log
19
+ .env
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - `LG000` — graph fails to `compile()` at all.
8
+ - `LG001` — a node with no path from `START` (dead code). Downgraded to
9
+ informational when the node is referenced by a `Send(...)` call, to avoid
10
+ false-positiving on map-reduce fan-out.
11
+ - `LG003` — a conditional router's declared return type includes a value
12
+ missing from its `path_map`. This is the flagship rule: LangGraph compiles
13
+ this without error and it only crashes, with `KeyError`, on whichever input
14
+ first takes the untested branch. Verified against a real reproduction.
15
+ - `LG005` — the mirror image of `LG003`: a `path_map` branch the router's own
16
+ return type can never produce (dead routing code).
17
+ - CLI (`langgraph-lint module.path:attribute`), library API (`lint()`), and
18
+ an optional LangChain/LangGraph agent tool (`langgraph-lint[agent]`).
19
+ - Zero required dependencies, verified in a clean virtualenv.
20
+
21
+ Two rules considered for this release — "no path to `END`" and "duplicate
22
+ edge" — were cut before shipping. Both turned out to be structurally
23
+ undetectable: LangGraph auto-inserts an implicit edge to `END` on nearly
24
+ every node (confirmed even inside a genuine unbreakable cycle), and plain
25
+ edges are deduplicated into a `set` before `get_graph()` ever sees them.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mathewOracle
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.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.5
2
+ Name: langgraph-lint
3
+ Version: 0.1.0
4
+ Summary: Find LangGraph wiring bugs before they become a runtime KeyError.
5
+ Project-URL: Homepage, https://github.com/mathewOracle/langgraph-lint
6
+ Project-URL: Repository, https://github.com/mathewOracle/langgraph-lint
7
+ Project-URL: Issues, https://github.com/mathewOracle/langgraph-lint/issues
8
+ Author: mathewOracle
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,ai,code-quality,graph,langchain,langgraph,linter,static-analysis
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Classifier: Topic :: Software Development :: Testing
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Provides-Extra: agent
24
+ Requires-Dist: langchain-core>=0.3; extra == 'agent'
25
+ Requires-Dist: langchain>=1.0; extra == 'agent'
26
+ Requires-Dist: langgraph>=0.2; extra == 'agent'
27
+ Provides-Extra: dev
28
+ Requires-Dist: build>=1.2; extra == 'dev'
29
+ Requires-Dist: langgraph>=0.2; extra == 'dev'
30
+ Requires-Dist: mypy>=1.11; extra == 'dev'
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # langgraph-lint
36
+
37
+ **Find LangGraph wiring bugs before they become a runtime `KeyError`.**
38
+
39
+ LangGraph validates some things at `compile()` time — an edge to a node that doesn't exist, for instance, fails immediately and loudly. But a lot of real wiring bugs compile perfectly fine and only surface the first time a specific input takes a specific untested branch, weeks later, in production.
40
+
41
+ ```console
42
+ $ langgraph-lint myapp.graph:app
43
+
44
+ [HIGH] LG003 (route_after_search): router can return 'maybe', which has no
45
+ matching entry in the path_map (['no', 'yes']) -- this raises KeyError at
46
+ runtime the first time this branch is taken
47
+
48
+ [HIGH] LG001 (validate_output): 'validate_output' has no incoming edge and
49
+ no path from START; it can never run
50
+
51
+ 2 finding(s) in myapp.graph:app.
52
+ ```
53
+
54
+ That first finding is real, reproducible, and proven — not a guess:
55
+
56
+ ```python
57
+ def router(state) -> Literal["yes", "no", "maybe"]:
58
+ ...
59
+
60
+ graph.add_conditional_edges("a", router, {"yes": "b", "no": "c"}) # no "maybe"!
61
+
62
+ compiled = graph.compile() # succeeds. no warning. nothing.
63
+ compiled.invoke({"x": 2}) # fine
64
+ compiled.invoke({"x": 3}) # fine
65
+ compiled.invoke({"x": 101}) # KeyError: 'maybe' <- only now
66
+ ```
67
+
68
+ `langgraph-lint` catches this the moment you write it, by checking the router's own `Literal[...]` return type against the `path_map` you gave `add_conditional_edges`. No invocation needed.
69
+
70
+ ## Install
71
+
72
+ ```console
73
+ pip install langgraph-lint # zero dependencies
74
+ pip install "langgraph-lint[agent]" # + a LangChain/LangGraph agent tool
75
+ ```
76
+
77
+ ## Use it as a CLI
78
+
79
+ ```console
80
+ langgraph-lint myapp.graph:app # module.path:attribute, like uvicorn
81
+ langgraph-lint myapp.graph:app --min-severity high
82
+ langgraph-lint myapp.graph:app --format json
83
+ langgraph-lint myapp.graph:app --format github # Actions annotations
84
+ ```
85
+
86
+ `target` accepts a compiled graph, an uncompiled `StateGraph` builder, or any object with `.get_graph()` — so it also works on plain LangChain LCEL runnables, just with a smaller rule set (the conditional-branch check is LangGraph-specific). Exit code `1` on findings, `0` clean, `2` on error.
87
+
88
+ ## Use it as a library
89
+
90
+ ```python
91
+ from langgraph_lint import lint
92
+
93
+ for finding in lint(app):
94
+ print(finding)
95
+ ```
96
+
97
+ ## Use it in an agent
98
+
99
+ ```python
100
+ from langgraph_lint.integrations import build_graph
101
+
102
+ auditor = build_graph("anthropic:claude-sonnet-4-5")
103
+ auditor.invoke({
104
+ "messages": [{"role": "user", "content": "Check myapp.graph:app for wiring bugs."}]
105
+ })
106
+ ```
107
+
108
+ Or `get_tools()` to bind `check_langgraph_graph` to your own agent.
109
+
110
+ ## Rules
111
+
112
+ | Code | Catches | LangGraph catches it today? |
113
+ |---|---|---|
114
+ | `LG000` | Graph fails to `compile()` at all | Raises, but as a stack trace, not a lint finding |
115
+ | `LG001` | A node with no path from `START` (dead code) | No |
116
+ | `LG003` | A router's `Literal[...]` return type has a value missing from its `path_map` | **No — silent until that branch runs** |
117
+ | `LG005` | A `path_map` branch the router's own return type can never produce (dead routing code, the mirror image of LG003) | No |
118
+
119
+ Two rules that seemed obviously useful up front — "node with no path to `END`" and "duplicate edge" — got cut before release. Both turned out to be structurally undetectable: LangGraph auto-inserts an implicit escape-hatch edge to `END` on essentially every node (even inside a genuine, unbreakable cycle — verified), and plain edges are deduplicated into a `set` before `get_graph()` ever sees them. A rule that can never fire is worse than no rule; better to ship four honest ones than six decorative ones.
120
+
121
+ ## On false positives
122
+
123
+ Every rule here has a real counter-pattern, and this tool knows about the big one: **`Send()`-based map-reduce fan-out creates zero static edges** to its target node. A node reached only via `Send("worker", ...)` has no incoming edge in the graph's structure at all — indistinguishable, by structure alone, from a typo'd dead node. `langgraph-lint` scans node and router source for `Send("literal_name", ...)` calls and downgrades those findings to informational rather than screaming at correct, idiomatic code.
124
+
125
+ Similarly:
126
+ - `LG005`'s check on unreachable `path_map` branches only trusts a `Literal[...]` type annotation, never the AST heuristic, since a false claim of "this can never happen" is worse than staying silent.
127
+ - `LG003`'s AST-based fallback (used when a router has no `Literal[...]` annotation) is a heuristic, not a guarantee — it scans `return "..."` statements and can miss dynamically computed values. Those findings are `MEDIUM`, not `HIGH`, and say so explicitly.
128
+
129
+ Every `Finding` carries a `caveat` field for exactly this reason. `LG003` findings backed by an actual type annotation carry an empty caveat, because there's nothing to hedge — that's about as close to certain as static analysis gets.
130
+
131
+ ## Compatibility
132
+
133
+ Built and tested against `langgraph==1.2.11`. The `LG003`/`LG005` branch checks read `StateGraph.branches` and `BranchSpec.path`/`.ends` -- internal, non-public attributes, not a documented API. They've been stable across recent LangGraph releases, but a future LangGraph refactor could rename them; if that happens, `langgraph-lint` degrades to `LG001`-only (dead-node detection, which only needs the public `get_graph()`), not a crash -- worth knowing if you pin an unusually old or new LangGraph version.
134
+
135
+ ## Development
136
+
137
+ ```console
138
+ uv venv && uv pip install -e ".[dev]"
139
+ pytest
140
+ ruff check . && mypy src
141
+ ```
142
+
143
+ The test suite builds real `langgraph.StateGraph` objects rather than mocks. The flagship test doesn't just assert a finding fires — it then actually invokes the compiled graph and confirms the predicted `KeyError` really happens, so the rule's claim is proven, not just plausible.
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,113 @@
1
+ # langgraph-lint
2
+
3
+ **Find LangGraph wiring bugs before they become a runtime `KeyError`.**
4
+
5
+ LangGraph validates some things at `compile()` time — an edge to a node that doesn't exist, for instance, fails immediately and loudly. But a lot of real wiring bugs compile perfectly fine and only surface the first time a specific input takes a specific untested branch, weeks later, in production.
6
+
7
+ ```console
8
+ $ langgraph-lint myapp.graph:app
9
+
10
+ [HIGH] LG003 (route_after_search): router can return 'maybe', which has no
11
+ matching entry in the path_map (['no', 'yes']) -- this raises KeyError at
12
+ runtime the first time this branch is taken
13
+
14
+ [HIGH] LG001 (validate_output): 'validate_output' has no incoming edge and
15
+ no path from START; it can never run
16
+
17
+ 2 finding(s) in myapp.graph:app.
18
+ ```
19
+
20
+ That first finding is real, reproducible, and proven — not a guess:
21
+
22
+ ```python
23
+ def router(state) -> Literal["yes", "no", "maybe"]:
24
+ ...
25
+
26
+ graph.add_conditional_edges("a", router, {"yes": "b", "no": "c"}) # no "maybe"!
27
+
28
+ compiled = graph.compile() # succeeds. no warning. nothing.
29
+ compiled.invoke({"x": 2}) # fine
30
+ compiled.invoke({"x": 3}) # fine
31
+ compiled.invoke({"x": 101}) # KeyError: 'maybe' <- only now
32
+ ```
33
+
34
+ `langgraph-lint` catches this the moment you write it, by checking the router's own `Literal[...]` return type against the `path_map` you gave `add_conditional_edges`. No invocation needed.
35
+
36
+ ## Install
37
+
38
+ ```console
39
+ pip install langgraph-lint # zero dependencies
40
+ pip install "langgraph-lint[agent]" # + a LangChain/LangGraph agent tool
41
+ ```
42
+
43
+ ## Use it as a CLI
44
+
45
+ ```console
46
+ langgraph-lint myapp.graph:app # module.path:attribute, like uvicorn
47
+ langgraph-lint myapp.graph:app --min-severity high
48
+ langgraph-lint myapp.graph:app --format json
49
+ langgraph-lint myapp.graph:app --format github # Actions annotations
50
+ ```
51
+
52
+ `target` accepts a compiled graph, an uncompiled `StateGraph` builder, or any object with `.get_graph()` — so it also works on plain LangChain LCEL runnables, just with a smaller rule set (the conditional-branch check is LangGraph-specific). Exit code `1` on findings, `0` clean, `2` on error.
53
+
54
+ ## Use it as a library
55
+
56
+ ```python
57
+ from langgraph_lint import lint
58
+
59
+ for finding in lint(app):
60
+ print(finding)
61
+ ```
62
+
63
+ ## Use it in an agent
64
+
65
+ ```python
66
+ from langgraph_lint.integrations import build_graph
67
+
68
+ auditor = build_graph("anthropic:claude-sonnet-4-5")
69
+ auditor.invoke({
70
+ "messages": [{"role": "user", "content": "Check myapp.graph:app for wiring bugs."}]
71
+ })
72
+ ```
73
+
74
+ Or `get_tools()` to bind `check_langgraph_graph` to your own agent.
75
+
76
+ ## Rules
77
+
78
+ | Code | Catches | LangGraph catches it today? |
79
+ |---|---|---|
80
+ | `LG000` | Graph fails to `compile()` at all | Raises, but as a stack trace, not a lint finding |
81
+ | `LG001` | A node with no path from `START` (dead code) | No |
82
+ | `LG003` | A router's `Literal[...]` return type has a value missing from its `path_map` | **No — silent until that branch runs** |
83
+ | `LG005` | A `path_map` branch the router's own return type can never produce (dead routing code, the mirror image of LG003) | No |
84
+
85
+ Two rules that seemed obviously useful up front — "node with no path to `END`" and "duplicate edge" — got cut before release. Both turned out to be structurally undetectable: LangGraph auto-inserts an implicit escape-hatch edge to `END` on essentially every node (even inside a genuine, unbreakable cycle — verified), and plain edges are deduplicated into a `set` before `get_graph()` ever sees them. A rule that can never fire is worse than no rule; better to ship four honest ones than six decorative ones.
86
+
87
+ ## On false positives
88
+
89
+ Every rule here has a real counter-pattern, and this tool knows about the big one: **`Send()`-based map-reduce fan-out creates zero static edges** to its target node. A node reached only via `Send("worker", ...)` has no incoming edge in the graph's structure at all — indistinguishable, by structure alone, from a typo'd dead node. `langgraph-lint` scans node and router source for `Send("literal_name", ...)` calls and downgrades those findings to informational rather than screaming at correct, idiomatic code.
90
+
91
+ Similarly:
92
+ - `LG005`'s check on unreachable `path_map` branches only trusts a `Literal[...]` type annotation, never the AST heuristic, since a false claim of "this can never happen" is worse than staying silent.
93
+ - `LG003`'s AST-based fallback (used when a router has no `Literal[...]` annotation) is a heuristic, not a guarantee — it scans `return "..."` statements and can miss dynamically computed values. Those findings are `MEDIUM`, not `HIGH`, and say so explicitly.
94
+
95
+ Every `Finding` carries a `caveat` field for exactly this reason. `LG003` findings backed by an actual type annotation carry an empty caveat, because there's nothing to hedge — that's about as close to certain as static analysis gets.
96
+
97
+ ## Compatibility
98
+
99
+ Built and tested against `langgraph==1.2.11`. The `LG003`/`LG005` branch checks read `StateGraph.branches` and `BranchSpec.path`/`.ends` -- internal, non-public attributes, not a documented API. They've been stable across recent LangGraph releases, but a future LangGraph refactor could rename them; if that happens, `langgraph-lint` degrades to `LG001`-only (dead-node detection, which only needs the public `get_graph()`), not a crash -- worth knowing if you pin an unusually old or new LangGraph version.
100
+
101
+ ## Development
102
+
103
+ ```console
104
+ uv venv && uv pip install -e ".[dev]"
105
+ pytest
106
+ ruff check . && mypy src
107
+ ```
108
+
109
+ The test suite builds real `langgraph.StateGraph` objects rather than mocks. The flagship test doesn't just assert a finding fires — it then actually invokes the compiled graph and confirms the predicted `KeyError` really happens, so the rule's claim is proven, not just plausible.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langgraph-lint"
7
+ version = "0.1.0"
8
+ description = "Find LangGraph wiring bugs before they become a runtime KeyError."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "mathewOracle" }]
13
+ keywords = [
14
+ "langgraph",
15
+ "langchain",
16
+ "static-analysis",
17
+ "linter",
18
+ "graph",
19
+ "agent",
20
+ "ai",
21
+ "code-quality",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Software Development :: Quality Assurance",
32
+ "Topic :: Software Development :: Testing",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = []
36
+
37
+ [project.optional-dependencies]
38
+ agent = ["langchain>=1.0", "langchain-core>=0.3", "langgraph>=0.2"]
39
+ dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11", "build>=1.2", "langgraph>=0.2"]
40
+
41
+ [project.scripts]
42
+ langgraph-lint = "langgraph_lint.cli:main"
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/mathewOracle/langgraph-lint"
46
+ Repository = "https://github.com/mathewOracle/langgraph-lint"
47
+ Issues = "https://github.com/mathewOracle/langgraph-lint/issues"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["src/langgraph_lint"]
51
+
52
+ [tool.ruff]
53
+ line-length = 100
54
+ target-version = "py311"
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
58
+
59
+ [tool.pytest.ini_options]
60
+ testpaths = ["tests"]
61
+
62
+ [tool.mypy]
63
+ python_version = "3.11"
64
+ strict = true
65
+
66
+ [[tool.mypy.overrides]]
67
+ module = "langgraph_lint.integrations"
68
+ disallow_untyped_decorators = false
69
+
70
+ [[tool.mypy.overrides]]
71
+ module = ["langchain.*", "langchain_core.*", "langgraph.*"]
72
+ ignore_missing_imports = true
@@ -0,0 +1,13 @@
1
+ """Public API.
2
+
3
+ Find LangGraph wiring bugs LangGraph itself won't catch until runtime.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from langgraph_lint.core import lint
9
+ from langgraph_lint.models import Finding, Severity
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ __all__ = ["Finding", "Severity", "__version__", "lint"]