sourcecode 1.30.27__py3-none-any.whl → 1.30.29__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.30.27"
3
+ __version__ = "1.30.29"
@@ -19,7 +19,7 @@ from typing import Any, Optional, cast
19
19
  from pathspec import GitIgnoreSpec
20
20
 
21
21
  from sourcecode.repo_classifier import RepoTopology
22
- from sourcecode.scanner import DEFAULT_EXCLUDES, MANIFEST_NAMES
22
+ from sourcecode.scanner import DEFAULT_EXCLUDES, EXCLUDED_FILE_SUFFIXES, MANIFEST_NAMES
23
23
 
24
24
 
25
25
  class AdaptiveScanner:
@@ -88,13 +88,14 @@ class AdaptiveScanner:
88
88
 
89
89
  def _load_gitignore_spec(self) -> GitIgnoreSpec:
90
90
  if self._gitignore_spec is None:
91
- gitignore = self.root / ".gitignore"
92
91
  lines: list[str] = []
93
- if gitignore.exists():
94
- try:
95
- lines = gitignore.read_text(encoding="utf-8", errors="replace").splitlines()
96
- except OSError:
97
- pass
92
+ for ignore_file in (".gitignore", ".sourcecodeIgnore"):
93
+ candidate = self.root / ignore_file
94
+ if candidate.exists():
95
+ try:
96
+ lines.extend(candidate.read_text(encoding="utf-8", errors="replace").splitlines())
97
+ except OSError:
98
+ pass
98
99
  self._gitignore_spec = GitIgnoreSpec.from_lines(lines)
99
100
  return self._gitignore_spec
100
101
 
@@ -202,6 +203,9 @@ class AdaptiveScanner:
202
203
  # Skip flag-shaped names (shell redirect artifacts)
203
204
  if fname.startswith("-"):
204
205
  continue
206
+ # Skip minified/map files
207
+ if any(fname.endswith(sfx) for sfx in EXCLUDED_FILE_SUFFIXES):
208
+ continue
205
209
  fpath = current / fname
206
210
  if fpath.is_symlink():
207
211
  continue
sourcecode/classifier.py CHANGED
@@ -25,7 +25,7 @@ _API_FRAMEWORKS = {
25
25
  "Ktor",
26
26
  "Play",
27
27
  }
28
- _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix"}
28
+ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix", "Angular"}
29
29
  _CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
30
30
  _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
31
31
  ConfidenceLevel = Literal["high", "medium", "low"]
@@ -105,6 +105,13 @@ class TypeClassifier:
105
105
  if "src/lib.rs" in flat_paths and not any(path.endswith("main.rs") for path in flat_paths):
106
106
  return "library"
107
107
 
108
+ # Angular SPA: angular.json is the canonical signal; framework detection is secondary
109
+ if (
110
+ "angular.json" in flat_paths
111
+ or "Angular" in framework_names
112
+ ):
113
+ return "angular-spa"
114
+
108
115
  if framework_names & _WEB_FRAMEWORKS or any(
109
116
  path.startswith(("app/", "pages/", "components/")) for path in flat_paths
110
117
  ):
