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 +1 -1
- sourcecode/cache.py +1 -0
- sourcecode/cli.py +141 -64
- sourcecode/env_analyzer.py +6 -1
- sourcecode/git_analyzer.py +3 -0
- sourcecode/license.py +213 -59
- sourcecode/mcp/onboarding/applier.py +14 -6
- sourcecode/mcp/runner.py +2 -2
- sourcecode/mcp/server.py +17 -14
- sourcecode/mcp_nudge.py +6 -1
- sourcecode/redactor.py +4 -0
- sourcecode/repository_ir.py +1 -1
- sourcecode/serializer.py +1 -1
- {sourcecode-1.31.31.dist-info → sourcecode-1.32.0.dist-info}/METADATA +3 -3
- {sourcecode-1.31.31.dist-info → sourcecode-1.32.0.dist-info}/RECORD +18 -18
- {sourcecode-1.31.31.dist-info → sourcecode-1.32.0.dist-info}/WHEEL +0 -0
- {sourcecode-1.31.31.dist-info → sourcecode-1.32.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.31.31.dist-info → sourcecode-1.32.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
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
|
-
|
|
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
|
-
#
|
|
180
|
-
#
|
|
181
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3455
|
-
"
|
|
3456
|
-
"
|
|
3457
|
-
"
|
|
3458
|
-
|
|
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
|
|
sourcecode/env_analyzer.py
CHANGED
|
@@ -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()
|
sourcecode/git_analyzer.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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://
|
|
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
|
-
#
|
|
197
|
+
# Entitlement helpers
|
|
70
198
|
# ---------------------------------------------------------------------------
|
|
71
199
|
|
|
72
|
-
def
|
|
73
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
133
|
-
_fail("invalid_license", "License key not
|
|
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
|
-
|
|
136
|
-
|
|
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": "
|
|
144
|
-
"
|
|
145
|
-
"
|
|
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
|
-
|
|
153
|
-
sys.stdout.write(json.dumps(
|
|
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
|
|
17
|
+
"""Parse JSON config from path. Returns empty dict if missing, empty, or unreadable."""
|
|
17
18
|
if not path.exists():
|
|
18
19
|
return {}
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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
|
|
23
|
+
from sourcecode.cli import _set_detected_path, _preprocess_args, app
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
536
|
-
return _err("format must be 'json'
|
|
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
|
|
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)
|
sourcecode/repository_ir.py
CHANGED
|
@@ -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 =
|
|
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[:
|
|
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.
|
|
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
|
-

|
|
229
229
|

|
|
230
230
|
|
|
231
231
|
---
|
|
@@ -263,7 +263,7 @@ pipx install sourcecode
|
|
|
263
263
|
|
|
264
264
|
```bash
|
|
265
265
|
sourcecode version
|
|
266
|
-
# sourcecode 1.
|
|
266
|
+
# sourcecode 1.32.0
|
|
267
267
|
```
|
|
268
268
|
|
|
269
269
|
---
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
26
|
-
sourcecode/mcp_nudge.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
69
|
-
sourcecode/mcp/server.py,sha256=
|
|
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=
|
|
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.
|
|
82
|
-
sourcecode-1.
|
|
83
|
-
sourcecode-1.
|
|
84
|
-
sourcecode-1.
|
|
85
|
-
sourcecode-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|