sourcecode 1.35.14__tar.gz → 1.35.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {sourcecode-1.35.14 → sourcecode-1.35.16}/PKG-INFO +4 -3
  2. {sourcecode-1.35.14 → sourcecode-1.35.16}/README.md +2 -2
  3. {sourcecode-1.35.14 → sourcecode-1.35.16}/pyproject.toml +2 -1
  4. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/ast_extractor.py +1 -1
  6. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/cir_graphs.py +9 -5
  7. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/cli.py +1 -1
  8. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/dependency_analyzer.py +1 -1
  9. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/explain.py +4 -2
  10. sourcecode-1.35.16/src/sourcecode/fqn_utils.py +53 -0
  11. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/license.py +30 -12
  12. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/pr_impact.py +3 -1
  13. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_impact.py +7 -12
  14. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/transport.py +4 -1
  15. {sourcecode-1.35.14 → sourcecode-1.35.16}/.github/workflows/build-windows.yml +0 -0
  16. {sourcecode-1.35.14 → sourcecode-1.35.16}/.gitignore +0 -0
  17. {sourcecode-1.35.14 → sourcecode-1.35.16}/.ruff.toml +0 -0
  18. {sourcecode-1.35.14 → sourcecode-1.35.16}/CHANGELOG.md +0 -0
  19. {sourcecode-1.35.14 → sourcecode-1.35.16}/CONTRIBUTING.md +0 -0
  20. {sourcecode-1.35.14 → sourcecode-1.35.16}/LICENSE +0 -0
  21. {sourcecode-1.35.14 → sourcecode-1.35.16}/SECURITY.md +0 -0
  22. {sourcecode-1.35.14 → sourcecode-1.35.16}/raw +0 -0
  23. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/architecture_analyzer.py +0 -0
  25. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/architecture_summary.py +0 -0
  26. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/cache.py +0 -0
  27. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/canonical_ir.py +0 -0
  28. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/code_notes_analyzer.py +0 -0
  30. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/confidence_analyzer.py +0 -0
  31. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/context_scorer.py +0 -0
  32. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/context_summarizer.py +0 -0
  33. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/contract_model.py +0 -0
  34. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/contract_pipeline.py +0 -0
  35. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/__init__.py +0 -0
  37. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/base.py +0 -0
  38. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/csproj_parser.py +0 -0
  39. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/dart.py +0 -0
  40. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/dotnet.py +0 -0
  41. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/elixir.py +0 -0
  42. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/go.py +0 -0
  43. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/heuristic.py +0 -0
  44. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/hybrid.py +0 -0
  45. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/java.py +0 -0
  46. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/parsers.py +0 -0
  49. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/php.py +0 -0
  50. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/project.py +0 -0
  51. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/entrypoint_classifier.py +0 -0
  59. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/env_analyzer.py +0 -0
  60. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/error_schema.py +0 -0
  61. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/file_classifier.py +0 -0
  62. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/flow_analyzer.py +0 -0
  63. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/git_analyzer.py +0 -0
  64. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/graph_analyzer.py +0 -0
  65. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/__init__.py +0 -0
  66. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  67. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  68. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  69. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  70. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  71. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/orchestrator.py +0 -0
  72. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/registry.py +0 -0
  73. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/runner.py +0 -0
  74. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp/server.py +0 -0
  75. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/mcp_nudge.py +0 -0
  76. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/metrics_analyzer.py +0 -0
  77. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/output_budget.py +0 -0
  78. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/path_filters.py +0 -0
  79. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/pr_comment_renderer.py +0 -0
  80. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/prepare_context.py +0 -0
  81. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/progress.py +0 -0
  82. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/ranking_engine.py +0 -0
  83. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/redactor.py +0 -0
  84. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/relevance_scorer.py +0 -0
  85. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/repo_classifier.py +0 -0
  86. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/repository_ir.py +0 -0
  87. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/ris.py +0 -0
  88. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/runtime_classifier.py +0 -0
  89. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/scanner.py +0 -0
  90. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/schema.py +0 -0
  91. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/semantic_analyzer.py +0 -0
  92. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/serializer.py +0 -0
  93. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_event_topology.py +0 -0
  94. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_findings.py +0 -0
  95. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_model.py +0 -0
  96. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_security_audit.py +0 -0
  97. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_semantic.py +0 -0
  98. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/spring_tx_analyzer.py +0 -0
  99. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/summarizer.py +0 -0
  100. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/__init__.py +0 -0
  101. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/config.py +0 -0
  102. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/consent.py +0 -0
  103. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/events.py +0 -0
  104. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/telemetry/filters.py +0 -0
  105. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/tree_utils.py +0 -0
  106. {sourcecode-1.35.14 → sourcecode-1.35.16}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.14
