sourcecode 1.35.15__tar.gz → 1.35.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {sourcecode-1.35.15 → sourcecode-1.35.17}/PKG-INFO +4 -3
  2. {sourcecode-1.35.15 → sourcecode-1.35.17}/README.md +2 -2
  3. {sourcecode-1.35.15 → sourcecode-1.35.17}/pyproject.toml +2 -1
  4. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ast_extractor.py +1 -1
  6. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cli.py +55 -14
  7. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/dependency_analyzer.py +1 -1
  8. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/license.py +92 -13
  9. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/transport.py +4 -1
  10. {sourcecode-1.35.15 → sourcecode-1.35.17}/.github/workflows/build-windows.yml +0 -0
  11. {sourcecode-1.35.15 → sourcecode-1.35.17}/.gitignore +0 -0
  12. {sourcecode-1.35.15 → sourcecode-1.35.17}/.ruff.toml +0 -0
  13. {sourcecode-1.35.15 → sourcecode-1.35.17}/CHANGELOG.md +0 -0
  14. {sourcecode-1.35.15 → sourcecode-1.35.17}/CONTRIBUTING.md +0 -0
  15. {sourcecode-1.35.15 → sourcecode-1.35.17}/LICENSE +0 -0
  16. {sourcecode-1.35.15 → sourcecode-1.35.17}/SECURITY.md +0 -0
  17. {sourcecode-1.35.15 → sourcecode-1.35.17}/raw +0 -0
  18. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/adaptive_scanner.py +0 -0
  19. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/architecture_analyzer.py +0 -0
  20. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/architecture_summary.py +0 -0
  21. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/canonical_ir.py +0 -0
  23. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cir_graphs.py +0 -0
  24. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/__init__.py +0 -0
  33. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/base.py +0 -0
  34. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/csproj_parser.py +0 -0
  35. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/dart.py +0 -0
  36. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/dotnet.py +0 -0
  37. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/elixir.py +0 -0
  38. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/go.py +0 -0
  39. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/heuristic.py +0 -0
  40. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/hybrid.py +0 -0
  41. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/java.py +0 -0
  42. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/jvm_ext.py +0 -0
  43. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/nodejs.py +0 -0
  44. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/parsers.py +0 -0
  45. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/php.py +0 -0
  46. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/project.py +0 -0
  47. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/python.py +0 -0
  48. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/ruby.py +0 -0
  49. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/rust.py +0 -0
  50. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/systems.py +0 -0
  51. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/terraform.py +0 -0
  52. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/tooling.py +0 -0
  53. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/doc_analyzer.py +0 -0
  54. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/entrypoint_classifier.py +0 -0
  55. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/env_analyzer.py +0 -0
  56. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/error_schema.py +0 -0
  57. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/explain.py +0 -0
  58. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/file_classifier.py +0 -0
  59. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/flow_analyzer.py +0 -0
  60. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/fqn_utils.py +0 -0
  61. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/git_analyzer.py +0 -0
  62. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/graph_analyzer.py +0 -0
  63. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/__init__.py +0 -0
  64. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  65. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  66. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  67. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  68. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  69. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/orchestrator.py +0 -0
  70. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/registry.py +0 -0
  71. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/runner.py +0 -0
  72. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/server.py +0 -0
  73. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp_nudge.py +0 -0
  74. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/metrics_analyzer.py +0 -0
  75. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/output_budget.py +0 -0
  76. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/path_filters.py +0 -0
  77. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/pr_comment_renderer.py +0 -0
  78. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/pr_impact.py +0 -0
  79. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/prepare_context.py +0 -0
  80. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/progress.py +0 -0
  81. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ranking_engine.py +0 -0
  82. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/redactor.py +0 -0
  83. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/relevance_scorer.py +0 -0
  84. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/repo_classifier.py +0 -0
  85. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/repository_ir.py +0 -0
  86. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ris.py +0 -0
  87. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/runtime_classifier.py +0 -0
  88. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/scanner.py +0 -0
  89. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/schema.py +0 -0
  90. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/semantic_analyzer.py +0 -0
  91. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/serializer.py +0 -0
  92. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_event_topology.py +0 -0
  93. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_findings.py +0 -0
  94. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_impact.py +0 -0
  95. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_model.py +0 -0
  96. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_security_audit.py +0 -0
  97. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_semantic.py +0 -0
  98. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_tx_analyzer.py +0 -0
  99. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/summarizer.py +0 -0
  100. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/__init__.py +0 -0
  101. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/config.py +0 -0
  102. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/consent.py +0 -0
  103. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/events.py +0 -0
  104. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/filters.py +0 -0
  105. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/tree_utils.py +0 -0
  106. {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.15
3
+ Version: 1.35.17
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Topic :: Utilities
19
19
  Requires-Python: >=3.9
20
+ Requires-Dist: defusedxml>=0.7
20
21
  Requires-Dist: mcp>=1.0.0
21
22
  Requires-Dist: pathspec>=1.0
22
23
  Requires-Dist: ruamel-yaml>=0.18
@@ -39,7 +40,7 @@ Description-Content-Type: text/markdown
39
40
 
40
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
42
 
42
- ![Version](https://img.shields.io/badge/version-1.35.15-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.16-blue)
43
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
45
 
45
46
  ---
@@ -113,7 +114,7 @@ pipx install sourcecode
113
114
 
114
115
  ```bash
115
116
  sourcecode version
116
- # sourcecode 1.35.15
117
+ # sourcecode 1.35.16
117
118
  ```
118
119
 
119
120
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.35.15-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.16-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.15
79
+ # sourcecode 1.35.16
80
80
  ```
81
81
 
82
82
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.15"
7
+ version = "1.35.17"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "ruamel.yaml>=0.18",
32
32
  "tomli>=2.0; python_version < '3.11'",
33
33
  "mcp>=1.0.0",
34
+ "defusedxml>=0.7",
34
35
  ]
35
36
 
36
37
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.15"
3
+ __version__ = "1.35.17"
@@ -1196,7 +1196,7 @@ def _detect_role(path: str, contract: FileContract) -> str:
1196
1196
  def _extract_mybatis_xml(rel_path: str, source: str) -> FileContract:
1197
1197
  """Extract namespace and SQL operations from a MyBatis *Mapper.xml file."""
1198
1198
  import re as _re
1199
- from xml.etree import ElementTree
1199
+ import defusedxml.ElementTree as ElementTree # type: ignore[import]
1200
1200
 
1201
1201
  _NS_STRIP = _re.compile(r"\{[^}]+\}")
1202
1202
  _SQL_OPS = frozenset({"select", "insert", "update", "delete"})
@@ -197,9 +197,13 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
197
197
  [dim]modernize (full) dead zones, tangles, full coupling[/dim]
198
198
  [dim]fix-bug (full) complete risk-ranked file list[/dim]
199
199
  [dim]review-pr (expanded) CI-grade PR review[/dim]
200
- [dim]prepare-context delta incremental context for CI/CD[/dim]
200
+ [dim]prepare-context delta incremental context for CI/CD (30 free runs/repo)[/dim]
201
201
  [dim]prepare-context generate-tests test gap analysis[/dim]
202
- [dim]--full removes all truncation limits[/dim]
202
+ [dim]--full removes all truncation limits (free up to 500 files)[/dim]
203
+ [dim]--rank-by git-churn file volatility ranking via git history[/dim]
204
+ [dim]rich exports (HTML/PDF/CI) structured reports for CI and stakeholders[/dim]
205
+ [dim]multi-repo analysis cross-repository blast radius[/dim]
206
+ [dim]team snapshots shared org-level cache[/dim]
203
207
 
204
208
  [dim cyan]→ sourcecode activate <key>[/dim cyan]
205
209
  """
@@ -573,7 +577,8 @@ GRAPH_EDGE_CHOICES = {"imports", "calls", "contains", "extends"}
573
577
  DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
574
578
 
575
579
  # ── Module-level constants ─────────────────────────────────────────────────────
576
- _FREE_TIER_NODE_CAP: int = 10 # semantic cap for graph nodes and semantic symbols in free tier
580
+ _FREE_TIER_NODE_CAP: int = 50 # semantic cap for graph nodes and semantic symbols in free tier
581
+ _FREE_FULL_FILE_THRESHOLD: int = 500 # Java source files; above this --full requires Pro
577
582
  _JAVA_MIN_SCAN_DEPTH: int = 12 # Maven src/main/java/<pkg>/<module>/File depth floor
578
583
  _JVM_STACKS: frozenset[str] = frozenset({"java", "kotlin", "scala", "groovy"})
579
584
  _IMPACT_PRIORITY_THRESHOLDS: list[tuple[float, str]] = [
@@ -878,6 +883,11 @@ def main(
878
883
  )
879
884
  raise typer.Exit(code=2) # FIX-P2-7: arg validation → exit 2
880
885
 
886
+ # Pro gate for --rank-by git-churn: git history analysis is a Pro feature.
887
+ if rank_by == "git-churn":
888
+ from sourcecode.license import require_feature as _req_git_history
889
+ _req_git_history("git-history")
890
+
881
891
  if symbol is not None and not symbol.strip():
882
892
  _emit_error_json(
883
893
  INVALID_INPUT_CODE,
@@ -911,10 +921,21 @@ def main(
911
921
  )
912
922
  raise typer.Exit(code=2) # FIX-P2-7: arg validation → exit 2
913
923
 
914
- # Pro gate for --full: removing truncation limits is enterprise-scale functionality.
924
+ # Pro gate for --full: free tier allowed up to _FREE_FULL_FILE_THRESHOLD Java files.
915
925
  if full:
916
- from sourcecode.license import require_feature as _req_full
917
- _req_full("--full")
926
+ from sourcecode.license import is_pro as _full_is_pro
927
+ if not _full_is_pro:
928
+ from itertools import islice as _islice
929
+ _full_check_path = Path(_get_detected_path()).resolve()
930
+ _java_count = sum(
931
+ 1 for _ in _islice(
932
+ (p for p in _full_check_path.rglob("*.java") if ".git" not in p.parts),
933
+ _FREE_FULL_FILE_THRESHOLD + 1,
934
+ )
935
+ )
936
+ if _java_count > _FREE_FULL_FILE_THRESHOLD:
937
+ from sourcecode.license import require_feature as _req_full
938
+ _req_full("--full")
918
939
 
919
940
  # P0-2 FIX: --compact and --full are mutually exclusive.
920
941
  # compact is designed to be a bounded summary; --full removes truncation limits,
@@ -2633,14 +2654,34 @@ def prepare_context_cmd(
2633
2654
  )
2634
2655
  raise typer.Exit(code=1)
2635
2656
 
2636
- # Pro gate: generate-tests and delta require an active Pro license.
2637
- _PRO_TASKS: frozenset[str] = frozenset({"generate-tests", "delta"})
2638
- if task in _PRO_TASKS:
2657
+ # Pro gate: generate-tests requires Pro. delta allows 30 free runs per repo.
2658
+ if task == "generate-tests":
2639
2659
  from sourcecode.license import require_feature as _require_feature
2640
- _extra: dict = {}
2641
- if task == "delta":
2642
- _extra["free_tier_alternative"] = "sourcecode prepare-context review-pr --since <ref>"
2643
- _require_feature(task, extra_fields=_extra if _extra else None)
2660
+ _require_feature("generate-tests")
2661
+ elif task == "delta":
2662
+ from sourcecode.license import is_pro as _delta_is_pro
2663
+ if not _delta_is_pro:
2664
+ from sourcecode.license import check_delta_free_tier as _check_delta
2665
+ _delta_allowed, _delta_used, _delta_remaining = _check_delta(str(path.resolve()))
2666
+ if not _delta_allowed:
2667
+ from sourcecode.license import require_feature as _require_feature_delta
2668
+ _require_feature_delta(
2669
+ "delta",
2670
+ extra_fields={
2671
+ "free_tier_note": (
2672
+ f"Free quota of {30} delta runs per repository exhausted."
2673
+ ),
2674
+ "free_tier_alternative": "sourcecode prepare-context review-pr --since <ref>",
2675
+ },
2676
+ )
2677
+ # Within quota: emit a header note so CI logs show remaining runs.
2678
+ elif _delta_remaining <= 5:
2679
+ import sys as _sys_delta
2680
+ _sys_delta.stderr.write(
2681
+ f"[sourcecode] delta free tier: {_delta_remaining} run(s) remaining"
2682
+ f" (used {_delta_used}/{30}). Upgrade to Pro for unlimited CI runs.\n"
2683
+ )
2684
+ _sys_delta.stderr.flush()
2644
2685
 
2645
2686
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2646
2687
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
@@ -4892,7 +4933,7 @@ def mcp_serve() -> None:
4892
4933
  except KeyboardInterrupt:
4893
4934
  log.info("sourcecode-mcp stopped")
4894
4935
  except Exception as exc:
4895
- log.critical("sourcecode-mcp fatal error: %s", exc, exc_info=True)
4936
+ log.critical("sourcecode-mcp fatal error: %s: %s", type(exc).__name__, exc)
4896
4937
  raise typer.Exit(code=1)
4897
4938
 
4898
4939
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- import xml.etree.ElementTree as ET
4
+ import defusedxml.ElementTree as ET # type: ignore[import]
5
5
  from collections.abc import Iterable
6
6
  from dataclasses import replace
7
7
  from pathlib import Path
@@ -17,6 +17,7 @@ from __future__ import annotations
17
17
 
18
18
  import json
19
19
  import os
20
+ import re
20
21
  import sys
21
22
  from datetime import datetime, timezone
22
23
  from pathlib import Path
@@ -25,18 +26,25 @@ from typing import Optional
25
26
  # ---------------------------------------------------------------------------
26
27
  # Supabase endpoint config — hardcoded for production; override via env for dev
27
28
  # ---------------------------------------------------------------------------
28
- _SUPABASE_URL: str = os.environ.get(
29
- "SOURCECODE_SUPABASE_URL",
30
- "https://qkndlmyekvujjdgthtmz.supabase.co",
31
- )
29
+ _DEFAULT_SUPABASE_URL: str = "https://qkndlmyekvujjdgthtmz.supabase.co"
30
+ _SUPABASE_URL: str = os.environ.get("SOURCECODE_SUPABASE_URL", _DEFAULT_SUPABASE_URL)
32
31
  _SUPABASE_ANON_KEY: str = os.environ.get(
33
32
  "SOURCECODE_SUPABASE_ANON_KEY",
34
33
  "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
35
34
  )
35
+ if _SUPABASE_URL != _DEFAULT_SUPABASE_URL:
36
+ sys.stderr.write(
37
+ f"[sourcecode] WARNING: SOURCECODE_SUPABASE_URL overridden to {_SUPABASE_URL!r}."
38
+ " License requests will be sent to this server.\n"
39
+ )
40
+ sys.stderr.flush()
36
41
 
37
42
  _LICENSE_DIR: Path = Path.home() / ".sourcecode"
38
43
  _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
44
+ _DELTA_RUNS_FILE: Path = _LICENSE_DIR / "delta_runs.json"
39
45
  _CACHE_TTL_SECONDS: int = 86400 # 24 hours
46
+ _DELTA_FREE_LIMIT: int = 30
47
+ _LICENSE_KEY_RE = re.compile(r"^[A-Za-z0-9_\-]{1,200}$")
40
48
 
41
49
  # ---------------------------------------------------------------------------
42
50
  # Per-feature descriptions for upgrade UX
@@ -77,12 +85,37 @@ _FEATURE_INFO: dict[str, dict[str, str]] = {
77
85
  "value": "Reduces test debt systematically across the entire codebase.",
78
86
  },
79
87
  "--full": {
80
- "display": "--full flag",
88
+ "display": "--full flag (large repos)",
81
89
  "description": (
82
90
  "Removes truncation limits on transactional boundaries, DTO mappers, and large result sets."
91
+ " Free tier may use --full on repositories under 500 Java source files."
83
92
  ),
84
93
  "value": "Essential for complete analysis of enterprise-scale codebases.",
85
94
  },
95
+ "git-history": {
96
+ "display": "git history analysis",
97
+ "description": (
98
+ "Churn ranking, commit frequency per file, volatility signals over 90-day window."
99
+ ),
100
+ "value": "Identifies which files change most — the highest-risk targets in any refactor.",
101
+ },
102
+ "multi-repo": {
103
+ "display": "multi-repo analysis",
104
+ "description": (
105
+ "Cross-repository dependency graphs, shared module impact, and org-level blast radius."
106
+ ),
107
+ "value": "Required for microservices and monorepo architectures.",
108
+ },
109
+ "export-rich": {
110
+ "display": "rich exports (HTML/PDF/CI)",
111
+ "description": "Structured HTML reports, PDF exports, and CI-consumable risk summaries.",
112
+ "value": "Embed analysis into your CI pipeline or share with non-CLI stakeholders.",
113
+ },
114
+ "team-snapshots": {
115
+ "display": "team snapshot sharing",
116
+ "description": "Shared org-level snapshots and multi-user cache access.",
117
+ "value": "Eliminates cold-cache overhead across the entire engineering team.",
118
+ },
86
119
  }
87
120
 
88
121
  # ---------------------------------------------------------------------------
@@ -92,6 +125,55 @@ _license_data: Optional[dict] = None
92
125
  is_pro: bool = False
93
126
 
94
127
 
128
+ def _write_license_file(data: dict) -> None:
129
+ """Atomically write license data via tmp file + rename."""
130
+ payload = json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")
131
+ tmp = _LICENSE_FILE.with_suffix(".tmp")
132
+ try:
133
+ tmp.write_bytes(payload)
134
+ tmp.replace(_LICENSE_FILE)
135
+ except Exception:
136
+ try:
137
+ tmp.unlink(missing_ok=True)
138
+ except Exception:
139
+ pass
140
+ raise
141
+
142
+
143
+ def _read_delta_runs() -> dict:
144
+ try:
145
+ if _DELTA_RUNS_FILE.exists():
146
+ return json.loads(_DELTA_RUNS_FILE.read_text(encoding="utf-8"))
147
+ except Exception:
148
+ pass
149
+ return {}
150
+
151
+
152
+ def check_delta_free_tier(repo_path: str) -> "tuple[bool, int, int]":
153
+ """Check and consume one delta free-tier run for repo_path.
154
+
155
+ Returns (allowed, runs_used, runs_remaining).
156
+ When allowed=True the run count is incremented atomically.
157
+ When allowed=False the quota is exhausted — caller should gate to Pro.
158
+ """
159
+ import hashlib
160
+ key = hashlib.sha256(str(Path(repo_path).resolve()).encode()).hexdigest()[:16]
161
+ runs = _read_delta_runs()
162
+ used = int(runs.get(key, 0))
163
+ if used >= _DELTA_FREE_LIMIT:
164
+ return False, used, 0
165
+ new_used = used + 1
166
+ runs[key] = new_used
167
+ try:
168
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
169
+ tmp = _DELTA_RUNS_FILE.with_suffix(".tmp")
170
+ tmp.write_text(json.dumps(runs, indent=2, ensure_ascii=False), encoding="utf-8")
171
+ tmp.replace(_DELTA_RUNS_FILE)
172
+ except Exception:
173
+ pass
174
+ return True, new_used, max(0, _DELTA_FREE_LIMIT - new_used)
175
+
176
+
95
177
  def _load_license_file() -> Optional[dict]:
96
178
  """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
97
179
  try:
@@ -173,10 +255,7 @@ def _maybe_revalidate() -> None:
173
255
  _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
174
256
  is_pro = _license_data.get("plan") == "pro"
175
257
  try:
176
- _LICENSE_FILE.write_text(
177
- json.dumps(_license_data, indent=2, ensure_ascii=False),
178
- encoding="utf-8",
179
- )
258
+ _write_license_file(_license_data)
180
259
  except Exception:
181
260
  pass
182
261
 
@@ -284,6 +363,9 @@ def activate_license(license_key: str) -> None:
284
363
  Outputs JSON to stdout; exits 0 on success, 1 on any failure.
285
364
  Never raises — all error paths emit JSON and call sys.exit(1).
286
365
  """
366
+ if not _LICENSE_KEY_RE.match(license_key):
367
+ _fail("invalid_license", "License key format is invalid.")
368
+
287
369
  if not _SUPABASE_ANON_KEY:
288
370
  _fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
289
371
 
@@ -308,10 +390,7 @@ def activate_license(license_key: str) -> None:
308
390
  "activated_at": now,
309
391
  "validated_at": now,
310
392
  }
311
- _LICENSE_FILE.write_text(
312
- json.dumps(data, indent=2, ensure_ascii=False),
313
- encoding="utf-8",
314
- )
393
+ _write_license_file(data)
315
394
 
316
395
  output = {"status": "activated", "plan": "pro", "features": data["features"]}
317
396
  sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
@@ -20,7 +20,10 @@ _TIMEOUT_S = 3
20
20
 
21
21
 
22
22
  def _endpoint() -> str:
23
- return os.environ.get("SOURCECODE_TELEMETRY_ENDPOINT", _DEFAULT_ENDPOINT)
23
+ override = os.environ.get("SOURCECODE_TELEMETRY_ENDPOINT")
24
+ if override and override.startswith("https://"):
25
+ return override
26
+ return _DEFAULT_ENDPOINT
24
27
 
25
28
 
26
29
  def _send_blocking(payload: dict[str, Any]) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes