sourcecode 1.31.30__py3-none-any.whl → 1.31.32__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.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.31.30"
3
+ __version__ = "1.31.32"
sourcecode/cache.py CHANGED
@@ -469,6 +469,7 @@ def _cas_load_blob(cache_d: Path, blob_hash: str) -> Optional[str]:
469
469
  try:
470
470
  return gzip.decompress(path.read_bytes()).decode("utf-8")
471
471
  except Exception:
472
+ _safe_unlink(path) # evict corrupted blob so it doesn't block future reads
472
473
  return None
473
474
 
474
475
 
sourcecode/cli.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import threading
5
6
  import time
6
7
  from pathlib import Path
7
8
  from typing import Any, Optional, cast
@@ -140,7 +141,7 @@ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[nam
140
141
  return issues
141
142
 
142
143
  _HELP = """\
143
- Compressed AI-ready context for Java/Spring enterprise codebases.
144
+ Deterministic codebase context for AI coding agents.
144
145
 
145
146
  [bold]Primary usage:[/bold]
146
147
  sourcecode --compact high-signal summary (~600-800 tokens)
@@ -171,12 +172,25 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
171
172
  "repo-ir", "mcp", "endpoints", "impact",
172
173
  # Enterprise workflow commands
173
174
  "onboard", "modernize", "fix-bug", "review-pr",
175
+ # License
176
+ "activate",
174
177
  }
175
178
  )
176
179
 
177
- # Mutable container holding the path extracted by _preprocess_argv().
178
- # Default "." means "current directory" when no path is given.
179
- _detected_path: list[str] = ["."]
180
+ # Thread-local storage for the path extracted by _preprocess_argv().
181
+ # Using threading.local() prevents concurrent MCP tool calls from clobbering
182
+ # each other's target path (the old module-level list was a shared mutable global).
183
+ _tls = threading.local()
184
+
185
+
186
+ def _get_detected_path() -> str:
187
+ """Return the thread-local detected path, defaulting to '.'."""
188
+ return _tls.__dict__.get("detected_path", ".")
189
+
190
+
191
+ def _set_detected_path(value: str) -> None:
192
+ """Set the thread-local detected path."""
193
+ _tls.detected_path = value
180
194
 
181
195
 
182
196
  # Options that take a value token — their next arg must not be treated as a path.
@@ -212,6 +226,7 @@ def _preprocess_args(args: list[str]) -> list[str]:
212
226
  """
213
227
  result = list(args)
214
228
  skip_next = False
229
+ _path_index: int = -1
215
230
  for i, arg in enumerate(result):
216
231
  if skip_next:
217
232
  skip_next = False
@@ -225,9 +240,11 @@ def _preprocess_args(args: list[str]) -> list[str]:
225
240
  if arg in _SUBCOMMANDS:
226
241
  return result # known subcommand — leave for Click to dispatch
227
242
  # First genuine positional: treat as repository path
228
- _detected_path[0] = arg
229
- result.pop(i)
230
- return result
243
+ _set_detected_path(arg)
244
+ _path_index = i
245
+ break
246
+ if _path_index >= 0:
247
+ result.pop(_path_index)
231
248
  return result
232
249
 
233
250
 
@@ -253,6 +270,35 @@ def _emit_error_json(error: str, message: str, **context: object) -> None:
253
270
  _sys.stderr.flush()
254
271
 
255
272
 
273
+ # H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
274
+ # Click's default show() writes "Error: No such option: --foo" as plain text.
275
+ # Automation consumers need JSON on stderr regardless of how the error originated.
276
+ try:
277
+ import click.exceptions as _click_exc
278
+
279
+ def _json_click_usage_error_show(self: Any, file: Any = None) -> None: # type: ignore[override]
280
+ import json as _je
281
+ import sys as _jse
282
+ _code_map = {
283
+ "NoSuchOption": "invalid_option",
284
+ "BadOptionUsage": "invalid_option",
285
+ "BadParameter": "bad_parameter",
286
+ "MissingParameter": "missing_required",
287
+ "BadArgumentUsage": "bad_argument",
288
+ }
289
+ code = _code_map.get(type(self).__name__, "invalid_option")
290
+ payload: dict[str, object] = {"error": code, "message": self.format_message()}
291
+ _opt = getattr(self, "option_name", None) or getattr(self, "param_hint", None)
292
+ if _opt:
293
+ payload["flag"] = str(_opt).strip("'\"")
294
+ _jse.stderr.write(_je.dumps(payload, ensure_ascii=False) + "\n")
295
+ _jse.stderr.flush()
296
+
297
+ _click_exc.UsageError.show = _json_click_usage_error_show # type: ignore[method-assign]
298
+ except Exception:
299
+ pass # click unavailable — plain-text fallback
300
+
301
+
256
302
  def _copy_to_clipboard(content: str) -> bool:
257
303
  """Copy text to system clipboard. Returns True on success, False otherwise (never raises)."""
258
304
  import subprocess
@@ -303,7 +349,7 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
303
349
  def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
304
350
  if args is not None:
305
351
  # CliRunner / programmatic call: preprocess the explicit args list.
306
- _detected_path[0] = "."
352
+ _set_detected_path(".")
307
353
  args = _preprocess_args(list(args))
308
354
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
309
355
  return _orig_cmd_main(args=args, **kwargs)
@@ -332,7 +378,7 @@ app.add_typer(mcp_app, name="mcp")
332
378
  def _maybe_ask_consent() -> None:
333
379
  """Show first-run consent prompt once, on interactive TTYs only."""
334
380
  try:
335
- from sourcecode.telemetry.config import has_been_asked, mark_asked, set_enabled
381
+ from sourcecode.telemetry.config import has_been_asked, set_enabled
336
382
  from sourcecode.telemetry.consent import ask_for_consent
337
383
  if not has_been_asked():
338
384
  enabled = ask_for_consent()
@@ -802,7 +848,7 @@ def main(
802
848
  # Path was extracted from argv by _preprocess_argv() before Click ran.
803
849
  # FIX-P2-8: preserve original user input in error messages (Windows Git Bash
804
850
  # rewrites "/nonexistent" → "C:\Program Files\Git\nonexistent" via Path.resolve()).
805
- _raw_path_input = _detected_path[0]
851
+ _raw_path_input = _get_detected_path()
806
852
  target = Path(_raw_path_input).resolve()
807
853
  if not target.exists():
808
854
  _emit_error_json(
@@ -821,6 +867,7 @@ def main(
821
867
 
822
868
  # Normalize mode aliases
823
869
  _CONTRACT_MODES = frozenset({"contract", "minimal", "standard"})
870
+ _user_mode_explicit = mode not in ("contract",) # track if user passed a non-default value
824
871
  if mode == "minimal":
825
872
  mode = "contract" # minimal is a documented alias for contract
826
873
  elif mode not in _CONTRACT_MODES and mode != "raw":
@@ -841,6 +888,20 @@ def main(
841
888
  or docs or semantics or full_metrics or architecture
842
889
  )
843
890
  if mode in ("contract", "standard") and _legacy_flags_active:
891
+ if _user_mode_explicit:
892
+ _overriding_flags = [
893
+ f for f, v in [
894
+ ("--compact", compact), ("--tree", tree),
895
+ ("--trace-pipeline", trace_pipeline), ("--docs", docs),
896
+ ("--semantics", semantics), ("--full-metrics", full_metrics),
897
+ ("--architecture", architecture),
898
+ ] if v
899
+ ]
900
+ typer.echo(
901
+ f"[warning] --mode {mode} was overridden to raw because legacy flags "
902
+ f"({', '.join(_overriding_flags)}) require raw output mode.",
903
+ err=True,
904
+ )
844
905
  mode = "raw"
845
906
 
846
907
  # Map mode to contract_view depth
@@ -964,7 +1025,7 @@ def main(
964
1025
  f"cn={code_notes},mode={mode},"
965
1026
  f"ex={_excl_key},depth={effective_depth}"
966
1027
  )
967
- _core_h = _hashlib.md5(_core_flags_str.encode()).hexdigest()[:8]
1028
+ _core_h = _hashlib.sha256(_core_flags_str.encode()).hexdigest()[:8]
968
1029
  _core_key = f"{_git_sha}-{_core_h}"
969
1030
 
970
1031
  # ── View flags: output presentation only (no re-analysis needed) ──
@@ -976,7 +1037,7 @@ def main(
976
1037
  f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
977
1038
  f"eg={emit_graph}"
978
1039
  )
979
- _view_h = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1040
+ _view_h = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
980
1041
 
981
1042
  # ── Lookup ──────────────────────────────────────────────────────
982
1043
  # Step 1: try L1 to obtain the core_hash needed for L2 key
@@ -1930,7 +1991,7 @@ def main(
1930
1991
  if _written_core_hash:
1931
1992
  if not _view_key:
1932
1993
  # _view_key not set (L1 was also a miss); compute it now
1933
- _wvh = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1994
+ _wvh = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
1934
1995
  _view_key = f"{_written_core_hash}-{_wvh}"
1935
1996
  _cache_mod.write_view(
1936
1997
  target,
@@ -2165,6 +2226,12 @@ def prepare_context_cmd(
2165
2226
  )
2166
2227
  raise typer.Exit(code=1)
2167
2228
 
2229
+ # Pro gate: generate-tests and delta require an active Pro license.
2230
+ _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2231
+ if task in _PRO_TASKS:
2232
+ from sourcecode.license import require_pro as _require_pro
2233
+ _require_pro(task)
2234
+
2168
2235
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2169
2236
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
2170
2237
  # Invalid values must error loudly — silently falling through to JSON is a lie.
@@ -2222,7 +2289,49 @@ def prepare_context_cmd(
2222
2289
  _sys.stderr.flush()
2223
2290
  _t0 = _time.perf_counter()
2224
2291
  try:
2225
- output = builder.build(task, since=since, symptom=symptom, fast=fast, include_config=include_config, all_gaps=all_gaps)
2292
+ # H-02: apply timeout for generate-tests large repos can stall indefinitely.
2293
+ # Mirrors SOURCECODE_TESTS_TIMEOUT_MS used by the MCP generate_tests_context tool.
2294
+ if task == "generate-tests" and not fast:
2295
+ import concurrent.futures as _cf
2296
+ import os as _os_gt
2297
+ _timeout_ms = int(_os_gt.environ.get("SOURCECODE_TESTS_TIMEOUT_MS", "30000"))
2298
+ _timeout_s = _timeout_ms / 1000.0
2299
+ _ex = _cf.ThreadPoolExecutor(max_workers=1)
2300
+ _fut = _ex.submit(
2301
+ builder.build, task,
2302
+ since=since, symptom=symptom, fast=fast,
2303
+ include_config=include_config, all_gaps=all_gaps,
2304
+ )
2305
+ _done_set, _nd_set = _cf.wait([_fut], timeout=_timeout_s)
2306
+ _ex.shutdown(wait=False)
2307
+ if _nd_set:
2308
+ import sys as _sys_gt
2309
+ if _sys_gt.stderr.isatty():
2310
+ _sys_gt.stderr.write(
2311
+ f"[generate-tests] timeout after {_timeout_ms}ms — returning partial result\n"
2312
+ )
2313
+ _sys_gt.stderr.flush()
2314
+ from sourcecode.prepare_context import TaskOutput as _TO
2315
+ output = _TO(
2316
+ task=task,
2317
+ goal=TASKS[task].goal,
2318
+ project_summary=None,
2319
+ architecture_summary=None,
2320
+ relevant_files=[],
2321
+ suspected_areas=[],
2322
+ improvement_opportunities=[],
2323
+ test_gaps=[],
2324
+ key_dependencies=[],
2325
+ code_notes_summary=None,
2326
+ limitations=[f"generate-tests timed out after {_timeout_ms}ms"],
2327
+ truncated=True,
2328
+ truncated_reason=f"timeout_{_timeout_ms}ms",
2329
+ confidence="low",
2330
+ )
2331
+ else:
2332
+ output = _fut.result()
2333
+ else:
2334
+ output = builder.build(task, since=since, symptom=symptom, fast=fast, include_config=include_config, all_gaps=all_gaps)
2226
2335
  finally:
2227
2336
  _progress.finish()
2228
2337
  _t_total = (_time.perf_counter() - _t0) * 1000
@@ -2532,6 +2641,19 @@ def prepare_context_cmd(
2532
2641
  if llm_prompt:
2533
2642
  out["llm_prompt"] = builder.render_prompt(output)
2534
2643
 
2644
+ # H-01: fast-mode analysis transparency — consumer must not confuse "not analyzed"
2645
+ # with "analyzed and found nothing". Fields that were never computed are absent or null,
2646
+ # not zero. analysis_mode and skipped_analyzers make the omission explicit.
2647
+ if fast:
2648
+ out["analysis_mode"] = "fast"
2649
+ _skipped: list[str] = ["deep_content_scan"]
2650
+ _spec = TASKS.get(task)
2651
+ if _spec and _spec.enable_code_notes:
2652
+ _skipped.append("code_notes")
2653
+ if task == "generate-tests":
2654
+ _skipped.append("test_gap_discovery")
2655
+ out["skipped_analyzers"] = _skipped
2656
+
2535
2657
  # P0-1: Apply output budget per task — safety net for large repos.
2536
2658
  from sourcecode.output_budget import (
2537
2659
  trim_to_budget as _pc_trim,
@@ -2864,6 +2986,9 @@ def impact_cmd(
2864
2986
  sourcecode impact org.keycloak.services.DefaultKeycloakSession /path/to/keycloak
2865
2987
  sourcecode impact UserService --depth 6 --output impact.json
2866
2988
  """
2989
+ from sourcecode.license import require_pro as _require_pro
2990
+ _require_pro("impact")
2991
+
2867
2992
  import json as _json
2868
2993
  import sys as _sys
2869
2994
 
@@ -2924,9 +3049,11 @@ def impact_cmd(
2924
3049
  if _copy_to_clipboard(output):
2925
3050
  typer.echo("✓ copied to clipboard", err=True)
2926
3051
 
2927
- # Non-zero exit when target not found
3052
+ # H-03: resolution=not_found is a valid structured answer, not an infra failure.
3053
+ # Exit 0 so pipelines can parse the JSON without treating it as an error.
3054
+ # Exit 1 is reserved for path-not-found, I/O failures, and real infra errors.
2928
3055
  if result.get("resolution") == "not_found":
2929
- raise typer.Exit(code=1)
3056
+ raise typer.Exit(code=0)
2930
3057
 
2931
3058
  from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
2932
3059
  _nudge()
@@ -3246,6 +3373,9 @@ def modernize_cmd(
3246
3373
  sourcecode onboard . — Architecture overview first
3247
3374
  sourcecode impact <target> — Verify impact before touching a hotspot
3248
3375
  """
3376
+ from sourcecode.license import require_pro as _require_pro
3377
+ _require_pro("modernize")
3378
+
3249
3379
  import json as _json
3250
3380
  import sys as _sys
3251
3381
  from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
@@ -3418,6 +3548,24 @@ def modernize_cmd(
3418
3548
 
3419
3549
  # ── version ───────────────────────────────────────────────────────────────────
3420
3550
 
3551
+ @app.command("activate")
3552
+ def activate_cmd(
3553
+ license_key: str = typer.Argument(..., help="Your Pro license key"),
3554
+ ) -> None:
3555
+ """Activate a Pro license key.
3556
+
3557
+ \b
3558
+ Validates the key against the license server and writes
3559
+ ~/.sourcecode/license.json.
3560
+
3561
+ \b
3562
+ Examples:
3563
+ sourcecode activate SC-XXXX-XXXX-XXXX
3564
+ """
3565
+ from sourcecode.license import activate_license as _activate
3566
+ _activate(license_key)
3567
+
3568
+
3421
3569
  @app.command("version")
3422
3570
  def version_cmd() -> None:
3423
3571
  """Show version and exit."""
@@ -3471,6 +3619,9 @@ def mcp_serve() -> None:
3471
3619
  }
3472
3620
  }
3473
3621
  """
3622
+ from sourcecode.license import require_pro as _require_pro
3623
+ _require_pro("mcp serve")
3624
+
3474
3625
  import logging
3475
3626
  import sys as _sys
3476
3627
 
@@ -478,8 +478,13 @@ class EnvAnalyzer:
478
478
  example_files_found: list,
479
479
  limitations: list,
480
480
  profiles_scanned: list,
481
+ depth: int = 0,
482
+ max_depth: int = 12,
481
483
  ) -> int:
482
484
  """Walk the directory tree accumulating env var findings. Returns spring_candidates count."""
485
+ if depth >= max_depth:
486
+ return 0
487
+
483
488
  try:
484
489
  entries = sorted(current.iterdir())
485
490
  except PermissionError:
@@ -496,7 +501,7 @@ class EnvAnalyzer:
496
501
  continue
497
502
  total_spring_candidates += self._walk(
498
503
  root, entry, findings, example_entries, example_files_found,
499
- limitations, profiles_scanned,
504
+ limitations, profiles_scanned, depth + 1, max_depth,
500
505
  )
501
506
  elif entry.is_file():
502
507
  rel = entry.relative_to(root).as_posix()
@@ -326,6 +326,9 @@ def _parse_uncommitted(output: str) -> "UncommittedChanges":
326
326
  continue
327
327
  x, y = line[0], line[1]
328
328
  filepath = line[3:].strip()
329
+ # Renamed files: git porcelain v1 produces "old -> new"; keep only the new path.
330
+ if x == "R" and " -> " in filepath:
331
+ filepath = filepath.split(" -> ", 1)[1]
329
332
  if x == "?" and y == "?":
330
333
  untracked.append(filepath)
331
334
  else:
sourcecode/license.py ADDED
@@ -0,0 +1,229 @@
1
+ """License activation and enforcement for the sourcecode CLI.
2
+
3
+ Flow:
4
+ 1. Module imported → _init() loads ~/.sourcecode/license.json (if present)
5
+ 2. is_pro set globally (True when plan == "pro")
6
+ 3. Pro commands call require_pro(feature_name) at entry — exits 1 if not Pro
7
+ 4. `sourcecode activate <key>` calls activate_license(key) — validates via
8
+ Edge Function, writes ~/.sourcecode/license.json, exits 0 on success
9
+ 5. Cached license is re-validated every 24 h (online); network errors keep
10
+ cached state (offline-first). Server-side invalidity clears cache.
11
+
12
+ Supabase credentials (baked in; override via env vars for testing):
13
+ SOURCECODE_SUPABASE_URL — project Edge Function base URL
14
+ SOURCECODE_SUPABASE_ANON_KEY — public anon key (not a secret)
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Supabase endpoint config — hardcoded for production; override via env for dev
27
+ # ---------------------------------------------------------------------------
28
+ _SUPABASE_URL: str = os.environ.get(
29
+ "SOURCECODE_SUPABASE_URL",
30
+ "https://qkndlmyekvujjdgthtmz.supabase.co",
31
+ )
32
+ _SUPABASE_ANON_KEY: str = os.environ.get(
33
+ "SOURCECODE_SUPABASE_ANON_KEY",
34
+ "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
35
+ )
36
+
37
+ _LICENSE_DIR: Path = Path.home() / ".sourcecode"
38
+ _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
39
+ _CACHE_TTL_SECONDS: int = 86400 # 24 hours
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Global license state — loaded once at import time
43
+ # ---------------------------------------------------------------------------
44
+ _license_data: Optional[dict] = None
45
+ is_pro: bool = False
46
+
47
+
48
+ def _load_license_file() -> Optional[dict]:
49
+ """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
50
+ try:
51
+ if _LICENSE_FILE.exists():
52
+ raw = _LICENSE_FILE.read_text(encoding="utf-8")
53
+ return json.loads(raw)
54
+ except Exception:
55
+ pass
56
+ return None
57
+
58
+
59
+ def _call_get_license(license_key: str) -> Optional[dict]:
60
+ """POST to /get-license edge function. Returns parsed dict or None on network error."""
61
+ import urllib.error
62
+ import urllib.request
63
+
64
+ if not _SUPABASE_ANON_KEY:
65
+ return None
66
+
67
+ url = f"{_SUPABASE_URL}/functions/v1/get-license"
68
+ body = json.dumps({"license_key": license_key}).encode("utf-8")
69
+ req = urllib.request.Request(url, data=body, method="POST")
70
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
71
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
72
+ req.add_header("Content-Type", "application/json")
73
+ req.add_header("Accept", "application/json")
74
+
75
+ try:
76
+ with urllib.request.urlopen(req, timeout=8) as resp:
77
+ return json.loads(resp.read().decode("utf-8"))
78
+ except urllib.error.HTTPError as exc:
79
+ try:
80
+ return json.loads(exc.read().decode("utf-8", errors="replace"))
81
+ except Exception:
82
+ return {"valid": False, "error": f"HTTP {exc.code}"}
83
+ except Exception:
84
+ return None # Network error — caller decides what to do
85
+
86
+
87
+ def _maybe_revalidate() -> None:
88
+ """Re-validate cached license if stale. Mutates globals; never raises."""
89
+ global _license_data, is_pro
90
+
91
+ if not _license_data:
92
+ return
93
+
94
+ validated_at_str = _license_data.get("validated_at") or _license_data.get("activated_at")
95
+ if validated_at_str:
96
+ try:
97
+ validated_at = datetime.fromisoformat(validated_at_str)
98
+ if validated_at.tzinfo is None:
99
+ validated_at = validated_at.replace(tzinfo=timezone.utc)
100
+ age = (datetime.now(timezone.utc) - validated_at).total_seconds()
101
+ if age < _CACHE_TTL_SECONDS:
102
+ return
103
+ except Exception:
104
+ pass
105
+
106
+ key = _license_data.get("license_key")
107
+ if not key:
108
+ return
109
+
110
+ result = _call_get_license(key)
111
+ if result is None:
112
+ return # Network error — keep cached data (offline-first)
113
+
114
+ if not result.get("valid"):
115
+ _license_data = None
116
+ is_pro = False
117
+ try:
118
+ if _LICENSE_FILE.exists():
119
+ _LICENSE_FILE.unlink()
120
+ except Exception:
121
+ pass
122
+ return
123
+
124
+ _license_data["plan"] = result.get("plan", "pro")
125
+ _license_data["features"] = result.get("features", [])
126
+ _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
127
+ is_pro = _license_data.get("plan") == "pro"
128
+ try:
129
+ _LICENSE_FILE.write_text(
130
+ json.dumps(_license_data, indent=2, ensure_ascii=False),
131
+ encoding="utf-8",
132
+ )
133
+ except Exception:
134
+ pass
135
+
136
+
137
+ def _init() -> None:
138
+ global _license_data, is_pro
139
+ _license_data = _load_license_file()
140
+ is_pro = (
141
+ _license_data is not None
142
+ and _license_data.get("plan") == "pro"
143
+ )
144
+
145
+
146
+ _init()
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Enforcement
151
+ # ---------------------------------------------------------------------------
152
+
153
+ def require_pro(feature_name: str) -> None:
154
+ """Exit with structured JSON error when not Pro.
155
+
156
+ Re-validates stale cached license before gating (once per 24 h, online).
157
+
158
+ Example:
159
+ from sourcecode.license import require_pro
160
+ require_pro("impact")
161
+ """
162
+ if is_pro:
163
+ _maybe_revalidate()
164
+
165
+ if not is_pro:
166
+ payload = {
167
+ "error": "pro_required",
168
+ "feature": feature_name,
169
+ "message": "Run: sourcecode activate <license_key>",
170
+ }
171
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
172
+ sys.stdout.flush()
173
+ sys.exit(1)
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # Activation
178
+ # ---------------------------------------------------------------------------
179
+
180
+ def activate_license(license_key: str) -> None:
181
+ """Validate license_key via Edge Function, write ~/.sourcecode/license.json.
182
+
183
+ Outputs JSON to stdout; exits 0 on success, 1 on any failure.
184
+ Never raises — all error paths emit JSON and call sys.exit(1).
185
+ """
186
+ if not _SUPABASE_ANON_KEY:
187
+ _fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
188
+
189
+ result = _call_get_license(license_key)
190
+
191
+ if result is None:
192
+ _fail("network_error", "Could not reach license server. Check your internet connection.")
193
+
194
+ if not result.get("valid"):
195
+ _fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
196
+
197
+ if result.get("plan") != "pro":
198
+ _fail("not_pro", "This license is not a Pro license.")
199
+
200
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
201
+ now = datetime.now(timezone.utc).isoformat()
202
+ data = {
203
+ "license_key": license_key,
204
+ "plan": result["plan"],
205
+ "features": result.get("features", []),
206
+ "email": result.get("email", ""),
207
+ "activated_at": now,
208
+ "validated_at": now,
209
+ }
210
+ _LICENSE_FILE.write_text(
211
+ json.dumps(data, indent=2, ensure_ascii=False),
212
+ encoding="utf-8",
213
+ )
214
+
215
+ output = {"status": "activated", "plan": "pro", "features": data["features"]}
216
+ sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
217
+ sys.stdout.flush()
218
+
219
+
220
+ # ---------------------------------------------------------------------------
221
+ # Internal helper
222
+ # ---------------------------------------------------------------------------
223
+
224
+ def _fail(error: str, message: str) -> None:
225
+ """Emit JSON error to stdout and exit 1. Never returns."""
226
+ payload = {"error": error, "message": message}
227
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
228
+ sys.stdout.flush()
229
+ sys.exit(1)
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  from pathlib import Path
6
7
 
7
8
  _MCP_SERVERS_KEY = "mcpServers"
@@ -13,13 +14,14 @@ _ENTRY_VALUE: dict[str, object] = {
13
14
 
14
15
 
15
16
  def read_config(path: Path) -> dict:
16
- """Parse JSON config from path. Returns empty dict if missing or empty."""
17
+ """Parse JSON config from path. Returns empty dict if missing, empty, or unreadable."""
17
18
  if not path.exists():
18
19
  return {}
19
- raw = path.read_text(encoding="utf-8").strip()
20
- if not raw:
20
+ try:
21
+ text = path.read_text(encoding="utf-8")
22
+ return json.loads(text) if text.strip() else {}
23
+ except (OSError, json.JSONDecodeError):
21
24
  return {}
22
- return json.loads(raw) # type: ignore[no-any-return]
23
25
 
24
26
 
25
27
  def is_installed(config: dict) -> bool:
@@ -49,9 +51,15 @@ def remove_entry(config: dict) -> dict:
49
51
 
50
52
 
51
53
  def write_config(path: Path, config: dict) -> None:
52
- """Atomically write config as formatted JSON."""
54
+ """Atomically write config as formatted JSON using a temp file + os.replace."""
53
55
  path.parent.mkdir(parents=True, exist_ok=True)
54
- path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
56
+ tmp = path.with_suffix(".tmp")
57
+ try:
58
+ tmp.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
59
+ os.replace(tmp, path)
60
+ finally:
61
+ if tmp.exists():
62
+ tmp.unlink(missing_ok=True)
55
63
 
56
64
 
57
65
  def validate(path: Path) -> bool:
sourcecode/mcp/runner.py CHANGED
@@ -20,9 +20,9 @@ def run_command(args: list[str]) -> Any:
20
20
  Returns parsed JSON dict when output is valid JSON, else the raw string.
21
21
  Raises RuntimeError on non-zero exit or empty output.
22
22
  """
23
- from sourcecode.cli import _detected_path, _preprocess_args, app
23
+ from sourcecode.cli import _set_detected_path, _preprocess_args, app
24
24
 
25
- _detected_path[0] = "."
25
+ _set_detected_path(".")
26
26
  processed = _preprocess_args(list(args))
27
27
  result = _runner.invoke(app, processed)
28
28
 
sourcecode/mcp/server.py CHANGED
@@ -88,6 +88,27 @@ def _normalize_repo_path(path: str) -> str:
88
88
  return path
89
89
 
90
90
 
91
+ def _check_repo_path(path: str) -> "CallToolResult | None":
92
+ """H-05: Validate repo_path exists and is a directory before executing.
93
+
94
+ Returns a structured CallToolResult(isError=True) when the path is invalid,
95
+ or None when the path is valid. Must be called after _normalize_repo_path().
96
+ Early validation prevents the MCP server from hanging when the CLI exits
97
+ non-zero with an empty stdout (error went to stderr, not captured by runner).
98
+ """
99
+ if not os.path.exists(path):
100
+ return _err(
101
+ f"directory_not_found: '{path}' does not exist.",
102
+ "DIRECTORY_NOT_FOUND",
103
+ )
104
+ if not os.path.isdir(path):
105
+ return _err(
106
+ f"not_a_directory: '{path}' is not a directory.",
107
+ "NOT_A_DIRECTORY",
108
+ )
109
+ return None
110
+
111
+
91
112
  @mcp.tool()
92
113
  def get_compact_context(repo_path: str = ".", git_context: bool = False) -> dict:
93
114
  """Compact human/LLM summary of a repository (~1000-3000 tokens). USE THIS FIRST.
@@ -108,6 +129,9 @@ def get_compact_context(repo_path: str = ".", git_context: bool = False) -> dict
108
129
  if not isinstance(git_context, bool):
109
130
  return _err("git_context must be boolean", "INVALID_ARGUMENT")
110
131
  repo_path = _normalize_repo_path(repo_path)
132
+ _path_err = _check_repo_path(repo_path)
133
+ if _path_err is not None:
134
+ return _path_err
111
135
  args = [repo_path, "--compact"]
112
136
  if git_context:
113
137
  args.append("--git-context")
@@ -139,6 +163,9 @@ def get_agent_context(repo_path: str = ".", git_context: bool = False) -> dict:
139
163
  if not isinstance(git_context, bool):
140
164
  return _err("git_context must be boolean", "INVALID_ARGUMENT")
141
165
  repo_path = _normalize_repo_path(repo_path)
166
+ _path_err = _check_repo_path(repo_path)
167
+ if _path_err is not None:
168
+ return _path_err
142
169
  args = [repo_path, "--agent"]
143
170
  if git_context:
144
171
  args.append("--git-context")
@@ -174,6 +201,9 @@ def get_endpoints(repo_path: str = ".") -> dict:
174
201
  if not isinstance(repo_path, str):
175
202
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
176
203
  repo_path = _normalize_repo_path(repo_path)
204
+ _path_err = _check_repo_path(repo_path)
205
+ if _path_err is not None:
206
+ return _path_err
177
207
  return _execute(["endpoints", repo_path])
178
208
  except Exception as exc:
179
209
  return _err(
@@ -197,6 +227,9 @@ def get_module_context(repo_path: str = ".", module: str = "") -> dict:
197
227
  if not isinstance(module, str) or not module.strip():
198
228
  return _err("module must be a non-empty string", "INVALID_ARGUMENT")
199
229
  repo_path = _normalize_repo_path(repo_path)
230
+ _path_err = _check_repo_path(repo_path)
231
+ if _path_err is not None:
232
+ return _path_err
200
233
  module_path = repo_path.rstrip("/") + "/" + module.strip("/")
201
234
  return _execute([module_path, "--compact"])
202
235
  except Exception as exc:
@@ -221,6 +254,9 @@ def get_delta(repo_path: str = ".", since: str = "HEAD~1") -> dict:
221
254
  if not isinstance(since, str) or not since.strip():
222
255
  return _err("since must be a non-empty git ref", "INVALID_ARGUMENT")
223
256
  repo_path = _normalize_repo_path(repo_path)
257
+ _path_err = _check_repo_path(repo_path)
258
+ if _path_err is not None:
259
+ return _path_err
224
260
  return _execute(["prepare-context", "delta", repo_path, "--since", since])
225
261
  except Exception as exc:
226
262
  return _err(
@@ -248,6 +284,9 @@ def get_ir_summary(repo_path: str = ".") -> dict:
248
284
  if not isinstance(repo_path, str):
249
285
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
250
286
  repo_path = _normalize_repo_path(repo_path)
287
+ _path_err = _check_repo_path(repo_path)
288
+ if _path_err is not None:
289
+ return _path_err
251
290
  return _execute(["repo-ir", repo_path, "--summary-only"])
252
291
  except Exception as exc:
253
292
  return _err(
@@ -271,6 +310,9 @@ def fix_bug_context(repo_path: str = ".", symptom: str = "") -> dict:
271
310
  if not isinstance(repo_path, str):
272
311
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
273
312
  repo_path = _normalize_repo_path(repo_path)
313
+ _path_err = _check_repo_path(repo_path)
314
+ if _path_err is not None:
315
+ return _path_err
274
316
  args = ["prepare-context", "fix-bug", repo_path]
275
317
  if symptom and isinstance(symptom, str) and symptom.strip():
276
318
  args.extend(["--symptom", symptom.strip()])
@@ -297,6 +339,9 @@ def review_pr_context(repo_path: str = ".", since: str = "") -> dict:
297
339
  if not isinstance(repo_path, str):
298
340
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
299
341
  repo_path = _normalize_repo_path(repo_path)
342
+ _path_err = _check_repo_path(repo_path)
343
+ if _path_err is not None:
344
+ return _path_err
300
345
  args = ["prepare-context", "review-pr", repo_path]
301
346
  if since and isinstance(since, str) and since.strip():
302
347
  args.extend(["--since", since.strip()])
@@ -320,6 +365,9 @@ def onboard_context(repo_path: str = ".") -> dict:
320
365
  if not isinstance(repo_path, str):
321
366
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
322
367
  repo_path = _normalize_repo_path(repo_path)
368
+ _path_err = _check_repo_path(repo_path)
369
+ if _path_err is not None:
370
+ return _path_err
323
371
  return _execute(["prepare-context", "onboard", repo_path])
324
372
  except Exception as exc:
325
373
  return _err(
@@ -341,6 +389,9 @@ def explain_context(repo_path: str = ".") -> dict:
341
389
  if not isinstance(repo_path, str):
342
390
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
343
391
  repo_path = _normalize_repo_path(repo_path)
392
+ _path_err = _check_repo_path(repo_path)
393
+ if _path_err is not None:
394
+ return _path_err
344
395
  return _execute(["prepare-context", "explain", repo_path])
345
396
  except Exception as exc:
346
397
  return _err(
@@ -362,6 +413,9 @@ def refactor_context(repo_path: str = ".") -> dict:
362
413
  if not isinstance(repo_path, str):
363
414
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
364
415
  repo_path = _normalize_repo_path(repo_path)
416
+ _path_err = _check_repo_path(repo_path)
417
+ if _path_err is not None:
418
+ return _path_err
365
419
  return _execute(["prepare-context", "refactor", repo_path])
366
420
  except Exception as exc:
367
421
  return _err(
@@ -388,6 +442,9 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
388
442
  if not isinstance(include_all, bool):
389
443
  return _err("include_all must be boolean", "INVALID_ARGUMENT")
390
444
  repo_path = _normalize_repo_path(repo_path)
445
+ _path_err = _check_repo_path(repo_path)
446
+ if _path_err is not None:
447
+ return _path_err
391
448
  args = ["prepare-context", "generate-tests", repo_path]
392
449
  if include_all:
393
450
  args.append("--all")
@@ -397,18 +454,21 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
397
454
  timeout_s = timeout_ms / 1000.0
398
455
 
399
456
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
400
- future = executor.submit(_execute, args)
401
- done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
402
- if _not_done:
403
- executor.shutdown(wait=False)
404
- return _ok({
405
- "truncated": True,
406
- "truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
407
- "files_analyzed": 0,
408
- "results": [],
409
- })
410
- executor.shutdown(wait=False)
411
- return future.result()
457
+ try:
458
+ future = executor.submit(_execute, args)
459
+ done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
460
+ if _not_done:
461
+ executor.shutdown(wait=False)
462
+ return _ok({
463
+ "truncated": True,
464
+ "truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
465
+ "files_analyzed": 0,
466
+ "results": [],
467
+ })
468
+ result = future.result()
469
+ finally:
470
+ executor.shutdown(wait=True)
471
+ return result
412
472
 
413
473
  except Exception as exc:
414
474
  return _err(
@@ -444,6 +504,9 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
444
504
  if not isinstance(depth, int) or depth < 1 or depth > 8:
445
505
  return _err("depth must be an integer between 1 and 8", "INVALID_ARGUMENT")
446
506
  repo_path = _normalize_repo_path(repo_path)
507
+ _path_err = _check_repo_path(repo_path)
508
+ if _path_err is not None:
509
+ return _path_err
447
510
  args = ["impact", target.strip(), repo_path, "--depth", str(depth)]
448
511
  return _execute(args)
449
512
  except Exception as exc:
@@ -472,9 +535,12 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
472
535
  try:
473
536
  if not isinstance(repo_path, str):
474
537
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
475
- if not isinstance(format, str) or format not in ("json", "yaml"):
476
- return _err("format must be 'json' or 'yaml'", "INVALID_ARGUMENT")
538
+ if not isinstance(format, str) or format != "json":
539
+ return _err("format must be 'json' yaml is not supported for modernize output", "INVALID_ARGUMENT")
477
540
  repo_path = _normalize_repo_path(repo_path)
541
+ _path_err = _check_repo_path(repo_path)
542
+ if _path_err is not None:
543
+ return _path_err
478
544
  return _execute(["modernize", repo_path])
479
545
  except Exception as exc:
480
546
  return _err(
sourcecode/mcp_nudge.py CHANGED
@@ -3,7 +3,12 @@
3
3
  Fires when:
4
4
  1. At least one known MCP client (Claude Desktop, Cursor) is installed
5
5
  2. sourcecode is NOT yet registered in that client's config
6
- 3. The nudge hasn't been shown this session (~/.sourcecode/nudge_shown flag)
6
+ 3. The nudge hasn't been shown yet (~/.sourcecode/nudge_shown flag absent)
7
+
8
+ The sentinel flag (~/.sourcecode/nudge_shown) persists globally on the
9
+ filesystem — it is NOT session-scoped. Once written it suppresses all future
10
+ nudges across all terminal sessions and process invocations until it is
11
+ deleted (which `sourcecode mcp init` does on successful installation).
7
12
 
8
13
  Cleared by: a successful `sourcecode mcp init` (deletes the flag so the
9
14
  post-init detection finds is_installed=True and never nudges again).
sourcecode/redactor.py CHANGED
@@ -23,6 +23,10 @@ _SECRET_PATTERNS: list[re.Pattern[str]] = [
23
23
  re.compile(r"sk-[A-Za-z0-9]{48}"), # OpenAI legacy key
24
24
  re.compile(r"AKIA[0-9A-Z]{16}"), # AWS Access Key ID
25
25
  re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*"), # Bearer tokens
26
+ re.compile(r"sk-ant-[A-Za-z0-9\-_]{32,}"), # Anthropic API key
27
+ re.compile(r"hf_[A-Za-z0-9]{32,}"), # Hugging Face token
28
+ re.compile(r"sk_live_[A-Za-z0-9]{24,}"), # Stripe live secret key
29
+ re.compile(r"pk_live_[A-Za-z0-9]{24,}"), # Stripe live publishable key
26
30
  ]
27
31
 
28
32
  # Patrones de nombres de fichero que deben excluirse (SEC-02)
@@ -1524,6 +1524,31 @@ def _get_git_old_content(git_root: Path, rel_path: str, since: str) -> Optional[
1524
1524
  return None
1525
1525
 
1526
1526
 
1527
+ def _get_git_changed_files(root: "Path", since: str) -> "Optional[frozenset[str]]":
1528
+ """H-04: Return set of paths changed between `since` and HEAD, or None on failure.
1529
+
1530
+ One `git diff --name-only` call replaces O(n) `git show` calls — only files
1531
+ in the returned set need old-content fetched for symbol diff computation.
1532
+ Returns None when git is unavailable or the ref cannot be resolved; the
1533
+ caller must fall back to the original per-file fetch in that case.
1534
+ """
1535
+ try:
1536
+ result = subprocess.run(
1537
+ ["git", "diff", "--name-only", since, "HEAD"],
1538
+ cwd=str(root),
1539
+ capture_output=True,
1540
+ text=True,
1541
+ encoding="utf-8",
1542
+ errors="replace",
1543
+ timeout=10,
1544
+ )
1545
+ if result.returncode == 0:
1546
+ return frozenset(p.strip() for p in result.stdout.splitlines() if p.strip())
1547
+ except (subprocess.TimeoutExpired, OSError, FileNotFoundError):
1548
+ pass
1549
+ return None
1550
+
1551
+
1527
1552
  # ---------------------------------------------------------------------------
1528
1553
  # Phase 5 — Evidence Engine
1529
1554
  # ---------------------------------------------------------------------------
@@ -2511,6 +2536,12 @@ def build_repo_ir(
2511
2536
  all_changed: list[ChangedSymbol] = []
2512
2537
  all_route_diffs: list[dict] = []
2513
2538
 
2539
+ # H-04: prefetch changed-file list once; avoids O(n) `git show` calls.
2540
+ # _since_changed=None means git unavailable → fall back to per-file fetch.
2541
+ _since_changed: "Optional[frozenset[str]]" = None
2542
+ if since:
2543
+ _since_changed = _get_git_changed_files(root, since)
2544
+
2514
2545
  for rel_path in sorted(file_paths):
2515
2546
  abs_path = root / rel_path
2516
2547
  try:
@@ -2520,7 +2551,11 @@ def build_repo_ir(
2520
2551
 
2521
2552
  old_source: Optional[str] = None
2522
2553
  if since:
2523
- old_source = _get_git_old_content(root, rel_path, since)
2554
+ # Only fetch old content for files known to have changed.
2555
+ # Unchanged files have no diff entries — skip git show entirely.
2556
+ _file_changed = _since_changed is None or rel_path in _since_changed
2557
+ if _file_changed:
2558
+ old_source = _get_git_old_content(root, rel_path, since)
2524
2559
 
2525
2560
  package, symbols, raw_imports = _extract_symbols(source, rel_path)
2526
2561
  relations = _build_relations(symbols, raw_imports, source, package, rel_path)
@@ -2529,7 +2564,8 @@ def build_repo_ir(
2529
2564
  _, old_symbols, _ = _extract_symbols(old_source, rel_path)
2530
2565
  all_changed.extend(_diff_symbols(old_symbols, symbols))
2531
2566
  all_route_diffs.extend(_diff_routes(old_symbols, symbols))
2532
- elif since:
2567
+ elif since and (_since_changed is None or rel_path in _since_changed):
2568
+ # File is new in since..HEAD (not in old ref) — treat as added.
2533
2569
  for sym in symbols:
2534
2570
  all_changed.append(ChangedSymbol(
2535
2571
  symbol=sym.symbol,
@@ -3210,7 +3246,7 @@ def compute_blast_radius(
3210
3246
  if _hub_class_guard and direct_callers:
3211
3247
  _n_direct = len(direct_callers)
3212
3248
  _k = min(_HUB_SAMPLE_SIZE, _n_direct)
3213
- _sample_seeds = random.sample(direct_callers, _k)
3249
+ _sample_seeds = sorted(direct_callers, key=lambda x: str(x))[:_k]
3214
3250
  _sample_visited: set[str] = set(matched_fqns) | set(direct_callers)
3215
3251
  _sample_queue: list[tuple[str, int]] = [(c, 1) for c in _sample_seeds]
3216
3252
  _sample_indirect: list[str] = []
sourcecode/serializer.py CHANGED
@@ -190,7 +190,7 @@ def _dependency_import_index(root: Path, file_paths: list[str]) -> set[str]:
190
190
  r"require\(['\"]([^'\"]+)['\"]\)|from\s+['\"]([^'\"]+)['\"])",
191
191
  re.MULTILINE,
192
192
  )
193
- for path in file_paths[:2000]:
193
+ for path in file_paths[:500]:
194
194
  if Path(path).suffix.lower() not in {".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}:
195
195
  continue
196
196
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.30
3
+ Version: 1.31.32
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
225
225
 
226
226
  **AI-ready change intelligence for Java/Spring enterprise monoliths.**
227
227
 
228
- ![Version](https://img.shields.io/badge/version-1.31.30-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.32-blue)
229
229
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
230
230
 
231
231
  ---
@@ -263,7 +263,7 @@ pipx install sourcecode
263
263
 
264
264
  ```bash
265
265
  sourcecode version
266
- # sourcecode 1.31.30
266
+ # sourcecode 1.31.32
267
267
  ```
268
268
 
269
269
  ---
@@ -1,12 +1,12 @@
1
- sourcecode/__init__.py,sha256=thS4KBhwEMTmgKe-XGLoD9avfVdy_-cdN7sf6C2_kA0,104
1
+ sourcecode/__init__.py,sha256=FAgcPCebMe_1K921d8TjlfM7T_GMcbLwHvBZvp_uat8,104
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=Ry3aYT9dc7XuLmWLT5IZ93RkCf_P14Qtew0nGPvUl_8,42184
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,50578
6
- sourcecode/cache.py,sha256=TiYa3ECjBKtvlfCk7GvQ9v6gZkAITpH3ow9PubA7sUo,22946
6
+ sourcecode/cache.py,sha256=pBrPdpPrOgpXHHQO670U3aUfVf5N3A3obsTKgiZtN4I,23030
7
7
  sourcecode/canonical_ir.py,sha256=_HM3AUmKSdna9u4dCoU6rpgSA6HdF8gzOKZykIUCNGY,23277
8
8
  sourcecode/classifier.py,sha256=yWeq6agTjkFa3zuNa-gdVIHtjoBoPoVlJnX-b7tdVJs,7851
9
- sourcecode/cli.py,sha256=7vwOxUdbWcKz42DnxKKp0TyxVhBmbl_eCOvcqct_gkw,155247
9
+ sourcecode/cli.py,sha256=7tkUpZEnXjU_swX1wadkMnfYS0A0p0NvSRZEBIOLLi0,161521
10
10
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
11
11
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
12
12
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -17,12 +17,13 @@ sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo
17
17
  sourcecode/dependency_analyzer.py,sha256=Po7GKJnClCkXty0np1B4F1zo_bPeKAtgbehazhXuaBM,56493
18
18
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
19
19
  sourcecode/entrypoint_classifier.py,sha256=MTa7yqbeuJ9XPbGCPuvtR9IqY-SN3hoXXyVtb3iXDhs,4316
20
- sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
20
+ sourcecode/env_analyzer.py,sha256=9_q_U_Q1tjiY5FSaeEImSDgToP2uHGrjBAFF7ihJn2I,22204
21
21
  sourcecode/file_classifier.py,sha256=QrYm7MlG29HQdAR1WOfpnIIBysAz62c5coz9eQ76meo,12892
22
22
  sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
23
- sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13177
23
+ sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
24
24
  sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
25
- sourcecode/mcp_nudge.py,sha256=rJzr_dirC6L3VMsUBmzsSSJb-j-nphZy-qq7i2JPjEQ,2625
25
+ sourcecode/license.py,sha256=oiAxBXHf1mz42rrDKQCPX28W2yaAHvWqaVZuijJGSF8,7902
26
+ sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
26
27
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
27
28
  sourcecode/output_budget.py,sha256=43307mJEyUPU3MI-QEQoVxrcAvNyUzdzF_SAPgisBQE,6603
28
29
  sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
@@ -30,15 +31,15 @@ sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrf
30
31
  sourcecode/prepare_context.py,sha256=RM7ka0rduJy8kwGHzLU9if6q7D9ST7tGjOf5LnsdTuw,201451
31
32
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
32
33
  sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
33
- sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
34
+ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
34
35
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
35
36
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
36
- sourcecode/repository_ir.py,sha256=gzgveIWxgT77JwUxS2dxEuLp6UbVnrHEkVo4a8x4QfY,151066
37
+ sourcecode/repository_ir.py,sha256=rga0I9L0K-3kSMTF8R8slBH4Fkwwrh-ut4Y3h6GxLvY,152757
37
38
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
38
39
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
39
40
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
40
41
  sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
41
- sourcecode/serializer.py,sha256=7TzN2GLtIP3PIVatoB98_7DQdoAkUNvvNVU7Bz7r_K8,123313
42
+ sourcecode/serializer.py,sha256=3JBvsMDj5pt1RXpi1zJpk5DGWUKbeb2Jl622-kmYWD4,123312
42
43
  sourcecode/summarizer.py,sha256=BMHJA0Do4rBnabc1_BxHoETTNb5ew0VqCX_eY3_PdCg,20706
43
44
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
44
45
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -64,10 +65,10 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
64
65
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
65
66
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
66
67
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
67
- sourcecode/mcp/runner.py,sha256=7PnFjKYbgxFeDnqVeSntXHxZX7ZtK3-krDkEuVjI24M,1386
68
- sourcecode/mcp/server.py,sha256=DVXWcmGiAjNQe3fxJS-AUH03rsA_VFjS6048f8RMkt0,21540
68
+ sourcecode/mcp/runner.py,sha256=YSw2DXEICau6mCBr3Gfia3D_tKxMbRvIIXEh4cHC1SY,1390
69
+ sourcecode/mcp/server.py,sha256=zbc4G5j2VAlVnluAksoSk6zV_wwgDevaH5Q4oVsl2kA,24018
69
70
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
70
- sourcecode/mcp/onboarding/applier.py,sha256=yfSMT0NKdZsjavtLkC8yQ7OtkfepOl5IXGByqg6bdEY,1894
71
+ sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
71
72
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
72
73
  sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
73
74
  sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
@@ -77,8 +78,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
77
78
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
78
79
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
79
80
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
80
- sourcecode-1.31.30.dist-info/METADATA,sha256=R8F2pFXTmotZdlErM06krEELPX74sev65gQk0WUvtrA,31103
81
- sourcecode-1.31.30.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
82
- sourcecode-1.31.30.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
83
- sourcecode-1.31.30.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
84
- sourcecode-1.31.30.dist-info/RECORD,,
81
+ sourcecode-1.31.32.dist-info/METADATA,sha256=69Ai2xiH7kn4Zmsm-KhGAfTTXtUbmyyy9EgLSSQ9GU4,31103
82
+ sourcecode-1.31.32.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
83
+ sourcecode-1.31.32.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
84
+ sourcecode-1.31.32.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
85
+ sourcecode-1.31.32.dist-info/RECORD,,