sourcecode 1.53.0__py3-none-any.whl → 1.55.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.53.0"
3
+ __version__ = "1.55.0"
sourcecode/cli.py CHANGED
@@ -4137,6 +4137,82 @@ def _directory_hashes(file_list: "list[str]", root: "Path") -> "dict[str, str]":
4137
4137
  return out
4138
4138
 
4139
4139
 
4140
+ # Architectural layer directory names used to recognize a layered module
4141
+ # (DDD / hexagonal). The module *root* is the directory directly above the
4142
+ # shallowest layer dir, so symbols living in domain/application/infrastructure
4143
+ # subdirs all roll up to one module — a consumer counts modules, not leaf dirs.
4144
+ _LAYER_MARKERS: "frozenset[str]" = frozenset({
4145
+ "domain", "application", "infrastructure",
4146
+ "interfaces", "presentation", "adapters", "ports", "api",
4147
+ })
4148
+ # Core DDD layers — presence of >=2 marks a module as DDD-layered vs flat/legacy.
4149
+ _DDD_CORE_LAYERS: "frozenset[str]" = frozenset({
4150
+ "domain", "application", "infrastructure",
4151
+ })
4152
+
4153
+
4154
+ def _module_root_of(leaf_dir: "str") -> "tuple[str, str | None]":
4155
+ """Map a leaf source directory to its architectural module root.
4156
+
4157
+ For a layered module ``<root>/<layer>/...`` the root is the path above the
4158
+ shallowest recognized layer dir, and the layer name is returned alongside.
4159
+ Flat directories (no layer marker) are their own root with a ``None`` layer.
4160
+ Pure-structural — no file reads.
4161
+ """
4162
+ parts = leaf_dir.split("/")
4163
+ for i, seg in enumerate(parts):
4164
+ if seg.lower() in _LAYER_MARKERS:
4165
+ return "/".join(parts[:i]) or ".", seg.lower()
4166
+ return leaf_dir, None
4167
+
4168
+
4169
+ def _detect_module_roots(by_directory: "dict[str, list]") -> "dict":
4170
+ """Roll leaf source dirs up to architectural module roots and classify them.
4171
+
4172
+ Resolves the leaf-directory-vs-module mismatch in the C4 component view: a
4173
+ DDD module split across ``domain/`` / ``application/`` / ``infrastructure/``
4174
+ subdirs is reported once, with its layers and a structural ``pattern``
4175
+ (``layered`` when it carries >=2 core DDD layers, else ``flat``). Gives a
4176
+ downstream consumer a verifiable module enumeration and a DDD-vs-legacy
4177
+ signal instead of forcing it to infer module boundaries from directory names.
4178
+ """
4179
+ roots: "dict[str, dict]" = {}
4180
+ for leaf, symbols in by_directory.items():
4181
+ root, layer = _module_root_of(leaf)
4182
+ slot = roots.setdefault(
4183
+ root, {"layers": set(), "symbol_count": 0, "leaf_dirs": 0}
4184
+ )
4185
+ if layer:
4186
+ slot["layers"].add(layer)
4187
+ slot["symbol_count"] += len(symbols)
4188
+ slot["leaf_dirs"] += 1
4189
+
4190
+ modules: "list[dict]" = []
4191
+ layered = flat = 0
4192
+ for root in sorted(roots):
4193
+ s = roots[root]
4194
+ pattern = "layered" if len(s["layers"] & _DDD_CORE_LAYERS) >= 2 else "flat"
4195
+ if pattern == "layered":
4196
+ layered += 1
4197
+ else:
4198
+ flat += 1
4199
+ modules.append({
4200
+ "root": root,
4201
+ "pattern": pattern,
4202
+ "layers": sorted(s["layers"]),
4203
+ "symbol_count": s["symbol_count"],
4204
+ "leaf_dir_count": s["leaf_dirs"],
4205
+ })
4206
+ return {
4207
+ "modules": modules,
4208
+ "summary": {
4209
+ "module_count": len(modules),
4210
+ "layered_module_count": layered,
4211
+ "flat_module_count": flat,
4212
+ },
4213
+ }
4214
+
4215
+
4140
4216
  def _build_c4_export(
4141
4217
  root: "Path",
4142
4218
  file_list: "list[str]",
@@ -4155,6 +4231,9 @@ def _build_c4_export(
4155
4231
  """
4156
4232
  by_directory = _group_symbols_by_directory(nodes)
4157
4233
  module_graph = _build_module_graph(nodes, edges)
4234
+ # Architectural module-root rollup + DDD/legacy classification, so a
4235
+ # consumer counts/classifies modules instead of inferring them from leaf dirs.
4236
+ module_graph["module_roots"] = _detect_module_roots(by_directory)
4158
4237
  api_surface = _group_endpoints_by_controller(endpoints)
4159
4238
  containers = _detect_containers(root)
4160
4239
 
@@ -634,11 +634,13 @@ def _find_symbol_files(
634
634
  try:
635
635
  result = subprocess.run(
636
636
  [
637
- "grep", "-rl",
637
+ "grep", "-rlF",
638
638
  "--include=*.ts", "--include=*.tsx",
639
639
  "--include=*.js", "--include=*.jsx",
640
640
  "--include=*.py", "--include=*.java",
641
- symbol, ".",
641
+ # -e guards a symbol that begins with '-' from being parsed as a
642
+ # flag; -- terminates options so the search root can't be either.
643
+ "-e", symbol, "--", ".",
642
644
  ],
643
645
  cwd=str(root),
644
646
  capture_output=True,
sourcecode/license.py CHANGED
@@ -45,7 +45,33 @@ _DEFAULT_SUPABASE_URL: str = "https://qkndlmyekvujjdgthtmz.supabase.co"
45
45
  # Paste your project's anon key here so `sourcecode activate` works out of the
46
46
  # box; env var still overrides for testing against another project.
47
47
  _DEFAULT_SUPABASE_ANON_KEY: str = "sb_publishable_qiJFLWjbBbTqjg-fb0mAGA_cl8PBOKH"
48
- _SUPABASE_URL: str = os.environ.get("SOURCECODE_SUPABASE_URL", _DEFAULT_SUPABASE_URL)
48
+ def _safe_supabase_url(override: "Optional[str]") -> str:
49
+ """Validate the SOURCECODE_SUPABASE_URL override before trusting it.
50
+
51
+ The license key is POSTed to this host, so a plaintext ``http://`` endpoint
52
+ would expose the credential on the wire. Allow ``https://`` to any host, and
53
+ ``http://`` only to loopback (Supabase local dev serves on
54
+ ``http://127.0.0.1:54321``). Anything else is rejected back to the default.
55
+ """
56
+ if not override or override == _DEFAULT_SUPABASE_URL:
57
+ return _DEFAULT_SUPABASE_URL
58
+ from urllib.parse import urlparse
59
+
60
+ parsed = urlparse(override)
61
+ host = (parsed.hostname or "").lower()
62
+ is_loopback = host in {"localhost", "127.0.0.1", "::1"}
63
+ if parsed.scheme == "https" or (parsed.scheme == "http" and is_loopback):
64
+ return override
65
+ sys.stderr.write(
66
+ f"[sourcecode] WARNING: ignoring SOURCECODE_SUPABASE_URL={override!r} — "
67
+ "must be https:// (or http:// to localhost). Using the default endpoint "
68
+ "to avoid sending the license key over plaintext.\n"
69
+ )
70
+ sys.stderr.flush()
71
+ return _DEFAULT_SUPABASE_URL
72
+
73
+
74
+ _SUPABASE_URL: str = _safe_supabase_url(os.environ.get("SOURCECODE_SUPABASE_URL"))
49
75
  _SUPABASE_ANON_KEY: str = os.environ.get(
50
76
  "SOURCECODE_SUPABASE_ANON_KEY",
51
77
  _DEFAULT_SUPABASE_ANON_KEY,
@@ -152,12 +178,35 @@ _license_data: Optional[dict] = None
152
178
  is_pro: bool = False
153
179
 
154
180
 
181
+ def _secure_dir() -> None:
182
+ """Create ~/.sourcecode owner-only (0700). Holds the license secret.
183
+
184
+ ``mkdir(mode=...)`` is ignored when the dir already exists, so chmod
185
+ unconditionally. Best-effort: a chmod failure (e.g. Windows) is non-fatal.
186
+ """
187
+ try:
188
+ _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
189
+ os.chmod(_LICENSE_DIR, 0o700)
190
+ except OSError:
191
+ pass
192
+
193
+
155
194
  def _write_license_file(data: dict) -> None:
156
- """Atomically write license data via tmp file + rename."""
195
+ """Atomically write license data via tmp file + rename, owner-only (0600).
196
+
197
+ The payload contains the Pro ``license_key`` and account email, so the file
198
+ must not be world/group-readable on shared hosts. Permissions are set on the
199
+ tmp file *before* the rename so there is no readable window at the final path.
200
+ """
201
+ _secure_dir()
157
202
  payload = json.dumps(data, indent=2, ensure_ascii=False).encode("utf-8")
158
203
  tmp = _LICENSE_FILE.with_suffix(".tmp")
159
204
  try:
160
205
  tmp.write_bytes(payload)
206
+ try:
207
+ os.chmod(tmp, 0o600)
208
+ except OSError:
209
+ pass
161
210
  tmp.replace(_LICENSE_FILE)
162
211
  except Exception:
163
212
  try:
@@ -192,7 +241,7 @@ def check_delta_free_tier(repo_path: str) -> "tuple[bool, int, int]":
192
241
  new_used = used + 1
193
242
  runs[key] = new_used
194
243
  try:
195
- _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
244
+ _secure_dir()
196
245
  tmp = _DELTA_RUNS_FILE.with_suffix(".tmp")
197
246
  tmp.write_text(json.dumps(runs, indent=2, ensure_ascii=False), encoding="utf-8")
198
247
  tmp.replace(_DELTA_RUNS_FILE)
@@ -506,7 +555,7 @@ def activate_license(license_key: str) -> None:
506
555
  _emit_telemetry("activation", feature="key", success=False, error_kind="NotPro")
507
556
  _fail("not_pro", "This license is not a Pro license.")
508
557
 
509
- _LICENSE_DIR.mkdir(parents=True, exist_ok=True)
558
+ _secure_dir()
510
559
  now = datetime.now(timezone.utc).isoformat()
511
560
  data = {
512
561
  "license_key": license_key,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.53.0
3
+ Version: 1.55.0
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
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.53.0-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.55.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -404,7 +404,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
404
404
  | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
405
405
  | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
406
406
  | `--integrations` | Outbound integrations (`RestTemplate`, `WebClient`, `@FeignClient`, `LdapTemplate`, `JmsTemplate`, ActiveMQ) with `file:line` evidence and a literal `target` URL/name when present. |
407
- | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). |
407
+ | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
408
408
 
409
409
  The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
410
410
 
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=iHGCfyboU5livWODKEj-u8oT6BwJInerv6YHn28vXno,103
1
+ sourcecode/__init__.py,sha256=Ii3NgTGWcclYkr3nNfo2p3kxEPuBIM33XeG_rUFCIeU,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -7,13 +7,13 @@ sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
7
7
  sourcecode/canonical_ir.py,sha256=DEwucOPJguLsVtg5cV8mWXNi112l5jmBhv73KGGebVk,24849
8
8
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
9
9
  sourcecode/classifier.py,sha256=hKzg-nQ47htqqIUzSGvYxv15cXrA3KgICTwJmdqal0o,8095
10
- sourcecode/cli.py,sha256=IZ_TcUzd-rUFoIYMgkOcGP-FqiYoOGPxZ3sjPkEp4OM,272648
10
+ sourcecode/cli.py,sha256=PpqOLcYhNljXCJ0V_RjO9RgsgjzgtDK5jIDTqI3Hk6k,275872
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
14
14
  sourcecode/context_summarizer.py,sha256=zlbm8ytdvJToohU108-dwBmEl52xl0gXpf6PZBOW_2A,6540
15
15
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
16
- sourcecode/contract_pipeline.py,sha256=gvTdDniedm_mjq4vaHqnBY2UkQ0s00gtXqzTLILNXHc,28719
16
+ sourcecode/contract_pipeline.py,sha256=bNn9SYtwfI1CGKlYGxewexlp7_IWhpwSCae-BMuuVwQ,28895
17
17
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
18
18
  sourcecode/dependency_analyzer.py,sha256=qEnRiKFkleZJyLf_DyznJbWD1GJ881iG4RRDqH9oGQ4,61524
19
19
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
@@ -29,7 +29,7 @@ sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
29
29
  sourcecode/git_analyzer.py,sha256=JStxTQXNjBWi_wLdwhsZs9mT-v50cSJIz4Agzn6Kh9I,13362
30
30
  sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,62403
31
31
  sourcecode/integration_detector.py,sha256=ZJqrGwvZ4ee2JTGhlazKk67aZi173HxkhNpl8Yntpd8,6503
32
- sourcecode/license.py,sha256=i_X1bYdobL_z9OVuLiycnWEFSaaNhcKKuTd6G55U3_k,20747
32
+ sourcecode/license.py,sha256=JD-1pnH_2XR9lKr9p9KQZn9U31I9e5o6GYsN1XCVccw,22577
33
33
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
34
34
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
35
35
  sourcecode/migrate_check.py,sha256=H8iy7Vk8cGL0dnR3ZkFPS20CtfF5LJWuzQVQE4awQ9s,56192
@@ -102,8 +102,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
102
102
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
103
103
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
104
104
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
105
- sourcecode-1.53.0.dist-info/METADATA,sha256=Ahvq7n0P2M28DyG8mqksS-g11VTDnqDCjdPSZ23NjH0,36719
106
- sourcecode-1.53.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
107
- sourcecode-1.53.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
108
- sourcecode-1.53.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
109
- sourcecode-1.53.0.dist-info/RECORD,,
105
+ sourcecode-1.55.0.dist-info/METADATA,sha256=uMl86Jm5iDPg0LDJ1QTmuLuEuiuqaihtwbuh0D_N06k,37049
106
+ sourcecode-1.55.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
107
+ sourcecode-1.55.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
108
+ sourcecode-1.55.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
109
+ sourcecode-1.55.0.dist-info/RECORD,,