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.
Files changed (74) hide show
  1. leangate/__init__.py +17 -0
  2. leangate/__main__.py +192 -0
  3. leangate/analysis/__init__.py +4 -0
  4. leangate/analysis/bloat.py +507 -0
  5. leangate/analysis/deps.py +43 -0
  6. leangate/analysis/errors.py +389 -0
  7. leangate/analysis/helpers.py +54 -0
  8. leangate/analysis/imports.py +168 -0
  9. leangate/analysis/missing.py +184 -0
  10. leangate/analysis/questions.py +168 -0
  11. leangate/analysis/redundancy.py +64 -0
  12. leangate/analysis/ruff_rules.py +65 -0
  13. leangate/analysis/scopes.py +31 -0
  14. leangate/analysis/security.py +440 -0
  15. leangate/backend.py +67 -0
  16. leangate/cache.py +68 -0
  17. leangate/cloud/__init__.py +8 -0
  18. leangate/cloud/client.py +184 -0
  19. leangate/cloud/sanitize.py +65 -0
  20. leangate/config.py +149 -0
  21. leangate/diff.py +163 -0
  22. leangate/engine.py +261 -0
  23. leangate/grounding/__init__.py +4 -0
  24. leangate/grounding/ground.py +70 -0
  25. leangate/grounding/registry.py +65 -0
  26. leangate/grounding/tavily_search.py +61 -0
  27. leangate/grounding/versions.py +75 -0
  28. leangate/hooks/__init__.py +0 -0
  29. leangate/hooks/edit_hook.py +173 -0
  30. leangate/hooks/install.py +76 -0
  31. leangate/hooks/prompt_hook.py +66 -0
  32. leangate/hooks/session_hook.py +47 -0
  33. leangate/hooks/stop_hook.py +44 -0
  34. leangate/index/__init__.py +3 -0
  35. leangate/index/conventions.py +193 -0
  36. leangate/index/house.py +263 -0
  37. leangate/index/repo_index.py +580 -0
  38. leangate/judge/__init__.py +3 -0
  39. leangate/judge/jev.py +147 -0
  40. leangate/learning.py +220 -0
  41. leangate/live/__init__.py +180 -0
  42. leangate/live/server.py +238 -0
  43. leangate/live/static/live.html +114 -0
  44. leangate/live/static/live.js +385 -0
  45. leangate/llm/__init__.py +3 -0
  46. leangate/llm/nemotron.py +98 -0
  47. leangate/lsp/__init__.py +35 -0
  48. leangate/lsp/convert.py +280 -0
  49. leangate/lsp/server.py +243 -0
  50. leangate/mcp_server.py +152 -0
  51. leangate/memory/__init__.py +3 -0
  52. leangate/memory/store.py +134 -0
  53. leangate/models.py +199 -0
  54. leangate/pipeline.py +843 -0
  55. leangate/pricing.py +33 -0
  56. leangate/router.py +121 -0
  57. leangate/sandbox/__init__.py +3 -0
  58. leangate/sandbox/runner.py +240 -0
  59. leangate/service/__init__.py +0 -0
  60. leangate/service/app.py +246 -0
  61. leangate/service/limits.py +178 -0
  62. leangate/service/playground.py +97 -0
  63. leangate/service/static/app.css +503 -0
  64. leangate/service/static/app.js +481 -0
  65. leangate/service/static/example.js +16 -0
  66. leangate/service/static/favicon.svg +1 -0
  67. leangate/service/static/index.html +205 -0
  68. leangate/service/static/mock.js +262 -0
  69. leangate/service/static/render.js +334 -0
  70. leangate-0.1.0.dist-info/METADATA +556 -0
  71. leangate-0.1.0.dist-info/RECORD +74 -0
  72. leangate-0.1.0.dist-info/WHEEL +4 -0
  73. leangate-0.1.0.dist-info/entry_points.txt +8 -0
  74. 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()
@@ -0,0 +1,4 @@
1
+ from leangate.analysis.bloat import analyze_file, static_minimize
2
+ from leangate.analysis.deps import added_dependencies
3
+
4
+ __all__ = ["added_dependencies", "analyze_file", "static_minimize"]