sourcecode 1.31.31__py3-none-any.whl → 1.32.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.
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.31"
3
+ __version__ = "1.32.0"
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)
@@ -176,9 +177,20 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
176
177
  }
177
178
  )
178
179
 
179
- # Mutable container holding the path extracted by _preprocess_argv().
180
- # Default "." means "current directory" when no path is given.
181
- _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
182
194
 
183
195
 
184
196
  # Options that take a value token — their next arg must not be treated as a path.
@@ -214,6 +226,7 @@ def _preprocess_args(args: list[str]) -> list[str]:
214
226
  """
215
227
  result = list(args)
216
228
  skip_next = False
229
+ _path_index: int = -1
217
230
  for i, arg in enumerate(result):
218
231
  if skip_next:
219
232
  skip_next = False
@@ -227,9 +240,11 @@ def _preprocess_args(args: list[str]) -> list[str]:
227
240
  if arg in _SUBCOMMANDS:
228
241
  return result # known subcommand — leave for Click to dispatch
229
242
  # First genuine positional: treat as repository path
230
- _detected_path[0] = arg
231
- result.pop(i)
232
- return result
243
+ _set_detected_path(arg)
244
+ _path_index = i
245
+ break
246
+ if _path_index >= 0:
247
+ result.pop(_path_index)
233
248
  return result
234
249
 
235
250
 
@@ -334,7 +349,7 @@ def _get_command_with_preprocessing(typer_instance: Any) -> Any:
334
349
  def _cmd_main(args: Optional[list[str]] = None, **kwargs: Any) -> Any:
335
350
  if args is not None:
336
351
  # CliRunner / programmatic call: preprocess the explicit args list.
337
- _detected_path[0] = "."
352
+ _set_detected_path(".")
338
353
  args = _preprocess_args(list(args))
339
354
  # args=None → Click reads sys.argv; _preprocess_argv() in main_entry handled it.
340
355
  return _orig_cmd_main(args=args, **kwargs)
@@ -363,7 +378,7 @@ app.add_typer(mcp_app, name="mcp")
363
378
  def _maybe_ask_consent() -> None:
364
379
  """Show first-run consent prompt once, on interactive TTYs only."""
365
380
  try:
366
- from sourcecode.telemetry.config import has_been_asked, mark_asked, set_enabled
381
+ from sourcecode.telemetry.config import has_been_asked, set_enabled
367
382
  from sourcecode.telemetry.consent import ask_for_consent
368
383
  if not has_been_asked():
369
384
  enabled = ask_for_consent()
@@ -766,6 +781,11 @@ def main(
766
781
  err=True,
767
782
  )
768
783
 
784
+ # Pro gate for --full: removing truncation limits is enterprise-scale functionality.
785
+ if full:
786
+ from sourcecode.license import require_feature as _req_full
787
+ _req_full("--full")
788
+
769
789
  # P0-2 FIX: --compact and --full are mutually exclusive.
770
790
  # compact is designed to be a bounded summary; --full removes truncation limits,
771
791
  # which contradicts compact's purpose. Use --agent --full for expanded output.
@@ -833,7 +853,7 @@ def main(
833
853
  # Path was extracted from argv by _preprocess_argv() before Click ran.
834
854
  # FIX-P2-8: preserve original user input in error messages (Windows Git Bash
835
855
  # rewrites "/nonexistent" → "C:\Program Files\Git\nonexistent" via Path.resolve()).
836
- _raw_path_input = _detected_path[0]
856
+ _raw_path_input = _get_detected_path()
837
857
  target = Path(_raw_path_input).resolve()
838
858
  if not target.exists():
839
859
  _emit_error_json(
@@ -852,6 +872,7 @@ def main(
852
872
 
853
873
  # Normalize mode aliases
854
874
  _CONTRACT_MODES = frozenset({"contract", "minimal", "standard"})
875
+ _user_mode_explicit = mode not in ("contract",) # track if user passed a non-default value
855
876
  if mode == "minimal":
856
877
  mode = "contract" # minimal is a documented alias for contract
857
878
  elif mode not in _CONTRACT_MODES and mode != "raw":
@@ -872,6 +893,20 @@ def main(
872
893
  or docs or semantics or full_metrics or architecture
873
894
  )
874
895
  if mode in ("contract", "standard") and _legacy_flags_active:
896
+ if _user_mode_explicit:
897
+ _overriding_flags = [
898
+ f for f, v in [
899
+ ("--compact", compact), ("--tree", tree),
900
+ ("--trace-pipeline", trace_pipeline), ("--docs", docs),
901
+ ("--semantics", semantics), ("--full-metrics", full_metrics),
902
+ ("--architecture", architecture),
903
+ ] if v
904
+ ]
905
+ typer.echo(
906
+ f"[warning] --mode {mode} was overridden to raw because legacy flags "
907
+ f"({', '.join(_overriding_flags)}) require raw output mode.",
908
+ err=True,
909
+ )
875
910
  mode = "raw"
876
911
 
877
912
  # Map mode to contract_view depth
@@ -995,7 +1030,7 @@ def main(
995
1030
  f"cn={code_notes},mode={mode},"
996
1031
  f"ex={_excl_key},depth={effective_depth}"
997
1032
  )
998
- _core_h = _hashlib.md5(_core_flags_str.encode()).hexdigest()[:8]
1033
+ _core_h = _hashlib.sha256(_core_flags_str.encode()).hexdigest()[:8]
999
1034
  _core_key = f"{_git_sha}-{_core_h}"
1000
1035
 
1001
1036
  # ── View flags: output presentation only (no re-analysis needed) ──
@@ -1007,7 +1042,7 @@ def main(
1007
1042
  f"mn={max_nodes},ge={graph_edges},mi={max_importers},"
1008
1043
  f"eg={emit_graph}"
1009
1044
  )
1010
- _view_h = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1045
+ _view_h = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
1011
1046
 
1012
1047
  # ── Lookup ──────────────────────────────────────────────────────
1013
1048
  # Step 1: try L1 to obtain the core_hash needed for L2 key
@@ -1961,7 +1996,7 @@ def main(
1961
1996
  if _written_core_hash:
1962
1997
  if not _view_key:
1963
1998
  # _view_key not set (L1 was also a miss); compute it now
1964
- _wvh = _hashlib.md5(_view_flags_str.encode()).hexdigest()[:8]
1999
+ _wvh = _hashlib.sha256(_view_flags_str.encode()).hexdigest()[:8]
1965
2000
  _view_key = f"{_written_core_hash}-{_wvh}"
1966
2001
  _cache_mod.write_view(
1967
2002
  target,
@@ -2642,6 +2677,32 @@ def prepare_context_cmd(
2642
2677
  _pc_budget = _pc_budgets.get(task, BUDGET_EXPLAIN)
2643
2678
  out = _pc_trim(out, _pc_budget, label=task)
2644
2679
 
2680
+ # Free-tier limits: fix-bug (top-5 files) and review-pr (lightweight).
2681
+ # Pro users get the full analysis; free users get enough to see the value.
2682
+ if task in ("fix-bug", "review-pr"):
2683
+ from sourcecode.license import can_use as _tier_can_use
2684
+ if not _tier_can_use(task):
2685
+ _FREE_FILE_LIMIT = 5
2686
+ if task == "fix-bug":
2687
+ _rf = out.get("relevant_files")
2688
+ if isinstance(_rf, list) and len(_rf) > _FREE_FILE_LIMIT:
2689
+ out["relevant_files"] = _rf[:_FREE_FILE_LIMIT]
2690
+ out["tier"] = "free"
2691
+ out["tier_note"] = (
2692
+ f"Showing top {_FREE_FILE_LIMIT} files. "
2693
+ "Upgrade to Pro for complete risk-ranked analysis across all files."
2694
+ )
2695
+ else: # review-pr
2696
+ for _cap_field in ("runtime_changes", "execution_paths", "review_hotspots", "suggested_review_order"):
2697
+ _fval = out.get(_cap_field)
2698
+ if isinstance(_fval, list) and len(_fval) > _FREE_FILE_LIMIT:
2699
+ out[_cap_field] = _fval[:_FREE_FILE_LIMIT]
2700
+ out["tier"] = "free"
2701
+ out["tier_note"] = (
2702
+ "Lightweight review. Upgrade to Pro for full blast-radius analysis, "
2703
+ "complete execution paths, and CI-grade risk scoring."
2704
+ )
2705
+
2645
2706
  if format == "github-comment" and task == "review-pr":
2646
2707
  from sourcecode.pr_comment_renderer import render_github_comment
2647
2708
  _pc_content = render_github_comment(out)
@@ -3343,13 +3404,11 @@ def modernize_cmd(
3343
3404
  sourcecode onboard . — Architecture overview first
3344
3405
  sourcecode impact <target> — Verify impact before touching a hotspot
3345
3406
  """
