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.
- {sourcecode-1.35.15 → sourcecode-1.35.17}/PKG-INFO +4 -3
- {sourcecode-1.35.15 → sourcecode-1.35.17}/README.md +2 -2
- {sourcecode-1.35.15 → sourcecode-1.35.17}/pyproject.toml +2 -1
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ast_extractor.py +1 -1
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cli.py +55 -14
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/dependency_analyzer.py +1 -1
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/license.py +92 -13
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/transport.py +4 -1
- {sourcecode-1.35.15 → sourcecode-1.35.17}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/.gitignore +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/.ruff.toml +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/CHANGELOG.md +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/LICENSE +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/SECURITY.md +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/raw +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.15 → sourcecode-1.35.17}/src/sourcecode/tree_utils.py +0 -0
- {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.
|
|
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
|
-

|
|
43
44
|

|
|
44
45
|
|
|
45
46
|
---
|
|
@@ -113,7 +114,7 @@ pipx install sourcecode
|
|
|
113
114
|
|
|
114
115
|
```bash
|
|
115
116
|
sourcecode version
|
|
116
|
-
# sourcecode 1.35.
|
|
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
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.35.
|
|
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.
|
|
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]
|
|
@@ -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
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
917
|
-
|
|
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
|
|
2637
|
-
|
|
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
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
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,
|
|
4936
|
+
log.critical("sourcecode-mcp fatal error: %s: %s", type(exc).__name__, exc)
|
|
4896
4937
|
raise typer.Exit(code=1)
|
|
4897
4938
|
|
|
4898
4939
|
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|