sourcecode/cli.py CHANGED
@@ -424,7 +424,7 @@ def main(
424
424
  no_redact: bool = typer.Option(
425
425
  False,
426
426
  "--no-redact",
427
- help="Disable secret redaction. Use with caution output may contain sensitive values.",
427
+ help="Disable secret redaction of output strings. Note: env var values from the OS are never included in output regardless of this flag (security policy).",
428
428
  ),
429
429
  version: Optional[bool] = typer.Option(
430
430
  None,
@@ -437,7 +437,7 @@ def main(
437
437
  depth: int = typer.Option(
438
438
  4,
439
439
  "--depth",
440
- help="File tree traversal depth (default: 4). Java/Maven projects auto-adjust to 12.",
440
+ help="File tree traversal depth (default: 4). Java/Maven projects auto-adjust to a minimum of 12; values below 12 have no effect on Java projects.",
441
441
  min=1,
442
442
  max=20,
443
443
  ),
@@ -592,6 +592,16 @@ def main(
592
592
  "-c",
593
593
  help="Copy output to system clipboard after a successful run. No-op when --output is used or clipboard is unavailable.",
594
594
  ),
595
+ exclude: Optional[str] = typer.Option(
596
+ None,
597
+ "--exclude",
598
+ help="Additional directories/patterns to exclude, comma-separated (e.g. 'legacy,generated').",
599
+ ),
600
+ no_cache: bool = typer.Option(
601
+ False,
602
+ "--no-cache",
603
+ help="Bypass the scan cache and force a fresh analysis.",
604
+ ),
595
605
  ) -> None:
596
606
  """Analyze a repository and produce structured context for AI coding agents.
597
607
 
@@ -798,10 +808,59 @@ def main(
798
808
  no_tree = True # agents never need the raw file tree
799
809
  architecture = True # agents need full architectural signal (M4)
800
810
 
811
+ # ── GAP-9: Cache check — serve from .sourcecode-cache when git SHA unchanged ──
812
+ import hashlib as _hashlib
813
+ import subprocess as _sub
814
+ _cache_dir = target / ".sourcecode-cache"
815
+ _cache_hit_content: Optional[str] = None
816
+ _git_sha = ""
817
+ _cache_key = ""
818
+ _cache_file: Optional[Path] = None
819
+ if not no_cache:
820
+ try:
821
+ _sha_r = _sub.run(
822
+ ["git", "-C", str(target), "rev-parse", "--short", "HEAD"],
823
+ capture_output=True, text=True, timeout=3,
824
+ )
825
+ _git_sha = _sha_r.stdout.strip()
826
+ # Only cache when target IS the git repo root (not a subdir of one),
827
+ # to avoid polluting sub-project directories used in tests.
828
+ if _git_sha and (target / ".git").exists():
829
+ # Include every output-affecting flag so different flag combos never collide
830
+ _flags_str = (
831
+ f"c={compact},ag={agent},fmt={format},full={full},"
832
+ f"co={changed_only},dep={dependencies},gm={graph_modules},"
833
+ f"docs={docs},fm={full_metrics},sem={semantics},"
834
+ f"arch={architecture},gc={git_context},em={env_map},"
835
+ f"cn={code_notes},tree={tree},mode={mode}"
836
+ )
837
+ _flags_h = _hashlib.md5(_flags_str.encode()).hexdigest()[:8]
838
+ _cache_key = f"{_git_sha}-{_flags_h}"
839
+ _cache_file = _cache_dir / f"snapshot-{_cache_key}.json"
840
+ if _cache_file.exists():
841
+ _cache_hit_content = _cache_file.read_text(encoding="utf-8")
842
+ except Exception:
843
+ _git_sha = ""
844
+ _cache_key = ""
845
+ _cache_file = None
846
+
847
+ if _cache_hit_content is not None:
848
+ from sourcecode.serializer import write_output
849
+ write_output(_cache_hit_content, output=output)
850
+ if copy and not output:
851
+ _copy_to_clipboard(_cache_hit_content)
852
+ return
853
+
854
+ # BUG-2: parse --exclude into extra_excludes frozenset
855
+ _extra_excludes: Optional[frozenset[str]] = None
856
+ if exclude:
857
+ _extra_excludes = frozenset(e.strip() for e in exclude.split(",") if e.strip())
858
+
801
859
  _progress = Progress()
802
860
  _progress.start("scanning files")
803
861
 
804
- scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth)
862
+ scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth,
863
+ extra_excludes=_extra_excludes)
805
864
  raw_tree = scanner.scan_tree()
806
865
 
807
866
  # 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
@@ -1514,9 +1573,10 @@ def main(
1514
1573
  content = json.dumps(data, indent=2, ensure_ascii=False)
1515
1574
  elif compact:
1516
1575
  if changed_only and _allowed_changed_files:
1576
+ # GAP-5: preserve full entry_points for architecture context even in
1577
+ # --changed-only mode. Only filter file_paths and code_notes.
1517
1578
  sm = _replace(sm,
1518
1579
  file_paths=[p for p in sm.file_paths if p in _allowed_changed_files],
1519
- entry_points=[ep for ep in sm.entry_points if ep.path in _allowed_changed_files],
1520
1580
  code_notes=[n for n in sm.code_notes if n.path in _allowed_changed_files],
1521
1581
  )
1522
1582
  data = compact_view(sm, no_tree=no_tree, full=full)
@@ -1585,6 +1645,14 @@ def main(
1585
1645
  _progress.finish()
1586
1646
  write_output(content, output=output)
1587
1647
 
1648
+ # GAP-9: Persist to cache for future identical runs (git SHA unchanged)
1649
+ if not no_cache and _cache_key and _cache_file is not None and not _pipeline_error:
1650
+ try:
1651
+ _cache_dir.mkdir(parents=True, exist_ok=True)
1652
+ _cache_file.write_text(content, encoding="utf-8")
1653
+ except Exception:
1654
+ pass
1655
+
1588
1656
  if _pipeline_error:
1589
1657
  raise typer.Exit(code=2)
1590
1658
 
@@ -1734,6 +1802,11 @@ def prepare_context_cmd(
1734
1802
  help="Emit per-phase timing to stderr (git scan ms, symptom scoring ms, total ms)",
1735
1803
  hidden=True,
1736
1804
  ),
1805
+ fast: bool = typer.Option(
1806
+ False,
1807
+ "--fast",
1808
+ help="Skip deep analysis (content search, test gap discovery, code annotations). Uses manifest/metadata only. Target: < 6 s.",
1809
+ ),
1737
1810
  ) -> None:
1738
1811
  """Task-specific context for AI coding agents.
1739
1812
 
@@ -1808,9 +1881,14 @@ def prepare_context_cmd(
1808
1881
  if since:
1809
1882
  _phase += f" since {since}"
1810
1883
  _progress.start(_phase)
1884
+ if not fast:
1885
+ import sys as _sys
1886
+ if _sys.stderr.isatty():
1887
+ _sys.stderr.write(f"Analyzing ({task})... (deep scan may take 15–35 s for large codebases)\n")
1888
+ _sys.stderr.flush()
1811
1889
  _t0 = _time.perf_counter()
1812
1890
  try:
1813
- output = builder.build(task, since=since, symptom=symptom)
1891
+ output = builder.build(task, since=since, symptom=symptom, fast=fast)
1814
1892
  finally:
1815
1893
  _progress.finish()
1816
1894
  _t_total = (_time.perf_counter() - _t0) * 1000
@@ -1960,8 +2038,8 @@ def prepare_context_cmd(
1960
2038
  _sys.stdout.buffer.write(b"\n")
1961
2039
  _sys.stdout.buffer.flush()
1962
2040
  if copy:
1963
- _copy_to_clipboard(_nc_json)
1964
- typer.echo("✓ copied to clipboard", err=True)
2041
+ if _copy_to_clipboard(_nc_json):
2042
+ typer.echo("✓ copied to clipboard", err=True)
1965
2043
  raise typer.Exit()
1966
2044
  if output.ci_decision:
1967
2045
  out["ci_decision"] = output.ci_decision
@@ -2082,6 +2160,10 @@ def prepare_context_cmd(
2082
2160
  out["symptom_note"] = output.symptom_note
2083
2161
  if output.symptom_explain:
2084
2162
  out["symptom_explain"] = output.symptom_explain
2163
+ if getattr(output, "symptom_hint", None):
2164
+ out["symptom_hint"] = output.symptom_hint
2165
+ if getattr(output, "warnings", None):
2166
+ out["warnings"] = output.warnings
2085
2167
  if llm_prompt:
2086
2168
  out["llm_prompt"] = builder.render_prompt(output)
2087
2169
 
@@ -217,6 +217,7 @@ class NodejsDetector(AbstractDetector):
217
217
  _INDEX_PATHS = {"src/index.js", "src/index.ts", "index.js", "index.ts"}
218
218
  for path in [
219
219
  "server.js", "server.ts",
220
+ "main.ts", "main.js", # Angular/root-level entry (e.g. Angular CLI projects)
220
221
  "src/index.js", "src/index.ts",
221
222
  "src/main.js", "src/main.ts", "src/main.tsx",
222
223
  "app/page.tsx", "pages/index.js",
@@ -335,6 +335,7 @@ class TaskOutput:
335
335
  related_notes: list[dict] = field(default_factory=list) # fix-bug + symptom only
336
336
  symptom_note: Optional[str] = None # fix-bug: cross-layer synonym note
337
337
  symptom_explain: Optional[dict] = None # fix-bug: structured evidence breakdown
338
+ symptom_hint: Optional[str] = None # fix-bug: redirect hint when term not found in this module
338
339
  # delta-specific impact fields
339
340
  impact_summary: Optional[str] = None
340
341
  affected_modules: list[str] = field(default_factory=list)
@@ -348,6 +349,7 @@ class TaskOutput:
348
349
  error_code: Optional[str] = None
349
350
  error_message: Optional[str] = None
350
351
  error_hints: list[str] = field(default_factory=list)
352
+ warnings: list[dict] = field(default_factory=list) # structured warnings (REF_NOT_FOUND, etc.)
351
353
  # CI decision state machine — machine-decidable signal
352
354
  ci_decision: Optional[str] = None # "no_changes" | "analysis_success" | "git_ref_error" | "no_git_repo"
353
355
  # git baseline resolution metadata
@@ -602,6 +604,33 @@ _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
602
604
  }
603
605
 
604
606
 
607
+ MAX_FILES_FAST = 2000 # above this threshold --fast uses git-index-only mode
608
+
609
+
610
+ def _count_files_bounded(root: "Path", limit: int = MAX_FILES_FAST + 1) -> int:
611
+ """Count files under root, stopping early once limit reached (O(n) fast exit)."""
612
+ import os as _os
613
+ count = 0
614
+ for _, _, fnames in _os.walk(str(root)):
615
+ count += len(fnames)
616
+ if count >= limit:
617
+ return count
618
+ return count
619
+
620
+
621
+ def _git_changed_files_fast(root: "Path") -> list[str]:
622
+ """Return files reported by git diff --name-only HEAD (for fast-mode scanning)."""
623
+ import subprocess as _sp
624
+ try:
625
+ r = _sp.run(
626
+ ["git", "diff", "--name-only", "HEAD"],
627
+ capture_output=True, text=True, cwd=str(root), timeout=5,
628
+ )
629
+ return [f.strip() for f in r.stdout.splitlines() if f.strip()]
630
+ except Exception:
631
+ return []
632
+
633
+
605
634
  def _build_analysis_scope(
606
635
  *,
607
636
  task_name: str,
@@ -683,7 +712,7 @@ class TaskContextBuilder:
683
712
  def __init__(self, root: Path) -> None:
684
713
  self.root = root
685
714
 
686
- def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None) -> TaskOutput:
715
+ def build(self, task_name: str, *, since: Optional[str] = None, symptom: Optional[str] = None, fast: bool = False) -> TaskOutput:
687
716
  if task_name not in TASKS:
688
717
  raise ValueError(
689
718
  f"Unknown task '{task_name}'. Available: {', '.join(TASKS)}"
@@ -759,6 +788,21 @@ class TaskContextBuilder:
759
788
  # for behavioral_impact reverse lookups without scanning the whole repo).
760
789
  file_tree: dict = {}
761
790
  all_paths = self._expand_scope_for_analysis(_pr_scope_files or [])
791
+ elif fast and _count_files_bounded(self.root) > MAX_FILES_FAST:
792
+ # Fast mode on large repo: git-index-only — only scan git-changed files.
793
+ # Skips full AdaptiveScanner traversal which takes 35s+ on 7k+ file repos.
794
+ _git_files = _git_changed_files_fast(self.root)
795
+ _git_files = [f for f in _git_files if (self.root / f).exists()]
796
+ if not _git_files:
797
+ # Fallback: use a shallow scan (depth 2) to get some context
798
+ scanner = AdaptiveScanner(self.root, base_depth=2)
799
+ file_tree = scanner.scan_tree()
800
+ manifests = scanner.find_manifests()
801
+ all_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
802
+ else:
803
+ file_tree = {}
804
+ all_paths = _git_files
805
+ # Keep manifests from shallow pre-scan
762
806
  else:
763
807
  _topology = RepoClassifier().classify(self.root)
764
808
  _base_depth = 12 if _is_java else 6
@@ -880,7 +924,7 @@ class TaskContextBuilder:
880
924
  improvement_opportunities: list[str] = []
881
925
  cn_notes_for_ranking: list = []
882
926
 
883
- if spec.enable_code_notes:
927
+ if spec.enable_code_notes and not fast:
884
928
  from dataclasses import asdict
885
929
  from sourcecode.code_notes_analyzer import CodeNotesAnalyzer
886
930
 
@@ -1317,6 +1361,7 @@ class TaskContextBuilder:
1317
1361
  related_notes: list[dict] = []
1318
1362
  symptom_note: Optional[str] = None
1319
1363
  symptom_explain: Optional[dict] = None
1364
+ symptom_hint: Optional[str] = None
1320
1365
  if task_name == "fix-bug" and symptom:
1321
1366
  import re as _re
1322
1367
  _camel_expanded = _re.sub(r'([a-z])([A-Z])', r'\1 \2', symptom)
@@ -1410,8 +1455,9 @@ class TaskContextBuilder:
1410
1455
  _existing_paths.add(_p)
1411
1456
  _sx_direct_path.append(_p)
1412
1457
 
1413
- # Pass 5+6 combined: single file read per candidate.
1414
- # Limit content scan to top 80 candidates by current score to bound I/O.
1458
+ # Pass 4b: grep-based injection for frontend→backend synonym terms.
1459
+ # Runs parallel grep for each backend term to find files not yet in
1460
+ # the candidate pool (e.g. AkitaBaseService containing setLoading).
1415
1461
  _src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
1416
1462
  _frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
1417
1463
  _backend_terms_set: list[str] = []
@@ -1421,6 +1467,40 @@ class TaskContextBuilder:
1421
1467
  _bt.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
1422
1468
  _backend_terms_set = list(dict.fromkeys(_bt))
1423
1469
 
1470
+ if _backend_terms_set and not fast:
1471
+ import subprocess as _sp2
1472
+ _grepped: set[str] = set()
1473
+ for _term in _backend_terms_set[:6]:
1474
+ try:
1475
+ _gr = _sp2.run(
1476
+ ["grep", "-r", "-l", "-i", _term,
1477
+ "--include=*.ts", "--include=*.java",
1478
+ str(self.root)],
1479
+ capture_output=True, text=True, timeout=4,
1480
+ )
1481
+ for _line in _gr.stdout.splitlines():
1482
+ try:
1483
+ _rel = str(Path(_line.strip()).relative_to(self.root)).replace("\\", "/")
1484
+ _grepped.add(_rel)
1485
+ except ValueError:
1486
+ pass
1487
+ except Exception:
1488
+ pass
1489
+ _existing_paths_now = {rf.path for rf in relevant_files}
1490
+ for _gf in sorted(_grepped):
1491
+ if _gf in _existing_paths_now:
1492
+ continue
1493
+ if Path(_gf).suffix.lower() not in _src_exts:
1494
+ continue
1495
+ relevant_files.append(RelevantFile(
1496
+ path=_gf,
1497
+ role="symptom_match",
1498
+ score=0.45,
1499
+ reason="content contains backend symptom term (grep)",
1500
+ why=f"grep injection: {', '.join(_backend_terms_set[:3])}",
1501
+ ))
1502
+ _existing_paths_now.add(_gf)
1503
+
1424
1504
  # Sort before content scan so top candidates get read first
1425
1505
  relevant_files = sorted(relevant_files, key=lambda rf: -rf.score)
1426
1506
  _CONTENT_SCAN_LIMIT = 80
@@ -1537,9 +1617,30 @@ class TaskContextBuilder:
1537
1617
  ),
1538
1618
  }
1539
1619
 
1620
+ # BUG #4: LOW confidence + 0 content matches → clear suspected_areas,
1621
+ # emit actionable redirect instead of unrelated files.
1622
+ if _sx_confidence == "LOW" and not _sx_content:
1623
+ suspected_areas = []
1624
+ _is_fe_term = any(kw in _FRONTEND_SYMPTOM_MAP for kw in symptom_keywords)
1625
+ _root_name = self.root.name
1626
+ if _is_fe_term:
1627
+ _fe_redirect = (
1628
+ f"Term {symptom!r} not found in sources under {_root_name!r}. "
1629
+ f"This appears to be a frontend symptom. "
1630
+ f"Try: prepare-context fix-bug . --symptom {symptom!r} "
1631
+ f"(monorepo root) or target a frontend sub-project directly."
1632
+ )
1633
+ else:
1634
+ _fe_redirect = (
1635
+ f"Term {symptom!r} not found in sources under {_root_name!r}. "
1636
+ f"Verify the spelling or try a related term. "
1637
+ f"If this is a frontend symptom, run against the frontend sub-project."
1638
+ )
1639
+ symptom_hint = _fe_redirect
1640
+
1540
1641
  # ── 7. Test gaps (generate-tests only) ────────────────────────────
1541
1642
  test_gaps: list[str] = []
1542
- if task_name == "generate-tests":
1643
+ if task_name == "generate-tests" and not fast:
1543
1644
  def _normalize_test_stem(stem: str) -> str:
1544
1645
  # Java: FooTest / FooTests → Foo; TestFoo → Foo
1545
1646
  if stem.endswith("Tests"):
@@ -1683,6 +1784,8 @@ class TaskContextBuilder:
1683
1784
  resolved_since_ref=_delta_baseline.get("resolved_ref") if task_name == "delta" else None,
1684
1785
  resolution_path=_delta_baseline.get("resolution_path") if task_name == "delta" else None,
1685
1786
  diff_validation_status=_delta_baseline.get("diff_validation_status") if task_name == "delta" else None,
1787
+ warnings=_delta_baseline.get("warnings", []) if task_name == "delta" else [],
1788
+ symptom_hint=symptom_hint if task_name == "fix-bug" else None,
1686
1789
  )
1687
1790
 
1688
1791
  def render_prompt(self, output: TaskOutput) -> str:
@@ -3261,6 +3364,7 @@ class TaskContextBuilder:
3261
3364
  "resolution_path": "head_minus_1_fallback",
3262
3365
  "diff_validation_status": "invalid_ref", # original ref unresolved
3263
3366
  "error": False,
3367
+ "warnings": [{"code": "REF_NOT_FOUND", "ref": since, "resolved_to": "HEAD~1"}],
3264
3368
  }
3265
3369
 
3266
3370
  # All stages failed
@@ -1091,8 +1091,54 @@ def _build_evidence_bundles(
1091
1091
  return bundles
1092
1092
 
1093
1093
 
1094
- def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> list[list[str]]:
1095
- """Connected components of the relation graph (Union-Find, graph-only)."""
1094
+ def _common_package_prefix(fqns: list[str]) -> str:
1095
+ """Longest common dot-separated package prefix across a list of FQNs."""
1096
+ if not fqns:
1097
+ return ""
1098
+ # Strip class/method suffix — keep only package parts (lowercase segments)
1099
+ def pkg_parts(fqn: str) -> list[str]:
1100
+ parts = fqn.split(".")
1101
+ # Drop trailing class/method names (PascalCase or after '#')
1102
+ result = []
1103
+ for p in parts:
1104
+ if "#" in p:
1105
+ break
1106
+ if p and p[0].isupper():
1107
+ break
1108
+ result.append(p)
1109
+ return result
1110
+
1111
+ segs = [pkg_parts(f) for f in fqns if pkg_parts(f)]
1112
+ if not segs:
1113
+ return fqns[0].rsplit(".", 1)[0] if "." in fqns[0] else fqns[0]
1114
+ common = segs[0]
1115
+ for s in segs[1:]:
1116
+ new_common = []
1117
+ for a, b in zip(common, s):
1118
+ if a == b:
1119
+ new_common.append(a)
1120
+ else:
1121
+ break
1122
+ common = new_common
1123
+ if not common:
1124
+ break
1125
+ return ".".join(common) if common else fqns[0].rsplit(".", 1)[0]
1126
+
1127
+
1128
+ def _subsystem_label(package_prefix: str) -> str:
1129
+ """Derive short human label from package prefix (last meaningful segment)."""
1130
+ parts = [p for p in package_prefix.split(".") if p]
1131
+ # Skip generic top-level segments
1132
+ _SKIP = {"com", "org", "net", "io", "java", "javax"}
1133
+ meaningful = [p for p in parts if p not in _SKIP]
1134
+ return meaningful[-1] if meaningful else (parts[-1] if parts else package_prefix)
1135
+
1136
+
1137
+ def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> list[dict]:
1138
+ """Connected components of the relation graph (Union-Find, graph-only).
1139
+
1140
+ Returns labeled subsystem objects instead of raw FQN arrays.
1141
+ """
1096
1142
  fqn_set = set(all_fqns)
1097
1143
  parent: dict[str, str] = {fqn: fqn for fqn in all_fqns}
1098
1144
 
@@ -1117,7 +1163,23 @@ def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> li
1117
1163
  root = find(fqn)
1118
1164
  components.setdefault(root, []).append(fqn)
1119
1165
 
1120
- return [sorted(v) for v in sorted(components.values())]
1166
+ result: list[dict] = []
1167
+ for members in sorted(components.values()):
1168
+ members = sorted(members)
1169
+ pkg_prefix = _common_package_prefix(members)
1170
+ label = _subsystem_label(pkg_prefix)
1171
+ # Summary: first 3 short class names + total count
1172
+ short_names = [m.split(".")[-1].split("#")[0] for m in members[:3]]
1173
+ summary = ", ".join(short_names)
1174
+ if len(members) > 3:
1175
+ summary += f" ... ({len(members)} total)"
1176
+ result.append({
1177
+ "label": label,
1178
+ "package_prefix": pkg_prefix,
1179
+ "member_count": len(members),
1180
+ "summary": summary,
1181
+ })
1182
+ return result
1121
1183
 
1122
1184
 
1123
1185
  _EDGE_REASON_TEMPLATES: dict[str, str] = {
@@ -1287,6 +1349,7 @@ def _assemble(
1287
1349
 
1288
1350
  # Score per node: ir_weight × graph_centrality × diff_intensity × evidence_strength
1289
1351
  # Unchanged nodes: diff_intensity=0 → score=0 (no diff signal)
1352
+ has_diff = bool(sorted_changed)
1290
1353
  node_scores: dict[str, float] = {}
1291
1354
  for sym in sorted_syms:
1292
1355
  fqn = sym.symbol
@@ -1299,6 +1362,28 @@ def _assemble(
1299
1362
  es = bundles[fqn].evidence_strength if fqn in bundles else 0.0
1300
1363
  node_scores[fqn] = round(w * c * di * es, 4) if di > 0 else 0.0
1301
1364
 
1365
+ # No diff signal (no --since): fall back to call-graph centrality scores.
1366
+ # Avoids emitting all-zero scores which mislead agents into thinking the tool is broken.
1367
+ score_basis: str
1368
+ impact_note: Optional[str]
1369
+ if not has_diff and sorted_syms:
1370
+ for sym in sorted_syms:
1371
+ fqn = sym.symbol
1372
+ role = spring_role_map.get(fqn, "other")
1373
+ w = _IR_WEIGHTS.get(role, _IR_WEIGHT_DEFAULT)
1374
+ raw_c = in_deg.get(fqn, 0) + out_deg.get(fqn, 0) + bfs_reach.get(fqn, 0) * 0.1
1375
+ c = min(1.0, raw_c / max_raw)
1376
+ node_scores[fqn] = round(w * c, 4)
1377
+ score_basis = "call_graph_centrality"
1378
+ impact_note = "impact scores based on call-graph centrality (no --since provided)"
1379
+ elif has_diff:
1380
+ score_basis = "diff_impact"
1381
+ impact_note = None
1382
+ else:
1383
+ # No symbols at all — omit scores entirely rather than emit zeros
1384
+ score_basis = "none"
1385
+ impact_note = None
1386
+
1302
1387
  # --- Analysis: classify changed symbols ---
1303
1388
  dropped_fields: list[dict] = []
1304
1389
  changed_entities_out: list[dict] = []
@@ -1435,6 +1520,8 @@ def _assemble(
1435
1520
  },
1436
1521
  "impact": {
1437
1522
  "global_score": global_score,
1523
+ "score_basis": score_basis,
1524
+ **({"impact_note": impact_note} if impact_note else {}),
1438
1525
  "ranked_nodes": ranked_nodes,
1439
1526
  },
1440
1527
  "subsystems": subsystems,
sourcecode/scanner.py CHANGED
@@ -27,6 +27,22 @@ DEFAULT_EXCLUDES: frozenset[str] = frozenset({
27
27
  "dist",
28
28
  "build",
29
29
  "target",
30
+ # Additional generated/IDE/tool dirs
31
+ ".gradle",
32
+ ".planning",
33
+ ".idea",
34
+ "coverage",
35
+ ".sourcecode-cache",
36
+ ".mvn",
37
+ "generated",
38
+ "generated-sources",
39
+ })
40
+
41
+ # File suffixes excluded by default (minified/map files add noise, not signal)
42
+ EXCLUDED_FILE_SUFFIXES: frozenset[str] = frozenset({
43
+ ".min.js",
44
+ ".min.css",
45
+ ".map",
30
46
  })
31
47
 
32
48
  # Nombres de ficheros de manifiesto conocidos (para find_manifests)
@@ -120,13 +136,13 @@ class FileScanner:
120
136
  self._gitignore_spec: Optional[GitIgnoreSpec] = None
121
137
 
122
138
  def _load_gitignore_spec(self) -> GitIgnoreSpec:
123
- """Carga .gitignore del proyecto como GitIgnoreSpec (SCAN-01)."""
139
+ """Carga .gitignore y .sourcecodeIgnore como GitIgnoreSpec (SCAN-01)."""
124
140
  if self._gitignore_spec is None:
125
- gitignore = self.root / ".gitignore"
126
- if gitignore.exists():
127
- lines = gitignore.read_text(encoding="utf-8", errors="replace").splitlines()
128
- else:
129
- lines = []
141
+ lines: list[str] = []
142
+ for ignore_file in (".gitignore", ".sourcecodeIgnore"):
143
+ candidate = self.root / ignore_file
144
+ if candidate.exists():
145
+ lines.extend(candidate.read_text(encoding="utf-8", errors="replace").splitlines())
130
146
  self._gitignore_spec = GitIgnoreSpec.from_lines(lines)
131
147
  return self._gitignore_spec
132
148
 
@@ -183,6 +199,9 @@ class FileScanner:
183
199
  # No legitimate source file starts with "-".
184
200
  if fname.startswith("-"):
185
201
  continue
202
+ # Skip minified/map files — no signal value
203
+ if any(fname.endswith(sfx) for sfx in EXCLUDED_FILE_SUFFIXES):
204
+ continue
186
205
  fpath = current / fname
187
206
  # SCAN-03: no incluir symlinks de fichero
188
207
  if fpath.is_symlink():
sourcecode/serializer.py CHANGED
@@ -248,6 +248,16 @@ def _compact_git_context(sm: "SourceMap") -> "Optional[dict[str, Any]]":
248
248
  for f, n in _fc.most_common(5)
249
249
  ]
250
250
  ctx["hotspots_source"] = "recent_commits"
251
+ if gc.recent_commits:
252
+ ctx["recent_commits"] = [
253
+ {
254
+ "hash": c.hash[:8],
255
+ "message": (c.message or "")[:80],
256
+ "date": (c.date or "")[:10],
257
+ "author": c.author or "",
258
+ }
259
+ for c in gc.recent_commits[:5]
260
+ ]
251
261
  return ctx if ctx else None
252
262
 
253
263
 
@@ -553,6 +563,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
553
563
  if security:
554
564
  result["security"] = security
555
565
  if controllers:
566
+ # Count unique files (classes) vs total entries (methods/endpoints)
567
+ controller_classes = len({c["path"] for c in controllers})
568
+ controller_methods = len(controllers)
569
+
556
570
  # Extract all DDD module names from controller paths and group by domain area.
557
571
  # Path pattern: .../ddd/{module}/infrastructure/rest/*Controller.java
558
572
  _DDD_LAYERS = {"application", "domain", "infrastructure"}
@@ -572,6 +586,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
572
586
  seen_modules.add(module)
573
587
  module_names.append(module)
574
588
 
589
+ _ctrl_note = (
590
+ f"{controller_methods} @RequestMapping methods across "
591
+ f"{controller_classes} controller classes"
592
+ )
575
593
  if len(module_names) > 30:
576
594
  # Group by first path segment under ddd/ (inferred domain area)
577
595
  domain_groups: dict[str, list[str]] = {}
@@ -590,12 +608,16 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
590
608
  if module not in domain_groups[domain_prefix or "other"]:
591
609
  domain_groups[domain_prefix or "other"].append(module)
592
610
  result["controllers"] = {
593
- "count": len(controllers),
611
+ "classes": controller_classes,
612
+ "methods": controller_methods,
613
+ "note": _ctrl_note,
594
614
  "modules": {k: sorted(v) for k, v in sorted(domain_groups.items())},
595
615
  }
596
616
  else:
597
617
  result["controllers"] = {
598
- "count": len(controllers),
618
+ "classes": controller_classes,
619
+ "methods": controller_methods,
620
+ "note": _ctrl_note,
599
621
  "modules": sorted(module_names),
600
622
  }
601
623
  return result
@@ -1305,6 +1327,13 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False, full: bool = False) ->
1305
1327
  result["transactional_boundaries"] = _transactional
1306
1328
  if _git_ctx:
1307
1329
  result["git_context"] = _git_ctx
1330
+ # Angular structural analysis (GAP-10)
1331
+ if sm.project_type in ("angular-spa", "webapp") or any(
1332
+ any(f.name == "Angular" for f in s.frameworks) for s in sm.stacks
1333
+ ):
1334
+ _ang = _angular_analysis(sm)
1335
+ if _ang and (_ang.get("component_count", 0) > 0 or _ang.get("angular_version")):
1336
+ result["angular_analysis"] = _ang
1308
1337
  if _spring_profiles:
1309
1338
  result["spring_profiles"] = _spring_profiles
1310
1339
  _always_include = {"project_type", "project_summary", "architecture_summary", "dependency_summary"}
@@ -1589,6 +1618,88 @@ def validate_cross_analyzer_consistency(
1589
1618
  return findings
1590
1619
 
1591
1620
 
1621
+ def _angular_analysis(sm: "SourceMap") -> "Optional[dict[str, Any]]":
1622
+ """Extract Angular structural metrics for TypeScript/Angular projects (GAP-10)."""
1623
+ import json as _json
1624
+ import re as _re
1625
+
1626
+ ts_files = [p for p in sm.file_paths if p.endswith(".ts") and not p.endswith(".d.ts")]
1627
+ if not ts_files:
1628
+ return None
1629
+
1630
+ root = Path(sm.metadata.analyzed_path) if sm.metadata.analyzed_path else Path(".")
1631
+
1632
+ component_count = 0
1633
+ service_count = 0
1634
+ lazy_routes_count = 0
1635
+ akita_stores = 0
1636
+ standalone_components = False
1637
+ route_paths: list[str] = []
1638
+
1639
+ for rel in ts_files:
1640
+ try:
1641
+ content = (root / rel).read_text(encoding="utf-8", errors="replace")
1642
+ except OSError:
1643
+ continue
1644
+ component_count += content.count("@Component(")
1645
+ service_count += content.count("@Injectable(")
1646
+ lazy_routes_count += content.count("loadChildren(")
1647
+ akita_stores += content.count("@StoreConfig(")
1648
+ if not standalone_components and "bootstrapApplication(" in content:
1649
+ standalone_components = True
1650
+ # Route tree: parse path: '...' in routing files
1651
+ fname = rel.replace("\\", "/").split("/")[-1]
1652
+ if "routing" in fname or fname in ("app.routes.ts",):
1653
+ for m in _re.finditer(r"path\s*:\s*['\"]([^'\"]*)['\"]", content):
1654
+ val = m.group(1)
1655
+ if val and val not in route_paths:
1656
+ route_paths.append(val)
1657
+
1658
+ # Angular version from package.json
1659
+ angular_version: Optional[str] = None
1660
+ pkg_json = root / "package.json"
1661
+ if pkg_json.exists():
1662
+ try:
1663
+ pkg = _json.loads(pkg_json.read_text(encoding="utf-8", errors="replace"))
1664
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
1665
+ av = deps.get("@angular/core")
1666
+ if av:
1667
+ angular_version = av.lstrip("^~>=")
1668
+ except Exception:
1669
+ pass
1670
+
1671
+ # Also check angular.json for entry point
1672
+ entry_point: Optional[str] = None
1673
+ angular_json = root / "angular.json"
1674
+ if angular_json.exists():
1675
+ try:
1676
+ aj = _json.loads(angular_json.read_text(encoding="utf-8", errors="replace"))
1677
+ projects = aj.get("projects") or {}
1678
+ for proj in projects.values():
1679
+ main = (
1680
+ (proj.get("architect") or {})
1681
+ .get("build", {})
1682
+ .get("options", {})
1683
+ .get("main")
1684
+ )
1685
+ if main:
1686
+ entry_point = main
1687
+ break
1688
+ except Exception:
1689
+ pass
1690
+
1691
+ return {
1692
+ "angular_version": angular_version,
1693
+ "standalone_components": standalone_components,
1694
+ "lazy_routes_count": lazy_routes_count,
1695
+ "akita_stores": akita_stores,
1696
+ "route_tree": route_paths[:20],
1697
+ "component_count": component_count,
1698
+ "service_count": service_count,
1699
+ **({"entry_point": entry_point} if entry_point else {}),
1700
+ }
1701
+
1702
+
1592
1703
  def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
1593
1704
  """Opinionated output for AI agents — structured, noise-free, gap-aware.
1594
1705
 
@@ -1813,6 +1924,14 @@ def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
1813
1924
  if signals:
1814
1925
  result["signals"] = signals
1815
1926
 
1927
+ # ── 8b. Angular structural analysis (GAP-10) ──────────────────────────────
1928
+ if sm.project_type in ("angular-spa", "webapp") or any(
1929
+ any(f.name == "Angular" for f in s.frameworks) for s in sm.stacks
1930
+ ):
1931
+ _ang = _angular_analysis(sm)
1932
+ if _ang and (_ang.get("component_count", 0) > 0 or _ang.get("angular_version")):
1933
+ result["angular_analysis"] = _ang
1934
+
1816
1935
  # ── 9. Git context — lightweight (top-5 hotspots, branch, uncommitted count)
1817
1936
  _gc = _compact_git_context(sm)
1818
1937
  if _gc:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.30.27
3
+ Version: 1.30.29
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -221,7 +221,7 @@ Description-Content-Type: text/markdown
221
221
 
222
222
  **Deterministic, behavior-aware codebase context for AI agents and PR review.**
223
223
 
224
- ![Version](https://img.shields.io/badge/version-1.30.27-blue)
224
+ ![Version](https://img.shields.io/badge/version-1.30.29-blue)
225
225
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
226
226
 
227
227
  ---
@@ -257,7 +257,7 @@ pipx install sourcecode
257
257
 
258
258
  ```bash
259
259
  sourcecode version
260
- # sourcecode 1.30.27
260
+ # sourcecode 1.30.29
261
261
  ```
262
262
 
263
263
  ---
@@ -1,10 +1,10 @@
1
- sourcecode/__init__.py,sha256=dwmvjucHTiWMZcDP2j41KFmIxCmu9Ib5cScfJsnEz_Q,104
2
- sourcecode/adaptive_scanner.py,sha256=RTNExwWPXzjgLaRueT7UuxkPj5ZEToWjGbx1j0LSZ9E,10250
1
+ sourcecode/__init__.py,sha256=dlUu_PMRN5Q_pvsTBF3LbFJMORBN4BVQ0B9j6mA3GWQ,104
2
+ sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
6
- sourcecode/classifier.py,sha256=pYve2J1LqtYssU3lYLMDz18PT-CjN5c18QYE7R_IG1Q,7507
7
- sourcecode/cli.py,sha256=xNbK_xwsqdhUbAJT4VieJDitGlhMu_O_Y5xzY8yrc9Q,94497
6
+ sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
7
+ sourcecode/cli.py,sha256=5hKnqahlEko1Z1GV1pg0uzEH2hES3RRqxJ95CRBgXwc,98320
8
8
  sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
9
9
  sourcecode/confidence_analyzer.py,sha256=H9VHYRzZhqMFlSCZffjtsMUGYLnDvrq1g5FjzyQ1hxE,16381
10
10
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -22,18 +22,18 @@ sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13
22
22
  sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
23
23
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
24
24
  sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
25
- sourcecode/prepare_context.py,sha256=4BwSEXUEuWiYaRZ8wPUcClSW5_Pl1BuUGe-VqM9MSPQ,166688
25
+ sourcecode/prepare_context.py,sha256=Too1ziyHGGvZfNhCMfgCvrTI01xYQeUrwA4veXtgTSI,172167
26
26
  sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
27
27
  sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
28
28
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
29
29
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
30
30
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
31
- sourcecode/repository_ir.py,sha256=e9TzNbfo_2Cyh3aASFZBq--rPjBMIYMuw5smIWJ5hx8,61099
31
+ sourcecode/repository_ir.py,sha256=KH5EehbjOh8ZwwTHcbzrAHiKDoquO49wgSvCX4bVq5k,64391
32
32
  sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
33
- sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
33
+ sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
34
34
  sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
35
35
  sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
36
- sourcecode/serializer.py,sha256=2Xi7bJUBiwsAGshy1a_CFgrYKudadakFAfKp_Lz2-qI,105749
36
+ sourcecode/serializer.py,sha256=JaEJonNIKaSV6dST3Jn8_Z6alYK9Ba1-M3luh8Xf-yw,110442
37
37
  sourcecode/summarizer.py,sha256=lPlKhMh28nueXkPo2xKeD3DUFYVGRlJMIdY-8TSM-ls,17486
38
38
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
39
39
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -48,7 +48,7 @@ sourcecode/detectors/heuristic.py,sha256=bCqqgbHavl4Sse3dqT8mwmo1wAdgeJr7VyXOmfC
48
48
  sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
49
49
  sourcecode/detectors/java.py,sha256=ldvaDZJADAKslOpS5qJ0bxexgeY6h_4Yx4NCtHio7J8,24203
50
50
  sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
51
- sourcecode/detectors/nodejs.py,sha256=7fsyAmrGkkguX6U80HUQpIe9MRaYyi_A7zbaRtmFmGc,13097
51
+ sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
52
52
  sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
53
53
  sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
54
54
  sourcecode/detectors/project.py,sha256=ghWWOlqg2_uywzeZX573CCkFWQPlc8bBGvvX1BfIDYI,8315
@@ -64,8 +64,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
64
64
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
65
65
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
66
66
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
67
- sourcecode-1.30.27.dist-info/METADATA,sha256=mi09pFkJXE7YELroOo3ewgHjwQp8je7jMFEvR2uiGYo,28956
68
- sourcecode-1.30.27.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
- sourcecode-1.30.27.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
- sourcecode-1.30.27.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
- sourcecode-1.30.27.dist-info/RECORD,,
67
+ sourcecode-1.30.29.dist-info/METADATA,sha256=39QsgfOfPa-UUti5nV0iLi-gH7KSIz48owb5RoyX12Q,28956
68
+ sourcecode-1.30.29.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
69
+ sourcecode-1.30.29.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
70
+ sourcecode-1.30.29.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
71
+ sourcecode-1.30.29.dist-info/RECORD,,