3346
- from sourcecode.license import require_pro as _require_pro
3347
- _require_pro("modernize")
3348
-
3349
3407
  import json as _json
3350
3408
  import sys as _sys
3351
3409
  from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
3352
3410
  from sourcecode.output_budget import trim_to_budget, BUDGET_ONBOARD
3411
+ from sourcecode.license import can_use as _mod_can_use
3353
3412
 
3354
3413
  root = path.resolve()
3355
3414
  if not root.is_dir():
@@ -3451,52 +3510,73 @@ def modernize_cmd(
3451
3510
  key=lambda s: -len(s.get("members") or []),
3452
3511
  )[:10]
3453
3512
 
3454
- result = {
3455
- "workflow": "modernize",
3456
- "path": str(root),
3457
- "summary": {
3458
- "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
3459
- "total_subsystems": len(subsystems),
3460
- "high_coupling_nodes": len(coupling_nodes),
3461
- "dead_zone_candidates": len(dead_zones),
3462
- },
3463
- "hotspot_candidates": hotspots,
3464
- "high_coupling_nodes": [
3465
- {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
3466
- for n in coupling_nodes
3467
- ],
3468
- "dead_zone_candidates": [
3469
- {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
3470
- for n in dead_zones
3471
- ],
3472
- "subsystem_summary": [
3473
- {
3474
- "label": s.get("label") or s.get("name") or "",
3475
- "package_prefix": s.get("package_prefix") or s.get("pkg") or "",
3476
- "member_count": len(s.get("members") or []),
3477
- }
3478
- for s in subsystems[:15]
3479
- ],
3480
- "cross_module_tangles": [
3481
- {
3482
- "label": s.get("label") or s.get("name") or "",
3483
- "member_count": len(s.get("members") or []),
3484
- }
3485
- for s in tangle_modules
3486
- ],
3487
- # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
3488
- # hotspots filters by role=service/repository/controller; annotation types and
3489
- # value objects end up in high_coupling_nodes instead.
3490
- "recommendation": (
3491
- (
3492
- "Start with hotspot_candidates (high fan-in = highest blast radius). "
3493
- if hotspots else
3494
- "high_coupling_nodes shows the most-referenced classes — start there. "
3495
- )
3496
- + "Dead zones are safe to remove or refactor. "
3497
- + "Cross-module tangles indicate coupling worth decomposing."
3498
- ),
3513
+ _summary = {
3514
+ "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
3515
+ "total_subsystems": len(subsystems),
3516
+ "high_coupling_nodes": len(coupling_nodes),
3517
+ "dead_zone_candidates": len(dead_zones),
3499
3518
  }
3519
+ _subsystem_summary = [
3520
+ {
3521
+ "label": s.get("label") or s.get("name") or "",
3522
+ "package_prefix": s.get("package_prefix") or s.get("pkg") or "",
3523
+ "member_count": len(s.get("members") or []),
3524
+ }
3525
+ for s in subsystems[:15]
3526
+ ]
3527
+
3528
+ if not _mod_can_use("modernize"):
3529
+ # Free tier: structural discovery only — no dead zones, tangles, or full refactor list.
3530
+ result = {
3531
+ "workflow": "modernize",
3532
+ "path": str(root),
3533
+ "tier": "free",
3534
+ "tier_note": (
3535
+ "Upgrade to Pro for full analysis: dead zones, dependency tangles, "
3536
+ "refactor candidates ranked by git churn, and complete coupling graphs."
3537
+ ),
3538
+ "summary": _summary,
3539
+ "subsystem_summary": _subsystem_summary,
3540
+ "hotspot_candidates": hotspots[:3],
3541
+ "high_coupling_nodes": [
3542
+ {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
3543
+ for n in coupling_nodes[:3]
3544
+ ],
3545
+ }
3546
+ else:
3547
+ # Pro tier: full analysis.
3548
+ result = {
3549
+ "workflow": "modernize",
3550
+ "path": str(root),
3551
+ "summary": _summary,
3552
+ "hotspot_candidates": hotspots,
3553
+ "high_coupling_nodes": [
3554
+ {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
3555
+ for n in coupling_nodes
3556
+ ],
3557
+ "dead_zone_candidates": [
3558
+ {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
3559
+ for n in dead_zones
3560
+ ],
3561
+ "subsystem_summary": _subsystem_summary,
3562
+ "cross_module_tangles": [
3563
+ {
3564
+ "label": s.get("label") or s.get("name") or "",
3565
+ "member_count": len(s.get("members") or []),
3566
+ }
3567
+ for s in tangle_modules
3568
+ ],
3569
+ # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
3570
+ "recommendation": (
3571
+ (
3572
+ "Start with hotspot_candidates (high fan-in = highest blast radius). "
3573
+ if hotspots else
3574
+ "high_coupling_nodes shows the most-referenced classes — start there. "
3575
+ )
3576
+ + "Dead zones are safe to remove or refactor. "
3577
+ + "Cross-module tangles indicate coupling worth decomposing."
3578
+ ),
3579
+ }
3500
3580
 
3501
3581
  result = trim_to_budget(result, BUDGET_ONBOARD, label="modernize")
3502
3582
  output = _json.dumps(result, indent=2, ensure_ascii=False)
@@ -3589,9 +3669,6 @@ def mcp_serve() -> None:
3589
3669
  }
3590
3670
  }
3591
3671
  """
3592
- from sourcecode.license import require_pro as _require_pro
3593
- _require_pro("mcp serve")
3594
-
3595
3672
  import logging
3596
3673
  import sys as _sys
3597
3674
 
@@ -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 CHANGED
@@ -3,12 +3,14 @@
3
3
  Flow:
4
4
  1. Module imported → _init() loads ~/.sourcecode/license.json (if present)
5
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
6
+ 3. Pro commands call require_feature(feature_name) at entry — exits 1 if not Pro
7
7
  4. `sourcecode activate <key>` calls activate_license(key) — validates via
8
- Supabase REST, writes ~/.sourcecode/license.json, exits 0 on success
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.
9
11
 
10
- Supabase credentials:
11
- SOURCECODE_SUPABASE_URL — project REST endpoint
12
+ Supabase credentials (baked in; override via env vars for testing):
13
+ SOURCECODE_SUPABASE_URL — project Edge Function base URL
12
14
  SOURCECODE_SUPABASE_ANON_KEY — public anon key (not a secret)
13
15
  """
14
16
  from __future__ import annotations
@@ -21,19 +23,67 @@ from pathlib import Path
21
23
  from typing import Optional
22
24
 
23
25
  # ---------------------------------------------------------------------------
24
- # Supabase endpoint config — override via env vars
26
+ # Supabase endpoint config — hardcoded for production; override via env for dev
25
27
  # ---------------------------------------------------------------------------
26
28
  _SUPABASE_URL: str = os.environ.get(
27
29
  "SOURCECODE_SUPABASE_URL",
28
- "https://YOUR_PROJECT.supabase.co",
30
+ "https://qkndlmyekvujjdgthtmz.supabase.co",
29
31
  )
30
32
  _SUPABASE_ANON_KEY: str = os.environ.get(
31
33
  "SOURCECODE_SUPABASE_ANON_KEY",
32
- "",
34
+ "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
33
35
  )
34
36
 
35
37
  _LICENSE_DIR: Path = Path.home() / ".sourcecode"
36
38
  _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
39
+ _CACHE_TTL_SECONDS: int = 86400 # 24 hours
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Per-feature descriptions for upgrade UX
43
+ # ---------------------------------------------------------------------------
44
+ _FEATURE_INFO: dict[str, dict[str, str]] = {
45
+ "impact": {
46
+ "display": "impact",
47
+ "description": (
48
+ "Shows blast radius, callers, affected endpoints, and persistence paths in one call."
49
+ ),
50
+ "value": "Answers: what breaks if I touch this? The core risk signal before any change.",
51
+ },
52
+ "modernize": {
53
+ "display": "modernize (full)",
54
+ "description": (
55
+ "Full analysis: dead zones, refactor candidates, dependency tangles, and coupling ranked by git churn."
56
+ ),
57
+ "value": "Prioritizes where to refactor and what is safe to touch.",
58
+ },
59
+ "fix-bug": {
60
+ "display": "fix-bug (full)",
61
+ "description": "Complete risk-ranked file list with all annotation and structural signals.",
62
+ "value": "More results means less time scanning the codebase manually.",
63
+ },
64
+ "review-pr": {
65
+ "display": "review-pr (expanded)",
66
+ "description": "Full PR review: blast radius, all execution paths, security and transaction impact.",
67
+ "value": "CI-grade review — the complete picture before merging.",
68
+ },
69
+ "delta": {
70
+ "display": "prepare-context delta",
71
+ "description": "Incremental context: git-changed files with impact propagation.",
72
+ "value": "Designed for CI/CD pipelines — runs on every PR, flags risk automatically.",
73
+ },
74
+ "generate-tests": {
75
+ "display": "prepare-context generate-tests",
76
+ "description": "Test gap analysis: finds untested files with coverage recommendations.",
77
+ "value": "Reduces test debt systematically across the entire codebase.",
78
+ },
79
+ "--full": {
80
+ "display": "--full flag",
81
+ "description": (
82
+ "Removes truncation limits on transactional boundaries, DTO mappers, and large result sets."
83
+ ),
84
+ "value": "Essential for complete analysis of enterprise-scale codebases.",
85
+ },
86
+ }
37
87
 
38
88
  # ---------------------------------------------------------------------------
39
89
  # Global license state — loaded once at import time
@@ -53,6 +103,84 @@ def _load_license_file() -> Optional[dict]:
53
103
  return None
54
104
 
55
105
 
106
+ def _call_get_license(license_key: str) -> Optional[dict]:
107
+ """POST to /get-license edge function. Returns parsed dict or None on network error."""
108
+ import urllib.error
109
+ import urllib.request
110
+
111
+ if not _SUPABASE_ANON_KEY:
112
+ return None
113
+
114
+ url = f"{_SUPABASE_URL}/functions/v1/get-license"
115
+ body = json.dumps({"license_key": license_key}).encode("utf-8")
116
+ req = urllib.request.Request(url, data=body, method="POST")
117
+ req.add_header("apikey", _SUPABASE_ANON_KEY)
118
+ req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
119
+ req.add_header("Content-Type", "application/json")
120
+ req.add_header("Accept", "application/json")
121
+
122
+ try:
123
+ with urllib.request.urlopen(req, timeout=8) as resp:
124
+ return json.loads(resp.read().decode("utf-8"))
125
+ except urllib.error.HTTPError as exc:
126
+ try:
127
+ return json.loads(exc.read().decode("utf-8", errors="replace"))
128
+ except Exception:
129
+ return {"valid": False, "error": f"HTTP {exc.code}"}
130
+ except Exception:
131
+ return None # Network error — caller decides what to do
132
+
133
+
134
+ def _maybe_revalidate() -> None:
135
+ """Re-validate cached license if stale. Mutates globals; never raises."""
136
+ global _license_data, is_pro
137
+
138
+ if not _license_data:
139
+ return
140
+
141
+ validated_at_str = _license_data.get("validated_at") or _license_data.get("activated_at")
142
+ if validated_at_str:
143
+ try:
144
+ validated_at = datetime.fromisoformat(validated_at_str)
145
+ if validated_at.tzinfo is None:
146
+ validated_at = validated_at.replace(tzinfo=timezone.utc)
147
+ age = (datetime.now(timezone.utc) - validated_at).total_seconds()
148
+ if age < _CACHE_TTL_SECONDS:
149
+ return
150
+ except Exception:
151
+ pass
152
+
153
+ key = _license_data.get("license_key")
154
+ if not key:
155
+ return
156
+
157
+ result = _call_get_license(key)
158
+ if result is None:
159
+ return # Network error — keep cached data (offline-first)
160
+
161
+ if not result.get("valid"):
162
+ _license_data = None
163
+ is_pro = False
164
+ try:
165
+ if _LICENSE_FILE.exists():
166
+ _LICENSE_FILE.unlink()
167
+ except Exception:
168
+ pass
169
+ return
170
+
171
+ _license_data["plan"] = result.get("plan", "pro")
172
+ _license_data["features"] = result.get("features", [])
173
+ _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
174
+ is_pro = _license_data.get("plan") == "pro"
175
+ try:
176
+ _LICENSE_FILE.write_text(
177
+ json.dumps(_license_data, indent=2, ensure_ascii=False),
178
+ encoding="utf-8",
179
+ )
180
+ except Exception:
181
+ pass
182
+
183
+
56
184
  def _init() -> None:
57
185
  global _license_data, is_pro
58
186
  _license_data = _load_license_file()
@@ -66,27 +194,74 @@ _init()
66
194
 
67
195
 
68
196
  # ---------------------------------------------------------------------------
69
- # Enforcement
197
+ # Entitlement helpers
70
198
  # ---------------------------------------------------------------------------
71
199
 
72
- def require_pro(feature_name: str) -> None:
73
- """Exit with structured JSON error when not Pro.
200
+ def can_use(feature_name: str) -> bool:
201
+ """Return True if the current plan has access to feature_name.
202
+
203
+ Does not trigger revalidation — use require_feature() at command entry
204
+ points where you want revalidation + gating in one call.
205
+ """
206
+ return is_pro
74
207
 
75
- Call at the very top of every Pro-gated command, before any work.
208
+
209
+ def require_feature(feature_name: str) -> None:
210
+ """Exit with a clean upgrade prompt when feature_name requires Pro.
211
+
212
+ Re-validates stale cached license before gating (once per 24 h, online).
213
+
214
+ Writes human-readable context to stderr (terminal UX) and a JSON error
215
+ to stdout (backward-compatible machine-readable format).
216
+
217
+ Example:
218
+ from sourcecode.license import require_feature
219
+ require_feature("impact")
220
+ """
221
+ _maybe_revalidate()
222
+
223
+ if is_pro:
224
+ return
225
+
226
+ info = _FEATURE_INFO.get(feature_name, {})
227
+ display = info.get("display", feature_name)
228
+ description = info.get("description", "")
229
+ value = info.get("value", "")
230
+
231
+ # Human-readable upgrade prompt on stderr
232
+ lines = [f"\n '{display}' is a Pro feature."]
233
+ if description:
234
+ lines.append(f" {description}")
235
+ if value:
236
+ lines.append(f" {value}")
237
+ lines.append("")
238
+ lines.append(" Upgrade: sourcecode activate <license_key>")
239
+ lines.append("")
240
+ sys.stderr.write("\n".join(lines) + "\n")
241
+ sys.stderr.flush()
242
+
243
+ # JSON on stdout — backward-compatible for CI / MCP consumers
244
+ payload = {
245
+ "error": "pro_required",
246
+ "feature": feature_name,
247
+ "message": (
248
+ f"'{display}' requires a Pro license. "
249
+ "Run: sourcecode activate <license_key>"
250
+ ),
251
+ }
252
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
253
+ sys.stdout.flush()
254
+ sys.exit(1)
255
+
256
+
257
+ def require_pro(feature_name: str) -> None:
258
+ """Backward-compatible alias for require_feature.
76
259
 
77
260
  Example:
78
261
  from sourcecode.license import require_pro
79
262
  require_pro("impact")
80
263
  """
81
- if not is_pro:
82
- payload = {
83
- "error": "pro_required",
84
- "feature": feature_name,
85
- "message": "Run sourcecode activate <license_key>",
86
- }
87
- sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
88
- sys.stdout.flush()
89
- sys.exit(1)
264
+ require_feature(feature_name)
90
265
 
91
266
 
92
267
  # ---------------------------------------------------------------------------
@@ -94,63 +269,42 @@ def require_pro(feature_name: str) -> None:
94
269
  # ---------------------------------------------------------------------------
95
270
 
96
271
  def activate_license(license_key: str) -> None:
97
- """Validate license_key via Supabase, write ~/.sourcecode/license.json.
272
+ """Validate license_key via Edge Function, write ~/.sourcecode/license.json.
98
273
 
99
274
  Outputs JSON to stdout; exits 0 on success, 1 on any failure.
100
275
  Never raises — all error paths emit JSON and call sys.exit(1).
101
276
  """
102
- import urllib.error
103
- import urllib.request
104
-
105
- # Bail early when Supabase isn't configured yet
106
- if not _SUPABASE_ANON_KEY or _SUPABASE_URL == "https://YOUR_PROJECT.supabase.co":
107
- _fail("configuration_error", "SOURCECODE_SUPABASE_URL / SOURCECODE_SUPABASE_ANON_KEY not configured.")
108
-
109
- url = (
110
- f"{_SUPABASE_URL}/rest/v1/users"
111
- f"?license_key=eq.{license_key}"
112
- f"&select=license_key,plan,email"
113
- )
114
- req = urllib.request.Request(url)
115
- req.add_header("apikey", _SUPABASE_ANON_KEY)
116
- req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
117
- req.add_header("Accept", "application/json")
277
+ if not _SUPABASE_ANON_KEY:
278
+ _fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
118
279
 
119
- try:
120
- with urllib.request.urlopen(req, timeout=10) as resp:
121
- body = resp.read().decode("utf-8")
122
- except urllib.error.HTTPError as exc:
123
- _fail("network_error", f"Supabase returned HTTP {exc.code}")
124
- except Exception as exc:
125
- _fail("network_error", str(exc))
280
+ result = _call_get_license(license_key)
126
281
 
127
- try:
128
- rows = json.loads(body)
129
- except Exception:
130
- _fail("network_error", "Invalid JSON response from Supabase")
282
+ if result is None:
283
+ _fail("network_error", "Could not reach license server. Check your internet connection.")
131
284
 
132
- if not rows:
133
- _fail("invalid_license", "License key not found")
285
+ if not result.get("valid"):
286
+ _fail("invalid_license", result.get("error", "License key is not valid or subscription is inactive."))
134
287
 
135
- user = rows[0]
136
- if user.get("plan") != "pro":
137
- _fail("not_pro", "This license is not Pro")
288
+ if result.get("plan") != "pro":
289
+ _fail("not_pro", "This license is not a Pro license.")
138
290
 
139
- # Write license file
140
291
  _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
292
+ now = datetime.now(timezone.utc).isoformat()
141
293
  data = {
142
294
  "license_key": license_key,
143
- "plan": "pro",
144
- "email": user.get("email", ""),
145
- "activated_at": datetime.now(timezone.utc).isoformat(),
295
+ "plan": result["plan"],
296
+ "features": result.get("features", []),
297
+ "email": result.get("email", ""),
298
+ "activated_at": now,
299
+ "validated_at": now,
146
300
  }
147
301
  _LICENSE_FILE.write_text(
148
302
  json.dumps(data, indent=2, ensure_ascii=False),
149
303
  encoding="utf-8",
150
304
  )
151
305
 
152
- result = {"status": "activated", "plan": "pro"}
153
- sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
306
+ output = {"status": "activated", "plan": "pro", "features": data["features"]}
307
+ sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
154
308
  sys.stdout.flush()
155
309
 
156
310
 
@@ -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
@@ -454,18 +454,21 @@ def generate_tests_context(repo_path: str = ".", include_all: bool = False) -> d
454
454
  timeout_s = timeout_ms / 1000.0
455
455
 
456
456
  executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
457
- future = executor.submit(_execute, args)
458
- done, _not_done = concurrent.futures.wait([future], timeout=timeout_s)
459
- if _not_done:
460
- executor.shutdown(wait=False)
461
- return _ok({
462
- "truncated": True,
463
- "truncated_reason": f"timeout_{timeout_ms // 1000}s" if timeout_ms >= 1000 else f"timeout_{timeout_ms}ms",
464
- "files_analyzed": 0,
465
- "results": [],
466
- })
467
- executor.shutdown(wait=False)
468
- 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
469
472
 
470
473
  except Exception as exc:
471
474
  return _err(
@@ -532,8 +535,8 @@ def modernize_context(repo_path: str = ".", format: str = "json") -> dict:
532
535
  try:
533
536
  if not isinstance(repo_path, str):
534
537
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
535
- if not isinstance(format, str) or format not in ("json", "yaml"):
536
- 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")
537
540
  repo_path = _normalize_repo_path(repo_path)
538
541
  _path_err = _check_repo_path(repo_path)
539
542
  if _path_err is not None:
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)
@@ -3246,7 +3246,7 @@ def compute_blast_radius(
3246
3246
  if _hub_class_guard and direct_callers:
3247
3247
  _n_direct = len(direct_callers)
3248
3248
  _k = min(_HUB_SAMPLE_SIZE, _n_direct)
3249
- _sample_seeds = random.sample(direct_callers, _k)
3249
+ _sample_seeds = sorted(direct_callers, key=lambda x: str(x))[:_k]
3250
3250
  _sample_visited: set[str] = set(matched_fqns) | set(direct_callers)
3251
3251
  _sample_queue: list[tuple[str, int]] = [(c, 1) for c in _sample_seeds]
3252
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.31
3
+ Version: 1.32.0
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.31-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.32.0-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.31
266
+ # sourcecode 1.32.0
267
267
  ```
268
268
 
269
269
  ---
@@ -1,12 +1,12 @@
1
- sourcecode/__init__.py,sha256=1Q8tXMLCUF_dRIEoNcIN_LLVJVN4jXymK6NlDJ1O1sA,104
1
+ sourcecode/__init__.py,sha256=1pcmq6UuzqBpI1Q4E_5ukKd_IJ8s8CN4xrW1_EyV0Gw,103
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=757JuVOVVPLsWvqui4jQCaoDew0vSIg4l3ookLFdMqU,160341
9
+ sourcecode/cli.py,sha256=7GfBpmy2_6lUbuNz8zft4Vof-WOeGnzpeSNwxzDQndM,163863
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,13 +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/license.py,sha256=VuGBBnUvlDvGm_PA-mCL8WuxQnD4Fa23SG6xxVzFpfk,5461
26
- sourcecode/mcp_nudge.py,sha256=rJzr_dirC6L3VMsUBmzsSSJb-j-nphZy-qq7i2JPjEQ,2625
25
+ sourcecode/license.py,sha256=m5n4PKZ7ZZZ17bpLjIsKwd94PXaIg5iS_2kFNjCV2Og,11378
26
+ sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
27
27
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
28
28
  sourcecode/output_budget.py,sha256=43307mJEyUPU3MI-QEQoVxrcAvNyUzdzF_SAPgisBQE,6603
29
29
  sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
@@ -31,15 +31,15 @@ sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrf
31
31
  sourcecode/prepare_context.py,sha256=RM7ka0rduJy8kwGHzLU9if6q7D9ST7tGjOf5LnsdTuw,201451
32
32
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
33
33
  sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
34
- sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
34
+ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
35
35
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
36
36
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
37
- sourcecode/repository_ir.py,sha256=y6d_ldrgolNwAiniWe54tR5HUE-UsxfYe5zDS_GNmrY,152741
37
+ sourcecode/repository_ir.py,sha256=rga0I9L0K-3kSMTF8R8slBH4Fkwwrh-ut4Y3h6GxLvY,152757
38
38
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
39
39
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
40
40
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
41
41
  sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
42
- sourcecode/serializer.py,sha256=7TzN2GLtIP3PIVatoB98_7DQdoAkUNvvNVU7Bz7r_K8,123313
42
+ sourcecode/serializer.py,sha256=3JBvsMDj5pt1RXpi1zJpk5DGWUKbeb2Jl622-kmYWD4,123312
43
43
  sourcecode/summarizer.py,sha256=BMHJA0Do4rBnabc1_BxHoETTNb5ew0VqCX_eY3_PdCg,20706
44
44
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
45
45
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -65,10 +65,10 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
65
65
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
66
66
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
67
67
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
68
- sourcecode/mcp/runner.py,sha256=7PnFjKYbgxFeDnqVeSntXHxZX7ZtK3-krDkEuVjI24M,1386
69
- sourcecode/mcp/server.py,sha256=YHhKKwhB0u-DsslX_zptJpplz3i3a_I4svrJg8IKgb4,23894
68
+ sourcecode/mcp/runner.py,sha256=YSw2DXEICau6mCBr3Gfia3D_tKxMbRvIIXEh4cHC1SY,1390
69
+ sourcecode/mcp/server.py,sha256=zbc4G5j2VAlVnluAksoSk6zV_wwgDevaH5Q4oVsl2kA,24018
70
70
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
71
- sourcecode/mcp/onboarding/applier.py,sha256=yfSMT0NKdZsjavtLkC8yQ7OtkfepOl5IXGByqg6bdEY,1894
71
+ sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
72
72
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
73
73
  sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
74
74
  sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
@@ -78,8 +78,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
78
78
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
79
79
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
80
80
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
81
- sourcecode-1.31.31.dist-info/METADATA,sha256=25SDVwl1Kx6PdAtuhy9QVT9WTSIFDVX_yXyMuLAzRsY,31103
82
- sourcecode-1.31.31.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
83
- sourcecode-1.31.31.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
84
- sourcecode-1.31.31.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
85
- sourcecode-1.31.31.dist-info/RECORD,,
81
+ sourcecode-1.32.0.dist-info/METADATA,sha256=FHXYjifmhRxjdL38qMdsKuEz9_H2nJIrodG2c7R2LHM,31100
82
+ sourcecode-1.32.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
83
+ sourcecode-1.32.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
84
+ sourcecode-1.32.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
85
+ sourcecode-1.32.0.dist-info/RECORD,,