sourcecode 1.31.32__py3-none-any.whl → 1.32.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.31.32"
3
+ __version__ = "1.32.0"
sourcecode/cli.py CHANGED
@@ -781,6 +781,11 @@ def main(
781
781
  err=True,
782
782
  )
783
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
+
784
789
  # P0-2 FIX: --compact and --full are mutually exclusive.
785
790
  # compact is designed to be a bounded summary; --full removes truncation limits,
786
791
  # which contradicts compact's purpose. Use --agent --full for expanded output.
@@ -2672,6 +2677,32 @@ def prepare_context_cmd(
2672
2677
  _pc_budget = _pc_budgets.get(task, BUDGET_EXPLAIN)
2673
2678
  out = _pc_trim(out, _pc_budget, label=task)
2674
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
+
2675
2706
  if format == "github-comment" and task == "review-pr":
2676
2707
  from sourcecode.pr_comment_renderer import render_github_comment
2677
2708
  _pc_content = render_github_comment(out)
@@ -3373,13 +3404,11 @@ def modernize_cmd(
3373
3404
  sourcecode onboard . — Architecture overview first
3374
3405
  sourcecode impact <target> — Verify impact before touching a hotspot
3375
3406
  """
3376
- from sourcecode.license import require_pro as _require_pro
3377
- _require_pro("modernize")
3378
-
3379
3407
  import json as _json
3380
3408
  import sys as _sys
3381
3409
  from sourcecode.repository_ir import build_repo_ir, find_java_files, apply_ir_size_limits
3382
3410
  from sourcecode.output_budget import trim_to_budget, BUDGET_ONBOARD
3411
+ from sourcecode.license import can_use as _mod_can_use
3383
3412
 
3384
3413
  root = path.resolve()
3385
3414
  if not root.is_dir():
@@ -3481,52 +3510,73 @@ def modernize_cmd(
3481
3510
  key=lambda s: -len(s.get("members") or []),
3482
3511
  )[:10]
3483
3512
 
3484
- result = {
3485
- "workflow": "modernize",
3486
- "path": str(root),
3487
- "summary": {
3488
- "total_classes": len([n for n in graph_nodes if n.get("type") in ("class", "interface")]),
3489
- "total_subsystems": len(subsystems),
3490
- "high_coupling_nodes": len(coupling_nodes),
3491
- "dead_zone_candidates": len(dead_zones),
3492
- },
3493
- "hotspot_candidates": hotspots,
3494
- "high_coupling_nodes": [
3495
- {"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
3496
- for n in coupling_nodes
3497
- ],
3498
- "dead_zone_candidates": [
3499
- {"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
3500
- for n in dead_zones
3501
- ],
3502
- "subsystem_summary": [
3503
- {
3504
- "label": s.get("label") or s.get("name") or "",
3505
- "package_prefix": s.get("package_prefix") or s.get("pkg") or "",
3506
- "member_count": len(s.get("members") or []),
3507
- }
3508
- for s in subsystems[:15]
3509
- ],
3510
- "cross_module_tangles": [
3511
- {
3512
- "label": s.get("label") or s.get("name") or "",
3513
- "member_count": len(s.get("members") or []),
3514
- }
3515
- for s in tangle_modules
3516
- ],
3517
- # BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
3518
- # hotspots filters by role=service/repository/controller; annotation types and
3519
- # value objects end up in high_coupling_nodes instead.
3520
- "recommendation": (
3521
- (
3522
- "Start with hotspot_candidates (high fan-in = highest blast radius). "
3523
- if hotspots else
3524
- "high_coupling_nodes shows the most-referenced classes — start there. "
3525
- )
3526
- + "Dead zones are safe to remove or refactor. "
3527
- + "Cross-module tangles indicate coupling worth decomposing."
3528
- ),
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),
3529
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
+ }
3530
3580
 
3531
3581
  result = trim_to_budget(result, BUDGET_ONBOARD, label="modernize")
3532
3582
  output = _json.dumps(result, indent=2, ensure_ascii=False)
@@ -3619,9 +3669,6 @@ def mcp_serve() -> None:
3619
3669
  }
3620
3670
  }
3621
3671
  """
3622
- from sourcecode.license import require_pro as _require_pro
3623
- _require_pro("mcp serve")
3624
-
3625
3672
  import logging
3626
3673
  import sys as _sys
3627
3674
 
sourcecode/license.py CHANGED
@@ -3,7 +3,7 @@
3
3
  Flow:
4
4
  1. Module imported → _init() loads ~/.sourcecode/license.json (if present)
5
5
  2. is_pro set globally (True when plan == "pro")
6
- 3. Pro commands call require_pro(feature_name) at entry — exits 1 if not Pro
6
+ 3. Pro commands call require_feature(feature_name) at entry — exits 1 if not Pro
7
7
  4. `sourcecode activate <key>` calls activate_license(key) — validates via
8
8
  Edge Function, writes ~/.sourcecode/license.json, exits 0 on success
9
9
  5. Cached license is re-validated every 24 h (online); network errors keep
@@ -38,6 +38,53 @@ _LICENSE_DIR: Path = Path.home() / ".sourcecode"
38
38
  _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
39
39
  _CACHE_TTL_SECONDS: int = 86400 # 24 hours
40
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
+ }
87
+
41
88
  # ---------------------------------------------------------------------------
42
89
  # Global license state — loaded once at import time
43
90
  # ---------------------------------------------------------------------------
@@ -147,30 +194,74 @@ _init()
147
194
 
148
195
 
149
196
  # ---------------------------------------------------------------------------
150
- # Enforcement
197
+ # Entitlement helpers
151
198
  # ---------------------------------------------------------------------------
152
199
 
153
- def require_pro(feature_name: str) -> None:
154
- """Exit with structured JSON error when not Pro.
200
+ def can_use(feature_name: str) -> bool:
201
+ """Return True if the current plan has access to feature_name.
202
+
203
+ Does not trigger revalidation — use require_feature() at command entry
204
+ points where you want revalidation + gating in one call.
205
+ """
206
+ return is_pro
207
+
208
+
209
+ def require_feature(feature_name: str) -> None:
210
+ """Exit with a clean upgrade prompt when feature_name requires Pro.
155
211
 
156
212
  Re-validates stale cached license before gating (once per 24 h, online).
157
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.
259
+
158
260
  Example:
159
261
  from sourcecode.license import require_pro
160
262
  require_pro("impact")
161
263
  """
162
- if is_pro:
163
- _maybe_revalidate()
164
-
165
- if not is_pro:
166
- payload = {
167
- "error": "pro_required",
168
- "feature": feature_name,
169
- "message": "Run: sourcecode activate <license_key>",
170
- }
171
- sys.stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
172
- sys.stdout.flush()
173
- sys.exit(1)
264
+ require_feature(feature_name)
174
265
 
175
266
 
176
267
  # ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.31.32
3
+ Version: 1.32.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
225
225
 
226
226
  **AI-ready change intelligence for Java/Spring enterprise monoliths.**
227
227
 
228
- ![Version](https://img.shields.io/badge/version-1.31.32-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.32.0-blue)
229
229
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
230
230
 
231
231
  ---
@@ -263,7 +263,7 @@ pipx install sourcecode
263
263
 
264
264
  ```bash
265
265
  sourcecode version
266
- # sourcecode 1.31.32
266
+ # sourcecode 1.32.0
267
267
  ```
268
268
 
269
269
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=FAgcPCebMe_1K921d8TjlfM7T_GMcbLwHvBZvp_uat8,104
1
+ sourcecode/__init__.py,sha256=1pcmq6UuzqBpI1Q4E_5ukKd_IJ8s8CN4xrW1_EyV0Gw,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=Ry3aYT9dc7XuLmWLT5IZ93RkCf_P14Qtew0nGPvUl_8,42184
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -6,7 +6,7 @@ sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,5
6
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=7tkUpZEnXjU_swX1wadkMnfYS0A0p0NvSRZEBIOLLi0,161521
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
@@ -22,7 +22,7 @@ sourcecode/file_classifier.py,sha256=QrYm7MlG29HQdAR1WOfpnIIBysAz62c5coz9eQ76meo
22
22
  sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
23
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=oiAxBXHf1mz42rrDKQCPX28W2yaAHvWqaVZuijJGSF8,7902
25
+ sourcecode/license.py,sha256=m5n4PKZ7ZZZ17bpLjIsKwd94PXaIg5iS_2kFNjCV2Og,11378
26
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
@@ -78,8 +78,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
78
78
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
79
79
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
80
80
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
81
- sourcecode-1.31.32.dist-info/METADATA,sha256=69Ai2xiH7kn4Zmsm-KhGAfTTXtUbmyyy9EgLSSQ9GU4,31103
82
- sourcecode-1.31.32.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
83
- sourcecode-1.31.32.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
84
- sourcecode-1.31.32.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
85
- sourcecode-1.31.32.dist-info/RECORD,,
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,,