sourcecode 1.30.28__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 +1 -1
- sourcecode/adaptive_scanner.py +11 -7
- sourcecode/classifier.py +8 -1
- sourcecode/cli.py +70 -2
- sourcecode/detectors/nodejs.py +1 -0
- sourcecode/prepare_context.py +79 -2
- sourcecode/repository_ir.py +90 -3
- sourcecode/scanner.py +25 -6
- sourcecode/serializer.py +111 -2
- {sourcecode-1.30.28.dist-info → sourcecode-1.30.29.dist-info}/METADATA +3 -3
- {sourcecode-1.30.28.dist-info → sourcecode-1.30.29.dist-info}/RECORD +14 -14
- {sourcecode-1.30.28.dist-info → sourcecode-1.30.29.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.28.dist-info → sourcecode-1.30.29.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.28.dist-info → sourcecode-1.30.29.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/adaptive_scanner.py
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
@@ -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
|
|
sourcecode/detectors/nodejs.py
CHANGED
|
@@ -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",
|
sourcecode/prepare_context.py
CHANGED
|
@@ -604,6 +604,33 @@ _FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
|
|
|
604
604
|
}
|
|
605
605
|
|
|
606
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
|
+
|
|
607
634
|
def _build_analysis_scope(
|
|
608
635
|
*,
|
|
609
636
|
task_name: str,
|
|
@@ -761,6 +788,21 @@ class TaskContextBuilder:
|
|
|
761
788
|
# for behavioral_impact reverse lookups without scanning the whole repo).
|
|
762
789
|
file_tree: dict = {}
|
|
763
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
|
|
764
806
|
else:
|
|
765
807
|
_topology = RepoClassifier().classify(self.root)
|
|
766
808
|
_base_depth = 12 if _is_java else 6
|
|
@@ -1413,8 +1455,9 @@ class TaskContextBuilder:
|
|
|
1413
1455
|
_existing_paths.add(_p)
|
|
1414
1456
|
_sx_direct_path.append(_p)
|
|
1415
1457
|
|
|
1416
|
-
# Pass
|
|
1417
|
-
#
|
|
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).
|
|
1418
1461
|
_src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
|
|
1419
1462
|
_frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
|
|
1420
1463
|
_backend_terms_set: list[str] = []
|
|
@@ -1424,6 +1467,40 @@ class TaskContextBuilder:
|
|
|
1424
1467
|
_bt.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
|
|
1425
1468
|
_backend_terms_set = list(dict.fromkeys(_bt))
|
|
1426
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
|
+
|
|
1427
1504
|
# Sort before content scan so top candidates get read first
|
|
1428
1505
|
relevant_files = sorted(relevant_files, key=lambda rf: -rf.score)
|
|
1429
1506
|
_CONTENT_SCAN_LIMIT = 80
|
sourcecode/repository_ir.py
CHANGED
|
@@ -1091,8 +1091,54 @@ def _build_evidence_bundles(
|
|
|
1091
1091
|
return bundles
|
|
1092
1092
|
|
|
1093
1093
|
|
|
1094
|
-
def
|
|
1095
|
-
"""
|
|
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
|
-
|
|
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
|
|
139
|
+
"""Carga .gitignore y .sourcecodeIgnore como GitIgnoreSpec (SCAN-01)."""
|
|
124
140
|
if self._gitignore_spec is None:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
@@ -563,6 +563,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
563
563
|
if security:
|
|
564
564
|
result["security"] = security
|
|
565
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
|
+
|
|
566
570
|
# Extract all DDD module names from controller paths and group by domain area.
|
|
567
571
|
# Path pattern: .../ddd/{module}/infrastructure/rest/*Controller.java
|
|
568
572
|
_DDD_LAYERS = {"application", "domain", "infrastructure"}
|
|
@@ -582,6 +586,10 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
582
586
|
seen_modules.add(module)
|
|
583
587
|
module_names.append(module)
|
|
584
588
|
|
|
589
|
+
_ctrl_note = (
|
|
590
|
+
f"{controller_methods} @RequestMapping methods across "
|
|
591
|
+
f"{controller_classes} controller classes"
|
|
592
|
+
)
|
|
585
593
|
if len(module_names) > 30:
|
|
586
594
|
# Group by first path segment under ddd/ (inferred domain area)
|
|
587
595
|
domain_groups: dict[str, list[str]] = {}
|
|
@@ -600,12 +608,16 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
|
|
|
600
608
|
if module not in domain_groups[domain_prefix or "other"]:
|
|
601
609
|
domain_groups[domain_prefix or "other"].append(module)
|
|
602
610
|
result["controllers"] = {
|
|
603
|
-
"
|
|
611
|
+
"classes": controller_classes,
|
|
612
|
+
"methods": controller_methods,
|
|
613
|
+
"note": _ctrl_note,
|
|
604
614
|
"modules": {k: sorted(v) for k, v in sorted(domain_groups.items())},
|
|
605
615
|
}
|
|
606
616
|
else:
|
|
607
617
|
result["controllers"] = {
|
|
608
|
-
"
|
|
618
|
+
"classes": controller_classes,
|
|
619
|
+
"methods": controller_methods,
|
|
620
|
+
"note": _ctrl_note,
|
|
609
621
|
"modules": sorted(module_names),
|
|
610
622
|
}
|
|
611
623
|
return result
|
|
@@ -1315,6 +1327,13 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False, full: bool = False) ->
|
|
|
1315
1327
|
result["transactional_boundaries"] = _transactional
|
|
1316
1328
|
if _git_ctx:
|
|
1317
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
|
|
1318
1337
|
if _spring_profiles:
|
|
1319
1338
|
result["spring_profiles"] = _spring_profiles
|
|
1320
1339
|
_always_include = {"project_type", "project_summary", "architecture_summary", "dependency_summary"}
|
|
@@ -1599,6 +1618,88 @@ def validate_cross_analyzer_consistency(
|
|
|
1599
1618
|
return findings
|
|
1600
1619
|
|
|
1601
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
|
+
|
|
1602
1703
|
def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
|
|
1603
1704
|
"""Opinionated output for AI agents — structured, noise-free, gap-aware.
|
|
1604
1705
|
|
|
@@ -1823,6 +1924,14 @@ def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
|
|
|
1823
1924
|
if signals:
|
|
1824
1925
|
result["signals"] = signals
|
|
1825
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
|
+
|
|
1826
1935
|
# ── 9. Git context — lightweight (top-5 hotspots, branch, uncommitted count)
|
|
1827
1936
|
_gc = _compact_git_context(sm)
|
|
1828
1937
|
if _gc:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.30.
|
|
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
|
-

|
|
225
225
|

|
|
226
226
|
|
|
227
227
|
---
|
|
@@ -257,7 +257,7 @@ pipx install sourcecode
|
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
259
|
sourcecode version
|
|
260
|
-
# sourcecode 1.30.
|
|
260
|
+
# sourcecode 1.30.29
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
---
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
2
|
-
sourcecode/adaptive_scanner.py,sha256=
|
|
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
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
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=
|
|
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=
|
|
31
|
+
sourcecode/repository_ir.py,sha256=KH5EehbjOh8ZwwTHcbzrAHiKDoquO49wgSvCX4bVq5k,64391
|
|
32
32
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
33
|
-
sourcecode/scanner.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
68
|
-
sourcecode-1.30.
|
|
69
|
-
sourcecode-1.30.
|
|
70
|
-
sourcecode-1.30.
|
|
71
|
-
sourcecode-1.30.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|