3
+ Version: 1.35.16
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
18
  Classifier: Topic :: Utilities
19
19
  Requires-Python: >=3.9
20
+ Requires-Dist: defusedxml>=0.7
20
21
  Requires-Dist: mcp>=1.0.0
21
22
  Requires-Dist: pathspec>=1.0
22
23
  Requires-Dist: ruamel-yaml>=0.18
@@ -39,7 +40,7 @@ Description-Content-Type: text/markdown
39
40
 
40
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
42
 
42
- ![Version](https://img.shields.io/badge/version-1.35.14-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.16-blue)
43
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
45
 
45
46
  ---
@@ -113,7 +114,7 @@ pipx install sourcecode
113
114
 
114
115
  ```bash
115
116
  sourcecode version
116
- # sourcecode 1.35.14
117
+ # sourcecode 1.35.16
117
118
  ```
118
119
 
119
120
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.35.14-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.16-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.14
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.14"
7
+ version = "1.35.16"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -31,6 +31,7 @@ dependencies = [
31
31
  "ruamel.yaml>=0.18",
32
32
  "tomli>=2.0; python_version < '3.11'",
33
33
  "mcp>=1.0.0",
34
+ "defusedxml>=0.7",
34
35
  ]
35
36
 
36
37
  [project.scripts]
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.14"
3
+ __version__ = "1.35.16"
@@ -1196,7 +1196,7 @@ def _detect_role(path: str, contract: FileContract) -> str:
1196
1196
  def _extract_mybatis_xml(rel_path: str, source: str) -> FileContract:
1197
1197
  """Extract namespace and SQL operations from a MyBatis *Mapper.xml file."""
1198
1198
  import re as _re
1199
- from xml.etree import ElementTree
1199
+ import defusedxml.ElementTree as ElementTree # type: ignore[import]
1200
1200
 
1201
1201
  _NS_STRIP = _re.compile(r"\{[^}]+\}")
1202
1202
  _SQL_OPS = frozenset({"select", "insert", "update", "delete"})
@@ -13,6 +13,8 @@ from __future__ import annotations
13
13
 
14
14
  from dataclasses import dataclass, field
15
15
 
16
+ from sourcecode.fqn_utils import normalize_owner_fqn
17
+
16
18
  # ---------------------------------------------------------------------------
17
19
  # ImplementationGraph — CH-001
18
20
  # ---------------------------------------------------------------------------
@@ -179,12 +181,14 @@ class InjectionGraph:
179
181
  if not from_fqn or not to_fqn:
180
182
  continue
181
183
 
182
- # Resolve injector to class level
183
- if "#" in from_fqn:
184
- class_fqn = from_fqn.rsplit("#", 1)[0]
184
+ # Resolve injector to class level.
185
+ # Three formats emitted by the CIR parser:
186
+ # Constructor: pkg.Class#<init> → class = pkg.Class
187
+ # Field: pkg.Class.field → class = pkg.Class (normalize_owner_fqn)
188
+ # Lombok: pkg.Class → class = pkg.Class (already class-level)
189
+ class_fqn = normalize_owner_fqn(from_fqn)
190
+ if class_fqn != from_fqn:
185
191
  injector_to_class[from_fqn] = class_fqn
186
- else:
187
- class_fqn = from_fqn
188
192
 
189
193
  # Build class → [dep, ...] and service → [class, ...] indices
190
194
  deps = deps_of.setdefault(class_fqn, [])
@@ -4892,7 +4892,7 @@ def mcp_serve() -> None:
4892
4892
  except KeyboardInterrupt:
4893
4893
  log.info("sourcecode-mcp stopped")
4894
4894
  except Exception as exc:
4895
- log.critical("sourcecode-mcp fatal error: %s", exc, exc_info=True)
4895
+ log.critical("sourcecode-mcp fatal error: %s: %s", type(exc).__name__, exc)
4896
4896
  raise typer.Exit(code=1)
4897
4897
 
4898
4898
 
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
- import xml.etree.ElementTree as ET
4
+ import defusedxml.ElementTree as ET # type: ignore[import]
5
5
  from collections.abc import Iterable
6
6
  from dataclasses import replace
7
7
  from pathlib import Path
@@ -13,6 +13,8 @@ from __future__ import annotations
13
13
  from dataclasses import dataclass, field
14
14
  from typing import TYPE_CHECKING, Optional
15
15
 
16
+ from sourcecode.fqn_utils import normalize_owner_fqn
17
+
16
18
  if TYPE_CHECKING:
17
19
  from sourcecode.canonical_ir import CanonicalRepositoryIR
18
20
  from sourcecode.spring_model import SpringSemanticModel
@@ -255,8 +257,8 @@ def _build_callers(class_fqn: str, cir: "CanonicalRepositoryIR") -> list[str]:
255
257
  rev: dict = (getattr(cir, "reverse_graph", None) or {}).get(class_fqn) or {}
256
258
  for callers in rev.values():
257
259
  for caller_fqn in (callers or []):
258
- # Strip method part to get class
259
- cls_fqn = caller_fqn.rsplit("#", 1)[0] if "#" in caller_fqn else caller_fqn
260
+ # Normalize: field (pkg.Class.field) and method (pkg.Class#method) class FQN
261
+ cls_fqn = normalize_owner_fqn(caller_fqn)
260
262
  if cls_fqn == class_fqn:
261
263
  continue
262
264
  s = _simple(cls_fqn)
@@ -0,0 +1,53 @@
1
+ """fqn_utils.py — FQN normalization utilities (single source of truth).
2
+
3
+ All code that needs to distinguish class FQNs from member FQNs (methods, fields,
4
+ constructors) must use these functions. No direct `.split("#")`, `.rsplit(".")`,
5
+ or lowercase-heuristic checks elsewhere.
6
+
7
+ Symbol FQN conventions in the CIR:
8
+ Class/Interface/Enum: pkg.ClassName (no # or lowercase-last-seg)
9
+ Method: pkg.ClassName#methodName (hash separator)
10
+ Constructor: pkg.ClassName#<init> (hash, angle-bracket name)
11
+ Field: pkg.ClassName.fieldName (dot separator, lowercase last segment)
12
+ Inner class: pkg.ClassName.InnerClass (dot separator, uppercase last segment)
13
+ """
14
+ from __future__ import annotations
15
+
16
+
17
+ def normalize_owner_fqn(fqn: str) -> str:
18
+ """Extract the owning class FQN from any symbol FQN.
19
+
20
+ Examples:
21
+ PatientServiceImpl -> PatientServiceImpl
22
+ org.foo.PatientServiceImpl -> org.foo.PatientServiceImpl
23
+ org.foo.PatientServiceImpl#savePatient -> org.foo.PatientServiceImpl
24
+ org.foo.PatientServiceImpl#<init> -> org.foo.PatientServiceImpl
25
+ org.foo.PatientServiceImpl.dao -> org.foo.PatientServiceImpl
26
+ org.foo.PatientServiceImpl.InnerClass -> org.foo.PatientServiceImpl.InnerClass (unchanged)
27
+ """
28
+ if "#" in fqn:
29
+ return fqn.rsplit("#", 1)[0]
30
+ if "." in fqn:
31
+ last_seg = fqn.rsplit(".", 1)[1]
32
+ if last_seg and last_seg[0].islower():
33
+ return fqn.rsplit(".", 1)[0]
34
+ return fqn
35
+
36
+
37
+ def is_member_fqn(fqn: str) -> bool:
38
+ """Return True for method/field/constructor FQNs; False for type FQNs.
39
+
40
+ True: pkg.Class#method, pkg.Class#<init>, pkg.Class.fieldName
41
+ False: pkg.Class, pkg.outer.InnerClass, simple.Name
42
+ """
43
+ if "#" in fqn:
44
+ return True
45
+ if "." in fqn:
46
+ last_seg = fqn.rsplit(".", 1)[1]
47
+ return bool(last_seg and last_seg[0].islower())
48
+ return False
49
+
50
+
51
+ def is_type_fqn(fqn: str) -> bool:
52
+ """Return True for class/interface/enum/record FQNs; False for member FQNs."""
53
+ return not is_member_fqn(fqn)
@@ -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,23 @@ from typing import Optional
25
26
  # ---------------------------------------------------------------------------
26
27
  # Supabase endpoint config — hardcoded for production; override via env for dev
27
28
  # ---------------------------------------------------------------------------
28
- _SUPABASE_URL: str = os.environ.get(
29
- "SOURCECODE_SUPABASE_URL",
30
- "https://qkndlmyekvujjdgthtmz.supabase.co",
31
- )
29
+ _DEFAULT_SUPABASE_URL: str = "https://qkndlmyekvujjdgthtmz.supabase.co"
30
+ _SUPABASE_URL: str = os.environ.get("SOURCECODE_SUPABASE_URL", _DEFAULT_SUPABASE_URL)
32
31
  _SUPABASE_ANON_KEY: str = os.environ.get(
33
32
  "SOURCECODE_SUPABASE_ANON_KEY",
34
33
  "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
35
34
  )
35
+ if _SUPABASE_URL != _DEFAULT_SUPABASE_URL:
36
+ sys.stderr.write(
37
+ f"[sourcecode] WARNING: SOURCECODE_SUPABASE_URL overridden to {_SUPABASE_URL!r}."
38
+ " License requests will be sent to this server.\n"
39
+ )
40
+ sys.stderr.flush()
36
41
 
37
42
  _LICENSE_DIR: Path = Path.home() / ".sourcecode"
38
43
  _LICENSE_FILE: Path = _LICENSE_DIR / "license.json"
39
44
  _CACHE_TTL_SECONDS: int = 86400 # 24 hours
45
+ _LICENSE_KEY_RE = re.compile(r"^[A-Za-z0-9_\-]{1,200}$")
40
46
 
41
47
  # ---------------------------------------------------------------------------
42
48
  # Per-feature descriptions for upgrade UX
@@ -92,6 +98,21 @@ _license_data: Optional[dict] = None
92
98
  is_pro: bool = False
93
99
 
94
100
 
101
+ def _write_license_file(data: dict) -> None:
102
+ """Atomically write license data via tmp file + rename."""
103
+ payload = json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")
104
+ tmp = _LICENSE_FILE.with_suffix(".tmp")
105
+ try:
106
+ tmp.write_bytes(payload)
107
+ tmp.replace(_LICENSE_FILE)
108
+ except Exception:
109
+ try:
110
+ tmp.unlink(missing_ok=True)
111
+ except Exception:
112
+ pass
113
+ raise
114
+
115
+
95
116
  def _load_license_file() -> Optional[dict]:
96
117
  """Read ~/.sourcecode/license.json. Returns parsed dict or None."""
97
118
  try:
@@ -173,10 +194,7 @@ def _maybe_revalidate() -> None:
173
194
  _license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
174
195
  is_pro = _license_data.get("plan") == "pro"
175
196
  try:
176
- _LICENSE_FILE.write_text(
177
- json.dumps(_license_data, indent=2, ensure_ascii=False),
178
- encoding="utf-8",
179
- )
197
+ _write_license_file(_license_data)
180
198
  except Exception:
181
199
  pass
182
200
 
@@ -284,6 +302,9 @@ def activate_license(license_key: str) -> None:
284
302
  Outputs JSON to stdout; exits 0 on success, 1 on any failure.
285
303
  Never raises — all error paths emit JSON and call sys.exit(1).
286
304
  """
305
+ if not _LICENSE_KEY_RE.match(license_key):
306
+ _fail("invalid_license", "License key format is invalid.")
307
+
287
308
  if not _SUPABASE_ANON_KEY:
288
309
  _fail("configuration_error", "SOURCECODE_SUPABASE_ANON_KEY not set. Contact support.")
289
310
 
@@ -308,10 +329,7 @@ def activate_license(license_key: str) -> None:
308
329
  "activated_at": now,
309
330
  "validated_at": now,
310
331
  }
311
- _LICENSE_FILE.write_text(
312
- json.dumps(data, indent=2, ensure_ascii=False),
313
- encoding="utf-8",
314
- )
332
+ _write_license_file(data)
315
333
 
316
334
  output = {"status": "activated", "plan": "pro", "features": data["features"]}
317
335
  sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
@@ -20,6 +20,8 @@ from dataclasses import dataclass, field
20
20
  from pathlib import Path
21
21
  from typing import TYPE_CHECKING, Optional
22
22
 
23
+ from sourcecode.fqn_utils import is_member_fqn
24
+
23
25
  if TYPE_CHECKING:
24
26
  from sourcecode.canonical_ir import CanonicalRepositoryIR
25
27
  from sourcecode.spring_model import SpringSemanticModel
@@ -147,7 +149,7 @@ def _build_file_class_index(cir: "CanonicalRepositoryIR") -> dict[str, list[str]
147
149
  for node in nodes:
148
150
  fqn: str = node.get("fqn") or ""
149
151
  sf: str = node.get("source_file") or ""
150
- if not fqn or not sf or "#" in fqn:
152
+ if not fqn or not sf or is_member_fqn(fqn):
151
153
  continue
152
154
  index.setdefault(sf, []).append(fqn)
153
155
  return index
@@ -25,6 +25,7 @@ from dataclasses import dataclass, field
25
25
  from pathlib import Path
26
26
  from typing import TYPE_CHECKING, Optional
27
27
 
28
+ from sourcecode.fqn_utils import normalize_owner_fqn
28
29
  from sourcecode.spring_findings import SEVERITY_ORDER, SpringFinding
29
30
  from sourcecode.spring_model import SpringSemanticModel
30
31
 
@@ -311,19 +312,13 @@ def _bfs_callers(
311
312
  if etype in _SKIP_EDGE_TYPES:
312
313
  continue
313
314
  for caller in fqn_list:
314
- _add_caller(caller, depth)
315
- # CH-002: injects edge to a field/constructor node → also traverse
316
- # the containing class, bypassing the skipped contained_in edge.
317
- # Two formats emitted by the CIR parser:
318
- # Constructor injection: pkg.Class#<init> (hash separator)
319
- # Field injection: pkg.Class.field (dot, lowercase last segment)
320
315
  if etype == "injects":
321
- if "#" in caller:
322
- _add_caller(caller.rsplit("#", 1)[0], depth)
323
- elif "." in caller:
324
- last_seg = caller.rsplit(".", 1)[1]
325
- if last_seg and last_seg[0].islower():
326
- _add_caller(caller.rsplit(".", 1)[0], depth)
316
+ # CH-002: field (pkg.Class.field) and constructor (pkg.Class#<init>)
317
+ # FQNs are injection sites, not callers. Normalize to owning class so
318
+ # member FQNs never appear in direct_callers / indirect_callers.
319
+ _add_caller(normalize_owner_fqn(caller), depth)
320
+ else:
321
+ _add_caller(caller, depth)
327
322
 
328
323
  return direct, indirect, was_truncated
329
324
 
@@ -20,7 +20,10 @@ _TIMEOUT_S = 3
20
20
 
21
21
 
22
22
  def _endpoint() -> str:
23
- return os.environ.get("SOURCECODE_TELEMETRY_ENDPOINT", _DEFAULT_ENDPOINT)
23
+ override = os.environ.get("SOURCECODE_TELEMETRY_ENDPOINT")
24
+ if override and override.startswith("https://"):
25
+ return override
26
+ return _DEFAULT_ENDPOINT
24
27
 
25
28
 
26
29
  def _send_blocking(payload: dict[str, Any]) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes