leangate 0.1.0__py3-none-any.whl
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.
- leangate/__init__.py +17 -0
- leangate/__main__.py +192 -0
- leangate/analysis/__init__.py +4 -0
- leangate/analysis/bloat.py +507 -0
- leangate/analysis/deps.py +43 -0
- leangate/analysis/errors.py +389 -0
- leangate/analysis/helpers.py +54 -0
- leangate/analysis/imports.py +168 -0
- leangate/analysis/missing.py +184 -0
- leangate/analysis/questions.py +168 -0
- leangate/analysis/redundancy.py +64 -0
- leangate/analysis/ruff_rules.py +65 -0
- leangate/analysis/scopes.py +31 -0
- leangate/analysis/security.py +440 -0
- leangate/backend.py +67 -0
- leangate/cache.py +68 -0
- leangate/cloud/__init__.py +8 -0
- leangate/cloud/client.py +184 -0
- leangate/cloud/sanitize.py +65 -0
- leangate/config.py +149 -0
- leangate/diff.py +163 -0
- leangate/engine.py +261 -0
- leangate/grounding/__init__.py +4 -0
- leangate/grounding/ground.py +70 -0
- leangate/grounding/registry.py +65 -0
- leangate/grounding/tavily_search.py +61 -0
- leangate/grounding/versions.py +75 -0
- leangate/hooks/__init__.py +0 -0
- leangate/hooks/edit_hook.py +173 -0
- leangate/hooks/install.py +76 -0
- leangate/hooks/prompt_hook.py +66 -0
- leangate/hooks/session_hook.py +47 -0
- leangate/hooks/stop_hook.py +44 -0
- leangate/index/__init__.py +3 -0
- leangate/index/conventions.py +193 -0
- leangate/index/house.py +263 -0
- leangate/index/repo_index.py +580 -0
- leangate/judge/__init__.py +3 -0
- leangate/judge/jev.py +147 -0
- leangate/learning.py +220 -0
- leangate/live/__init__.py +180 -0
- leangate/live/server.py +238 -0
- leangate/live/static/live.html +114 -0
- leangate/live/static/live.js +385 -0
- leangate/llm/__init__.py +3 -0
- leangate/llm/nemotron.py +98 -0
- leangate/lsp/__init__.py +35 -0
- leangate/lsp/convert.py +280 -0
- leangate/lsp/server.py +243 -0
- leangate/mcp_server.py +152 -0
- leangate/memory/__init__.py +3 -0
- leangate/memory/store.py +134 -0
- leangate/models.py +199 -0
- leangate/pipeline.py +843 -0
- leangate/pricing.py +33 -0
- leangate/router.py +121 -0
- leangate/sandbox/__init__.py +3 -0
- leangate/sandbox/runner.py +240 -0
- leangate/service/__init__.py +0 -0
- leangate/service/app.py +246 -0
- leangate/service/limits.py +178 -0
- leangate/service/playground.py +97 -0
- leangate/service/static/app.css +503 -0
- leangate/service/static/app.js +481 -0
- leangate/service/static/example.js +16 -0
- leangate/service/static/favicon.svg +1 -0
- leangate/service/static/index.html +205 -0
- leangate/service/static/mock.js +262 -0
- leangate/service/static/render.js +334 -0
- leangate-0.1.0.dist-info/METADATA +556 -0
- leangate-0.1.0.dist-info/RECORD +74 -0
- leangate-0.1.0.dist-info/WHEEL +4 -0
- leangate-0.1.0.dist-info/entry_points.txt +8 -0
- leangate-0.1.0.dist-info/licenses/LICENSE +21 -0
leangate/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""LeanGate: keeps AI-written code lean in production codebases."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
# What ast.parse raises on hostile or broken input: null bytes (ValueError before 3.12), deep nesting.
|
|
8
|
+
PARSE_ERRORS = (SyntaxError, ValueError, RecursionError, MemoryError)
|
|
9
|
+
|
|
10
|
+
_LINE = re.compile(r"[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+\Z")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def split_lines(text: str, keepends: bool = False) -> list[str]:
|
|
14
|
+
"""str.splitlines, but only at \\n, \\r\\n and \\r: where ast, git and LSP clients count lines. splitlines also
|
|
15
|
+
breaks at form feeds and \\u2028, which shifts every line number after one."""
|
|
16
|
+
lines = _LINE.findall(text)
|
|
17
|
+
return lines if keepends else [line.rstrip("\r\n") for line in lines]
|
leangate/__main__.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""CLI: `leangate verify`, `leangate reuse "<intent>"`, `leangate dep <name>`, `leangate waive <signature>`,
|
|
2
|
+
`leangate providers`, `leangate dashboard` (local live report on 127.0.0.1), `leangate lsp` (language server over
|
|
3
|
+
stdio).
|
|
4
|
+
|
|
5
|
+
Handy for demos, pre-commit hooks, and CI. Exit codes: 0 pass, 1 `verify` blocked, 2 usage or environment error.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(prog="leangate")
|
|
18
|
+
parser.add_argument("--repo", default=".")
|
|
19
|
+
parser.add_argument("--json", action="store_true", help="print the full JSON report")
|
|
20
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
21
|
+
v = sub.add_parser("verify", help="check the working tree (or a diff) for bloat, missing error handling and security problems")
|
|
22
|
+
source = v.add_mutually_exclusive_group()
|
|
23
|
+
source.add_argument("--diff", help="path to a unified diff; default: working tree vs HEAD")
|
|
24
|
+
source.add_argument("--base", help="check the changes since the merge-base with this ref (CI on a PR)")
|
|
25
|
+
source.add_argument("--staged", action="store_true", help="check only staged changes (pre-commit)")
|
|
26
|
+
v.add_argument("--fast", action="store_true", help="Jev only, no Nemotron or web grounding")
|
|
27
|
+
r = sub.add_parser("reuse", help="find existing code before writing new code")
|
|
28
|
+
r.add_argument("intent")
|
|
29
|
+
d = sub.add_parser("dep", help="check a dependency before adding it")
|
|
30
|
+
d.add_argument("name")
|
|
31
|
+
d.add_argument("--purpose", default="")
|
|
32
|
+
w = sub.add_parser("waive", help="dismiss a finding as a person (the only way to waive a security finding)")
|
|
33
|
+
w.add_argument("signature", nargs="?", help="the finding's signature; omit to list what agents asked to waive")
|
|
34
|
+
w.add_argument("--note", default="", help="why it's wrong; committed with the waiver")
|
|
35
|
+
w.add_argument("--local", action="store_true", help="only on this machine; don't write .leangate/waivers.json")
|
|
36
|
+
sub.add_parser("providers", help="show which providers are live")
|
|
37
|
+
dash = sub.add_parser("dashboard", help="serve the live report on 127.0.0.1")
|
|
38
|
+
dash.add_argument("--port", type=int, default=None)
|
|
39
|
+
sub.add_parser("lsp", help="run the language server over stdio (editors launch this)")
|
|
40
|
+
hk = sub.add_parser("hooks", help="show the Claude Code hooks for --repo (per-edit feedback + the Stop gate)")
|
|
41
|
+
hk.add_argument("--write", action="store_true", help="merge them into <repo>/.claude/settings.local.json")
|
|
42
|
+
lg = sub.add_parser("login", help="push this machine's runs (reports, no code) to your LeanGate Cloud dashboard")
|
|
43
|
+
lg.add_argument("--url", required=True, help="the site, e.g. https://leangate.example")
|
|
44
|
+
lg.add_argument("--key", help="an API key from the site's account page; omit to type it in")
|
|
45
|
+
sub.add_parser("logout", help="stop pushing runs from this machine and forget its key")
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
try:
|
|
48
|
+
_run(args)
|
|
49
|
+
except Exception as exc: # exit 1 means "blocked"; a crash must not read as a verdict on the code
|
|
50
|
+
_fail(f"{type(exc).__name__}: {exc}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run(args: argparse.Namespace) -> None:
|
|
54
|
+
if args.cmd == "hooks":
|
|
55
|
+
from leangate.hooks.install import install
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
print(install(args.repo, args.write))
|
|
59
|
+
except ValueError as e:
|
|
60
|
+
_fail(str(e))
|
|
61
|
+
return
|
|
62
|
+
if args.cmd == "dashboard":
|
|
63
|
+
from leangate.live import serve
|
|
64
|
+
|
|
65
|
+
serve(port=args.port)
|
|
66
|
+
return
|
|
67
|
+
if args.cmd == "lsp":
|
|
68
|
+
from leangate.lsp import serve
|
|
69
|
+
|
|
70
|
+
serve()
|
|
71
|
+
return
|
|
72
|
+
if args.cmd == "waive":
|
|
73
|
+
print(_waive(args.repo, args.signature, args.note, args.local))
|
|
74
|
+
return
|
|
75
|
+
if args.cmd in ("login", "logout"):
|
|
76
|
+
print(_login(args.url, args.key) if args.cmd == "login" else _logout())
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
from leangate.pipeline import LeanGate
|
|
80
|
+
|
|
81
|
+
if args.cmd == "providers":
|
|
82
|
+
from leangate.backend import get_backend
|
|
83
|
+
|
|
84
|
+
print(json.dumps(get_backend().providers(), indent=2))
|
|
85
|
+
return
|
|
86
|
+
gate = LeanGate(args.repo, escalate=not getattr(args, "fast", False), source="cli")
|
|
87
|
+
if args.cmd == "verify":
|
|
88
|
+
diff = open(args.diff, encoding="utf-8").read() if args.diff else None
|
|
89
|
+
if diff is None and not _is_git_repo(args.repo):
|
|
90
|
+
# A gate that silently passes outside a repo is worse than one that fails loudly.
|
|
91
|
+
_fail(f"{args.repo} is not a git repository")
|
|
92
|
+
try:
|
|
93
|
+
report = gate.verify_lean(diff, base=args.base, staged=args.staged)
|
|
94
|
+
except ValueError as e:
|
|
95
|
+
_fail(str(e))
|
|
96
|
+
print(report.model_dump_json(indent=2) if args.json else report.summary)
|
|
97
|
+
sys.exit(1 if report.gate == "block" else 0)
|
|
98
|
+
if args.cmd == "reuse":
|
|
99
|
+
report = gate.check_reuse(args.intent)
|
|
100
|
+
if args.json:
|
|
101
|
+
print(report.model_dump_json(indent=2))
|
|
102
|
+
else:
|
|
103
|
+
print(f"Recommendation: {report.recommendation}")
|
|
104
|
+
for c in report.candidates:
|
|
105
|
+
print(f" {c.decision.choice if c.decision else '?':9} {c.similarity:.2f} {c.symbol} ({c.path}:{c.line})")
|
|
106
|
+
return
|
|
107
|
+
if args.cmd == "dep":
|
|
108
|
+
print(gate.check_dependency(args.name, args.purpose).model_dump_json(indent=2))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _login(url: str, key: str | None) -> str:
|
|
112
|
+
"""Check the key against the site, then save it: from now on each run's report (no code) is pushed there."""
|
|
113
|
+
import getpass
|
|
114
|
+
|
|
115
|
+
import httpx
|
|
116
|
+
|
|
117
|
+
from leangate.cloud.client import save_config
|
|
118
|
+
|
|
119
|
+
key = (key or getpass.getpass("API key: ")).strip()
|
|
120
|
+
url = url.rstrip("/")
|
|
121
|
+
try:
|
|
122
|
+
res = httpx.get(f"{url}/v1/whoami", headers={"Authorization": f"Bearer {key}"}, timeout=15)
|
|
123
|
+
except httpx.HTTPError as exc:
|
|
124
|
+
_fail(f"couldn't reach {url}: {exc}")
|
|
125
|
+
if res.status_code != 200:
|
|
126
|
+
_fail(f"{url} refused the key ({res.status_code}): make a new one on the account page")
|
|
127
|
+
path = save_config(url, key)
|
|
128
|
+
who = res.json().get("login") or "your account"
|
|
129
|
+
return (f"Signed in as {who}. Runs in repos with a git remote will show up at {url}/app/ "
|
|
130
|
+
f"(reports only, no code). Key saved in {path}; `leangate logout` removes it.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _logout() -> str:
|
|
134
|
+
from leangate.cloud.client import save_config
|
|
135
|
+
|
|
136
|
+
save_config("", None)
|
|
137
|
+
return "Signed out: this machine no longer pushes runs. Its runs already on the site stay until you delete them there."
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _waive(repo: str, signature: str | None, note: str, local: bool) -> str:
|
|
141
|
+
"""A person's waiver: suppressed here, and (unless --local) written to .leangate/waivers.json to commit, so CI
|
|
142
|
+
and teammates honor it too. Refused inside a Claude Code session: an agent with a shell could otherwise waive
|
|
143
|
+
its own security findings, which is what this command exists to prevent."""
|
|
144
|
+
import datetime
|
|
145
|
+
import os
|
|
146
|
+
from pathlib import Path
|
|
147
|
+
|
|
148
|
+
from leangate.memory import Memory
|
|
149
|
+
from leangate.memory.store import WAIVERS
|
|
150
|
+
|
|
151
|
+
root = Path(subprocess.run(["git", "-C", repo, "rev-parse", "--show-toplevel"], capture_output=True, text=True,
|
|
152
|
+
stdin=subprocess.DEVNULL, timeout=10).stdout.strip() or repo)
|
|
153
|
+
memory = Memory(root, remote=False)
|
|
154
|
+
if signature is None:
|
|
155
|
+
requests = memory.waiver_requests
|
|
156
|
+
if not requests:
|
|
157
|
+
return "No waiver requests from agents."
|
|
158
|
+
return "\n".join(["Agents asked to waive these (run `leangate waive <signature> --note ...` to agree):"]
|
|
159
|
+
+ [f"- {sig}: {why or '(no reason given)'}" for sig, why in requests.items()])
|
|
160
|
+
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_ENTRYPOINT"):
|
|
161
|
+
_fail("waivers must come from a person: run this in your own terminal, not through the agent")
|
|
162
|
+
memory.reject(signature, note)
|
|
163
|
+
if local:
|
|
164
|
+
return f"Waived {signature} on this machine."
|
|
165
|
+
target = root / WAIVERS
|
|
166
|
+
try:
|
|
167
|
+
data = json.loads(target.read_text(encoding="utf-8")) if target.is_file() else {}
|
|
168
|
+
except ValueError:
|
|
169
|
+
_fail(f"{target} isn't valid JSON; fix it first (the local waiver was saved)")
|
|
170
|
+
author = subprocess.run(["git", "-C", str(root), "config", "user.name"], capture_output=True, text=True,
|
|
171
|
+
stdin=subprocess.DEVNULL, timeout=10).stdout.strip()
|
|
172
|
+
data.setdefault("waivers", {})[signature] = {"note": note, "by": author, "date": datetime.date.today().isoformat()}
|
|
173
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
target.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
175
|
+
return f"Waived {signature}. Commit {WAIVERS} so CI and teammates honor it too."
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _is_git_repo(repo: str) -> bool:
|
|
179
|
+
try:
|
|
180
|
+
return subprocess.run(["git", "-C", repo, "rev-parse", "--git-dir"], capture_output=True,
|
|
181
|
+
stdin=subprocess.DEVNULL, timeout=10).returncode == 0
|
|
182
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _fail(message: str) -> None:
|
|
187
|
+
print(f"leangate: {message}", file=sys.stderr)
|
|
188
|
+
sys.exit(2)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
main()
|