sourcecode 1.30.17__py3-none-any.whl → 1.30.19__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/cli.py +59 -3
- sourcecode/contract_pipeline.py +2 -0
- sourcecode/git_analyzer.py +7 -0
- sourcecode/prepare_context.py +189 -67
- sourcecode/repository_ir.py +95 -1
- {sourcecode-1.30.17.dist-info → sourcecode-1.30.19.dist-info}/METADATA +3 -3
- {sourcecode-1.30.17.dist-info → sourcecode-1.30.19.dist-info}/RECORD +11 -11
- {sourcecode-1.30.17.dist-info → sourcecode-1.30.19.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.17.dist-info → sourcecode-1.30.19.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.17.dist-info → sourcecode-1.30.19.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -1648,6 +1648,12 @@ def prepare_context_cmd(
|
|
|
1648
1648
|
"--format",
|
|
1649
1649
|
help="Output format: json (default) | github-comment (Markdown PR comment for review-pr task)",
|
|
1650
1650
|
),
|
|
1651
|
+
debug_perf: bool = typer.Option(
|
|
1652
|
+
False,
|
|
1653
|
+
"--debug-perf",
|
|
1654
|
+
help="Emit per-phase timing to stderr (git scan ms, symptom scoring ms, total ms)",
|
|
1655
|
+
hidden=True,
|
|
1656
|
+
),
|
|
1651
1657
|
) -> None:
|
|
1652
1658
|
"""Task-specific context for AI coding agents.
|
|
1653
1659
|
|
|
@@ -1714,9 +1720,21 @@ def prepare_context_cmd(
|
|
|
1714
1720
|
raise typer.Exit()
|
|
1715
1721
|
|
|
1716
1722
|
from dataclasses import asdict
|
|
1723
|
+
import time as _time
|
|
1717
1724
|
|
|
1718
1725
|
builder = TaskContextBuilder(target)
|
|
1726
|
+
_t0 = _time.perf_counter()
|
|
1719
1727
|
output = builder.build(task, since=since, symptom=symptom)
|
|
1728
|
+
_t_total = (_time.perf_counter() - _t0) * 1000
|
|
1729
|
+
|
|
1730
|
+
if debug_perf:
|
|
1731
|
+
_perf = getattr(output, "_perf_ms", {}) or {}
|
|
1732
|
+
typer.echo(
|
|
1733
|
+
f"[debug-perf] total={_t_total:.0f}ms"
|
|
1734
|
+
+ (f" git_scan={_perf.get('git_scan_ms', '?')}ms" if "git_scan_ms" in _perf else "")
|
|
1735
|
+
+ (f" symptom={_perf.get('symptom_ms', '?')}ms" if "symptom_ms" in _perf else ""),
|
|
1736
|
+
err=True,
|
|
1737
|
+
)
|
|
1720
1738
|
|
|
1721
1739
|
# Task-specific content-filter: each task emphasizes different output fields.
|
|
1722
1740
|
# Fields marked False are suppressed from this task's output to reduce noise.
|
|
@@ -1918,6 +1936,8 @@ def prepare_context_cmd(
|
|
|
1918
1936
|
out["related_notes"] = output.related_notes
|
|
1919
1937
|
if output.symptom_note:
|
|
1920
1938
|
out["symptom_note"] = output.symptom_note
|
|
1939
|
+
if output.symptom_explain:
|
|
1940
|
+
out["symptom_explain"] = output.symptom_explain
|
|
1921
1941
|
if llm_prompt:
|
|
1922
1942
|
out["llm_prompt"] = builder.render_prompt(output)
|
|
1923
1943
|
|
|
@@ -2012,12 +2032,33 @@ def repo_ir_cmd(
|
|
|
2012
2032
|
"--include-tests",
|
|
2013
2033
|
help="Include test files in analysis (excluded by default)",
|
|
2014
2034
|
),
|
|
2035
|
+
max_nodes: Optional[int] = typer.Option(
|
|
2036
|
+
None,
|
|
2037
|
+
"--max-nodes",
|
|
2038
|
+
help="Limit graph.nodes to top N by impact score (reduces output size)",
|
|
2039
|
+
),
|
|
2040
|
+
max_edges: Optional[int] = typer.Option(
|
|
2041
|
+
None,
|
|
2042
|
+
"--max-edges",
|
|
2043
|
+
help="Limit graph.edges to N (priority: edges between kept nodes)",
|
|
2044
|
+
),
|
|
2045
|
+
summary_only: bool = typer.Option(
|
|
2046
|
+
False,
|
|
2047
|
+
"--summary-only",
|
|
2048
|
+
help="Omit full graph.nodes/edges; keep analysis summary, impact, and change_set (<300KB typical)",
|
|
2049
|
+
),
|
|
2015
2050
|
) -> None:
|
|
2016
2051
|
"""Deterministic symbol-level IR for Java repositories.
|
|
2017
2052
|
|
|
2018
2053
|
\b
|
|
2019
2054
|
Extracts symbols, relations, Spring roles, and (with --since) symbol-level diffs.
|
|
2020
|
-
Output is JSON:
|
|
2055
|
+
Output is JSON: graph{nodes,edges}, analysis, impact, subsystems, change_set.
|
|
2056
|
+
|
|
2057
|
+
\b
|
|
2058
|
+
Size control:
|
|
2059
|
+
--summary-only Omit full graph; keep analysis + impact (smallest output)
|
|
2060
|
+
--max-nodes N Keep top N nodes by score
|
|
2061
|
+
--max-edges N Keep top N edges (priority: both endpoints kept)
|
|
2021
2062
|
|
|
2022
2063
|
\b
|
|
2023
2064
|
Examples:
|
|
@@ -2025,11 +2066,13 @@ def repo_ir_cmd(
|
|
|
2025
2066
|
sourcecode repo-ir /path/to/repo --since HEAD~1
|
|
2026
2067
|
sourcecode repo-ir --files src/main/java/UserService.java
|
|
2027
2068
|
sourcecode repo-ir --since main --output ir.json
|
|
2069
|
+
sourcecode repo-ir --since HEAD~3 --summary-only --output ir-small.json
|
|
2070
|
+
sourcecode repo-ir --max-nodes 200 --max-edges 500
|
|
2028
2071
|
"""
|
|
2029
2072
|
import json as _json
|
|
2030
2073
|
import sys as _sys
|
|
2031
2074
|
|
|
2032
|
-
from sourcecode.repository_ir import build_repo_ir, find_java_files
|
|
2075
|
+
from sourcecode.repository_ir import apply_ir_size_limits, build_repo_ir, find_java_files
|
|
2033
2076
|
|
|
2034
2077
|
root = path.resolve()
|
|
2035
2078
|
if not root.is_dir():
|
|
@@ -2063,11 +2106,24 @@ def repo_ir_cmd(
|
|
|
2063
2106
|
return
|
|
2064
2107
|
|
|
2065
2108
|
ir = build_repo_ir(file_list, root, since=since)
|
|
2109
|
+
ir = apply_ir_size_limits(
|
|
2110
|
+
ir,
|
|
2111
|
+
max_nodes=max_nodes,
|
|
2112
|
+
max_edges=max_edges,
|
|
2113
|
+
summary_only=summary_only,
|
|
2114
|
+
)
|
|
2066
2115
|
output = _json.dumps(ir, indent=2, ensure_ascii=False)
|
|
2067
2116
|
|
|
2068
2117
|
if output_path:
|
|
2069
2118
|
output_path.write_text(output, encoding="utf-8")
|
|
2070
|
-
|
|
2119
|
+
n_nodes = len((ir.get("graph") or {}).get("nodes") or [])
|
|
2120
|
+
n_edges = len((ir.get("graph") or {}).get("edges") or [])
|
|
2121
|
+
size_kb = len(output.encode("utf-8")) // 1024
|
|
2122
|
+
typer.echo(
|
|
2123
|
+
f"IR written to {output_path} "
|
|
2124
|
+
f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)",
|
|
2125
|
+
err=True,
|
|
2126
|
+
)
|
|
2071
2127
|
else:
|
|
2072
2128
|
_sys.stdout.write(output)
|
|
2073
2129
|
_sys.stdout.write("\n")
|
sourcecode/contract_pipeline.py
CHANGED
sourcecode/git_analyzer.py
CHANGED
|
@@ -52,8 +52,15 @@ _HOTSPOT_AUX_DIRS: frozenset[str] = frozenset({
|
|
|
52
52
|
"sandbox", "sandboxes",
|
|
53
53
|
"ci", "translations", "locales", "locale", "i18n", "l10n",
|
|
54
54
|
".planning",
|
|
55
|
+
# vendor / build dirs: high path count, zero operational signal
|
|
56
|
+
"vendor", "node_modules", "dist", "target", "build",
|
|
57
|
+
".gradle", ".mvn", "generated-sources", "generated-resources",
|
|
55
58
|
})
|
|
56
59
|
|
|
60
|
+
# When git diff returns more changed files than this, degrade gracefully:
|
|
61
|
+
# truncate hotspot analysis depth and emit a structured warning.
|
|
62
|
+
_CHANGED_FILES_DEGRADATION_THRESHOLD = 200
|
|
63
|
+
|
|
57
64
|
|
|
58
65
|
def _run_git(args: list[str], cwd: Path, timeout: int = 15) -> tuple[str, int]:
|
|
59
66
|
result = subprocess.run(
|
sourcecode/prepare_context.py
CHANGED
|
@@ -333,6 +333,7 @@ class TaskOutput:
|
|
|
333
333
|
symptom: Optional[str] = None # fix-bug only
|
|
334
334
|
related_notes: list[dict] = field(default_factory=list) # fix-bug + symptom only
|
|
335
335
|
symptom_note: Optional[str] = None # fix-bug: cross-layer synonym note
|
|
336
|
+
symptom_explain: Optional[dict] = None # fix-bug: structured evidence breakdown
|
|
336
337
|
# delta-specific impact fields
|
|
337
338
|
impact_summary: Optional[str] = None
|
|
338
339
|
affected_modules: list[str] = field(default_factory=list)
|
|
@@ -561,17 +562,23 @@ _ARTIFACT_CHANGE_EFFECT: dict[str, str] = {
|
|
|
561
562
|
# Maps frontend symptom keywords → backend terms likely to contain the root cause.
|
|
562
563
|
# Used to boost service/interceptor files when the symptom is UI-only.
|
|
563
564
|
_FRONTEND_SYMPTOM_MAP: dict[str, list[str]] = {
|
|
564
|
-
"spinner":
|
|
565
|
-
"loading":
|
|
566
|
-
"login":
|
|
567
|
-
"logout":
|
|
568
|
-
"dropdown":
|
|
569
|
-
"modal":
|
|
570
|
-
"popup":
|
|
571
|
-
"table":
|
|
572
|
-
"grid":
|
|
573
|
-
"button":
|
|
574
|
-
"form":
|
|
565
|
+
"spinner": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
|
|
566
|
+
"loading": ["loading", "setloading", "finalize", "httpinterceptor", "interceptor", "service"],
|
|
567
|
+
"login": ["authcontroller", "securityconfig", "filterconfig", "jwtfilter", "auth", "authentication"],
|
|
568
|
+
"logout": ["authcontroller", "securityconfig", "jwtfilter", "auth", "session"],
|
|
569
|
+
"dropdown": ["getmapping", "findall", "obtenertodos", "listall", "findby"],
|
|
570
|
+
"modal": ["controller", "getmapping", "findby", "search"],
|
|
571
|
+
"popup": ["controller", "getmapping", "findby", "search"],
|
|
572
|
+
"table": ["paginated", "findby", "search", "getmapping", "listall"],
|
|
573
|
+
"grid": ["paginated", "findby", "search", "getmapping"],
|
|
574
|
+
"button": ["postmapping", "putmapping", "deletemapping", "controller", "service"],
|
|
575
|
+
"form": ["postmapping", "putmapping", "controller", "service", "dto"],
|
|
576
|
+
# session-related symptoms (Spanish + English)
|
|
577
|
+
"sesion": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
|
|
578
|
+
"sesiones": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
|
|
579
|
+
"session": ["httpsession", "sessionmanager", "sessionservice", "sessionrepository", "sessionfactory", "authentication"],
|
|
580
|
+
# worker/assignment domain terms (common in RRHH/HR systems)
|
|
581
|
+
"trabajador": ["trabajador", "empleado", "worker", "asignacion", "trabajadordao", "trabajadorservice"],
|
|
575
582
|
}
|
|
576
583
|
|
|
577
584
|
|
|
@@ -812,7 +819,7 @@ class TaskContextBuilder:
|
|
|
812
819
|
if not self._is_source(_p) or self._is_test(_p):
|
|
813
820
|
continue
|
|
814
821
|
try:
|
|
815
|
-
_loc = len((self.root / _p).read_text(errors="replace").splitlines())
|
|
822
|
+
_loc = len((self.root / _p).read_text(encoding="utf-8", errors="replace").splitlines())
|
|
816
823
|
if _loc > 200:
|
|
817
824
|
_large.append((_loc, _p))
|
|
818
825
|
except OSError:
|
|
@@ -826,12 +833,14 @@ class TaskContextBuilder:
|
|
|
826
833
|
# ── 5b. Git signals for ranking ────────────────────────────────────
|
|
827
834
|
git_hotspots: dict[str, int] = {}
|
|
828
835
|
uncommitted_files: set[str] = set()
|
|
836
|
+
_recent_commits_for_symptom: list = []
|
|
829
837
|
try:
|
|
830
838
|
from sourcecode.git_analyzer import GitAnalyzer
|
|
831
839
|
_gc = GitAnalyzer().analyze(self.root, depth=30, days=90)
|
|
832
840
|
_bad = {"no_git_repo", "git_not_found", "git_timeout"}
|
|
833
841
|
if _gc and not (_bad & set(_gc.limitations)):
|
|
834
842
|
git_hotspots = {h.file: h.commit_count for h in _gc.change_hotspots}
|
|
843
|
+
_recent_commits_for_symptom = list(_gc.recent_commits)
|
|
835
844
|
if _gc.uncommitted_changes:
|
|
836
845
|
_uc = _gc.uncommitted_changes
|
|
837
846
|
uncommitted_files = set(_uc.staged) | set(_uc.unstaged)
|
|
@@ -1199,6 +1208,7 @@ class TaskContextBuilder:
|
|
|
1199
1208
|
symptom_keywords: list[str] = []
|
|
1200
1209
|
related_notes: list[dict] = []
|
|
1201
1210
|
symptom_note: Optional[str] = None
|
|
1211
|
+
symptom_explain: Optional[dict] = None
|
|
1202
1212
|
if task_name == "fix-bug" and symptom:
|
|
1203
1213
|
import re as _re
|
|
1204
1214
|
_camel_expanded = _re.sub(r'([a-z])([A-Z])', r'\1 \2', symptom)
|
|
@@ -1208,19 +1218,68 @@ class TaskContextBuilder:
|
|
|
1208
1218
|
if len(w) > 2
|
|
1209
1219
|
]
|
|
1210
1220
|
if symptom_keywords:
|
|
1211
|
-
#
|
|
1221
|
+
# Pre-compile combined keyword pattern for fast content scanning
|
|
1222
|
+
_kw_re = _re.compile(
|
|
1223
|
+
"|".join(_re.escape(kw) for kw in symptom_keywords),
|
|
1224
|
+
_re.IGNORECASE,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
# Structured evidence collectors for symptom_explain
|
|
1228
|
+
_sx_direct_path: list[str] = []
|
|
1229
|
+
_sx_content: list[str] = []
|
|
1230
|
+
_sx_commits: list[dict] = []
|
|
1231
|
+
_sx_synonyms: list[str] = []
|
|
1232
|
+
_sx_boosts: list[dict] = []
|
|
1233
|
+
|
|
1234
|
+
# Pass 1: surface code notes whose text contains any keyword
|
|
1235
|
+
_note_matched_paths: dict[str, int] = {} # path → count of matching notes
|
|
1212
1236
|
for _n in cn_notes_for_ranking:
|
|
1213
1237
|
_text = (getattr(_n, "text", "") or "").lower()
|
|
1214
|
-
if
|
|
1238
|
+
if _kw_re.search(_text):
|
|
1239
|
+
_np = getattr(_n, "path", "")
|
|
1215
1240
|
related_notes.append({
|
|
1216
1241
|
"kind": getattr(_n, "kind", ""),
|
|
1217
|
-
"path":
|
|
1242
|
+
"path": _np,
|
|
1218
1243
|
"line": getattr(_n, "line", None),
|
|
1219
1244
|
"text": getattr(_n, "text", ""),
|
|
1220
1245
|
})
|
|
1221
|
-
|
|
1222
|
-
|
|
1246
|
+
_note_matched_paths[_np] = _note_matched_paths.get(_np, 0) + 1
|
|
1247
|
+
|
|
1248
|
+
# Pass 2: build commit message index — cap at 60 most-recent commits.
|
|
1249
|
+
# Files touched in commits whose message matches a symptom keyword get
|
|
1250
|
+
# a strong recency signal. Primary signal for domain keywords ("sesiones")
|
|
1251
|
+
# that appear in commit messages but not file paths.
|
|
1252
|
+
_commit_file_hits: dict[str, int] = {} # path → n matching commits
|
|
1253
|
+
_commits_scanned = _recent_commits_for_symptom[:60]
|
|
1254
|
+
for _cr in _commits_scanned:
|
|
1255
|
+
_msg_lower = (_cr.message or "").lower()
|
|
1256
|
+
if _kw_re.search(_msg_lower):
|
|
1257
|
+
for _cf in (_cr.files_changed or []):
|
|
1258
|
+
_cf_norm = _cf.replace("\\", "/")
|
|
1259
|
+
_commit_file_hits[_cf_norm] = _commit_file_hits.get(_cf_norm, 0) + 1
|
|
1260
|
+
_sx_commits.append({
|
|
1261
|
+
"message": (_cr.message or "")[:80],
|
|
1262
|
+
"files": list((_cr.files_changed or [])[:5]),
|
|
1263
|
+
})
|
|
1264
|
+
|
|
1265
|
+
# Pass 3: inject files from commit index not yet in candidate pool
|
|
1223
1266
|
_existing_paths = {rf.path for rf in relevant_files}
|
|
1267
|
+
for _cp, _nhits in _commit_file_hits.items():
|
|
1268
|
+
if _cp in _existing_paths:
|
|
1269
|
+
continue
|
|
1270
|
+
if Path(_cp).suffix.lower() not in _ALL_EXTENSIONS:
|
|
1271
|
+
continue
|
|
1272
|
+
_ci_score = round(min(0.5 + 0.15 * _nhits, 0.85), 2)
|
|
1273
|
+
relevant_files.append(RelevantFile(
|
|
1274
|
+
path=_cp,
|
|
1275
|
+
role="symptom_match",
|
|
1276
|
+
score=_ci_score,
|
|
1277
|
+
reason=f"commit message matches symptom ({_nhits} commit{'s' if _nhits > 1 else ''})",
|
|
1278
|
+
why=f"symptom commit-index: {', '.join(symptom_keywords)}",
|
|
1279
|
+
))
|
|
1280
|
+
_existing_paths.add(_cp)
|
|
1281
|
+
|
|
1282
|
+
# Pass 4: inject files whose path matches symptom keywords
|
|
1224
1283
|
for _p in all_paths:
|
|
1225
1284
|
if _p in _existing_paths:
|
|
1226
1285
|
continue
|
|
@@ -1241,72 +1300,134 @@ class TaskContextBuilder:
|
|
|
1241
1300
|
why=f"symptom injection: {', '.join(_matching_kws)}",
|
|
1242
1301
|
))
|
|
1243
1302
|
_existing_paths.add(_p)
|
|
1303
|
+
_sx_direct_path.append(_p)
|
|
1244
1304
|
|
|
1245
|
-
#
|
|
1246
|
-
|
|
1247
|
-
path_lower = rf.path.lower()
|
|
1248
|
-
return rf.score + 0.2 * sum(1.0 for kw in symptom_keywords if kw in path_lower)
|
|
1249
|
-
relevant_files = sorted(relevant_files, key=lambda rf: -_symptom_score(rf))
|
|
1250
|
-
|
|
1251
|
-
# Content scan boost: read file body for symptom keywords
|
|
1305
|
+
# Pass 5+6 combined: single file read per candidate.
|
|
1306
|
+
# Limit content scan to top 80 candidates by current score to bound I/O.
|
|
1252
1307
|
_src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
|
|
1253
|
-
|
|
1254
|
-
|
|
1308
|
+
_frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
|
|
1309
|
+
_backend_terms_set: list[str] = []
|
|
1310
|
+
if _frontend_kws:
|
|
1311
|
+
_bt: list[str] = []
|
|
1312
|
+
for _fkw in _frontend_kws:
|
|
1313
|
+
_bt.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
|
|
1314
|
+
_backend_terms_set = list(dict.fromkeys(_bt))
|
|
1315
|
+
|
|
1316
|
+
# Sort before content scan so top candidates get read first
|
|
1317
|
+
relevant_files = sorted(relevant_files, key=lambda rf: -rf.score)
|
|
1318
|
+
_CONTENT_SCAN_LIMIT = 80
|
|
1319
|
+
_scan_candidates = relevant_files[:_CONTENT_SCAN_LIMIT]
|
|
1320
|
+
_no_scan_candidates = relevant_files[_CONTENT_SCAN_LIMIT:]
|
|
1321
|
+
|
|
1322
|
+
_boosted: list[RelevantFile] = []
|
|
1323
|
+
for _rf in _scan_candidates:
|
|
1255
1324
|
_extra = 0.0
|
|
1325
|
+
_extra_syn = 0.0
|
|
1326
|
+
_reasons: list[str] = []
|
|
1327
|
+
_p_lower = _rf.path.lower()
|
|
1328
|
+
|
|
1329
|
+
# Commit message boost: +0.25/commit, cap +0.40
|
|
1330
|
+
_c_hits = _commit_file_hits.get(_rf.path, 0)
|
|
1331
|
+
if _c_hits:
|
|
1332
|
+
_cb = min(0.40, _c_hits * 0.25)
|
|
1333
|
+
_extra += _cb
|
|
1334
|
+
_reasons.append(f"commit-msg symptom ×{_c_hits} (+{_cb:.2f})")
|
|
1335
|
+
_sx_boosts.append({"type": "commit_message", "value": round(_cb, 3), "evidence": _rf.path})
|
|
1336
|
+
|
|
1337
|
+
# Code note boost: +0.20/note, cap +0.30
|
|
1338
|
+
_n_hits = _note_matched_paths.get(_rf.path, 0)
|
|
1339
|
+
if _n_hits:
|
|
1340
|
+
_nb = min(0.30, _n_hits * 0.20)
|
|
1341
|
+
_extra += _nb
|
|
1342
|
+
_reasons.append(f"note-match symptom ×{_n_hits} (+{_nb:.2f})")
|
|
1343
|
+
_sx_boosts.append({"type": "code_note", "value": round(_nb, 3), "evidence": _rf.path})
|
|
1344
|
+
|
|
1345
|
+
# Path keyword boost for pre-existing candidates
|
|
1346
|
+
_path_kws = [kw for kw in symptom_keywords if kw in _p_lower]
|
|
1347
|
+
if _path_kws and _rf.role != "symptom_match":
|
|
1348
|
+
_pb = 0.20 * len(_path_kws)
|
|
1349
|
+
_extra += _pb
|
|
1350
|
+
_reasons.append(f"path-kw symptom ({', '.join(_path_kws)}) (+{_pb:.2f})")
|
|
1351
|
+
_sx_boosts.append({"type": "path_match", "value": round(_pb, 3), "evidence": _rf.path})
|
|
1352
|
+
|
|
1353
|
+
# Single file read — covers both content scan and synonym scan
|
|
1354
|
+
_body_lower = ""
|
|
1256
1355
|
if Path(_rf.path).suffix.lower() in _src_exts:
|
|
1257
1356
|
try:
|
|
1258
|
-
|
|
1357
|
+
_body_lower = (self.root / _rf.path).read_text(
|
|
1259
1358
|
encoding="utf-8", errors="replace"
|
|
1260
|
-
).
|
|
1261
|
-
_body = "\n".join(_lines).lower()
|
|
1262
|
-
_hits = sum(_body.count(kw) for kw in symptom_keywords)
|
|
1263
|
-
_extra = min(0.30, _hits * 0.02)
|
|
1359
|
+
)[:12000].lower() # ~300 lines avg
|
|
1264
1360
|
except OSError:
|
|
1265
1361
|
pass
|
|
1266
|
-
|
|
1362
|
+
|
|
1363
|
+
# Content scan boost: +0.05/hit, cap +0.50
|
|
1364
|
+
if _body_lower:
|
|
1365
|
+
_hits = len(_kw_re.findall(_body_lower))
|
|
1366
|
+
_content_b = min(0.50, _hits * 0.05)
|
|
1367
|
+
if _content_b > 0:
|
|
1368
|
+
_extra += _content_b
|
|
1369
|
+
_reasons.append(f"content-match symptom ×{_hits} (+{_content_b:.2f})")
|
|
1370
|
+
_sx_boosts.append({"type": "content_match", "value": round(_content_b, 3), "evidence": _rf.path})
|
|
1371
|
+
_sx_content.append(_rf.path)
|
|
1372
|
+
|
|
1373
|
+
# Synonym scan (Pass 6): only apply when file has prior non-synonym
|
|
1374
|
+
# evidence (commit hit, note, path, or content) — prevents boosting
|
|
1375
|
+
# arbitrary interceptors/configs with no other signal.
|
|
1376
|
+
if _backend_terms_set and _body_lower:
|
|
1377
|
+
_prior_boost = _extra # boost accumulated before synonym
|
|
1378
|
+
if _prior_boost >= 0.10: # min threshold: must have real prior signal
|
|
1379
|
+
_hits_syn = sum(_body_lower.count(t) for t in _backend_terms_set)
|
|
1380
|
+
_extra_syn = min(0.20, _hits_syn * 0.02)
|
|
1381
|
+
if _extra_syn > 0:
|
|
1382
|
+
_sx_synonyms.append(_rf.path)
|
|
1383
|
+
_sx_boosts.append({"type": "synonym_match", "value": round(_extra_syn, 3), "evidence": _rf.path})
|
|
1384
|
+
|
|
1385
|
+
_total_extra = _extra + _extra_syn
|
|
1386
|
+
_new_reason = _rf.reason
|
|
1387
|
+
if _reasons:
|
|
1388
|
+
_syn_suffix = f", synonym-match backend (+{_extra_syn:.2f})" if _extra_syn > 0 else ""
|
|
1389
|
+
_new_reason = _rf.reason + ", " + ", ".join(_reasons) + _syn_suffix
|
|
1390
|
+
elif _extra_syn > 0:
|
|
1391
|
+
_new_reason = _rf.reason + f", synonym-match backend (+{_extra_syn:.2f})"
|
|
1392
|
+
|
|
1393
|
+
_boosted.append(RelevantFile(
|
|
1267
1394
|
path=_rf.path,
|
|
1268
1395
|
role=_rf.role,
|
|
1269
|
-
score=round(min(_rf.score +
|
|
1270
|
-
reason=
|
|
1396
|
+
score=round(min(_rf.score + _total_extra, 1.0), 2),
|
|
1397
|
+
reason=_new_reason,
|
|
1271
1398
|
why=_rf.why,
|
|
1272
1399
|
))
|
|
1273
|
-
relevant_files = sorted(_content_boosted, key=lambda rf: -rf.score)
|
|
1274
1400
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if _frontend_kws:
|
|
1279
|
-
|
|
1280
|
-
for _fkw in _frontend_kws:
|
|
1281
|
-
_backend_terms.extend(_FRONTEND_SYMPTOM_MAP[_fkw])
|
|
1282
|
-
_backend_terms_set = list(dict.fromkeys(_backend_terms)) # dedup, preserve order
|
|
1283
|
-
_synonym_boosted: list[RelevantFile] = []
|
|
1284
|
-
for _rf in relevant_files:
|
|
1285
|
-
_extra_syn = 0.0
|
|
1286
|
-
if Path(_rf.path).suffix.lower() in _src_exts:
|
|
1287
|
-
try:
|
|
1288
|
-
_lines_syn = (self.root / _rf.path).read_text(
|
|
1289
|
-
encoding="utf-8", errors="replace"
|
|
1290
|
-
).splitlines()[:300]
|
|
1291
|
-
_body_syn = "\n".join(_lines_syn).lower()
|
|
1292
|
-
_hits_syn = sum(_body_syn.count(t) for t in _backend_terms_set)
|
|
1293
|
-
_extra_syn = min(0.20, _hits_syn * 0.02)
|
|
1294
|
-
except OSError:
|
|
1295
|
-
pass
|
|
1296
|
-
_synonym_boosted.append(RelevantFile(
|
|
1297
|
-
path=_rf.path,
|
|
1298
|
-
role=_rf.role,
|
|
1299
|
-
score=round(min(_rf.score + _extra_syn, 1.0), 2),
|
|
1300
|
-
reason=_rf.reason + (f", synonym-match backend (+{_extra_syn:.2f})" if _extra_syn > 0 else ""),
|
|
1301
|
-
why=_rf.why,
|
|
1302
|
-
))
|
|
1303
|
-
relevant_files = sorted(_synonym_boosted, key=lambda rf: -rf.score)
|
|
1304
|
-
_synonym_note = (
|
|
1401
|
+
relevant_files = sorted(_boosted + _no_scan_candidates, key=lambda rf: -rf.score)
|
|
1402
|
+
|
|
1403
|
+
# Synonym note (only when synonyms actually fired)
|
|
1404
|
+
if _frontend_kws and _sx_synonyms:
|
|
1405
|
+
symptom_note = (
|
|
1305
1406
|
f"Frontend concept detected ({', '.join(_frontend_kws)}). "
|
|
1306
|
-
"Backend service-layer
|
|
1407
|
+
"Backend service-layer files boosted by synonym match "
|
|
1307
1408
|
"[INFERRED (LOW CONFIDENCE) — pattern heuristic, not structural proof]."
|
|
1308
1409
|
)
|
|
1309
|
-
|
|
1410
|
+
|
|
1411
|
+
# Confidence: based on richest signal type present
|
|
1412
|
+
if _commit_file_hits:
|
|
1413
|
+
_sx_confidence = "HIGH"
|
|
1414
|
+
elif _sx_direct_path or _sx_content:
|
|
1415
|
+
_sx_confidence = "MEDIUM"
|
|
1416
|
+
else:
|
|
1417
|
+
_sx_confidence = "LOW"
|
|
1418
|
+
|
|
1419
|
+
symptom_explain = {
|
|
1420
|
+
"keywords": symptom_keywords,
|
|
1421
|
+
"confidence": _sx_confidence,
|
|
1422
|
+
"direct_path_matches": _sx_direct_path[:10],
|
|
1423
|
+
"content_matches": _sx_content[:10],
|
|
1424
|
+
"commit_matches": _sx_commits[:10],
|
|
1425
|
+
"synonym_matches": _sx_synonyms[:10],
|
|
1426
|
+
"boosts": _sx_boosts[:30],
|
|
1427
|
+
"final_boost": round(
|
|
1428
|
+
sum(b["value"] for b in _sx_boosts), 3
|
|
1429
|
+
),
|
|
1430
|
+
}
|
|
1310
1431
|
|
|
1311
1432
|
# ── 7. Test gaps (generate-tests only) ────────────────────────────
|
|
1312
1433
|
test_gaps: list[str] = []
|
|
@@ -1405,6 +1526,7 @@ class TaskContextBuilder:
|
|
|
1405
1526
|
symptom=symptom if task_name == "fix-bug" and symptom else None,
|
|
1406
1527
|
related_notes=related_notes,
|
|
1407
1528
|
symptom_note=symptom_note,
|
|
1529
|
+
symptom_explain=symptom_explain if task_name == "fix-bug" else None,
|
|
1408
1530
|
impact_summary=_delta_impact_summary,
|
|
1409
1531
|
affected_modules=_delta_affected_modules,
|
|
1410
1532
|
risk_areas=_delta_risk_areas,
|
sourcecode/repository_ir.py
CHANGED
|
@@ -997,6 +997,8 @@ def _get_git_old_content(git_root: Path, rel_path: str, since: str) -> Optional[
|
|
|
997
997
|
cwd=str(git_root),
|
|
998
998
|
capture_output=True,
|
|
999
999
|
text=True,
|
|
1000
|
+
encoding="utf-8",
|
|
1001
|
+
errors="replace",
|
|
1000
1002
|
timeout=5,
|
|
1001
1003
|
)
|
|
1002
1004
|
if result.returncode == 0:
|
|
@@ -1542,12 +1544,99 @@ def build_repo_ir(
|
|
|
1542
1544
|
return _assemble(all_symbols, unique_relations, all_changed, spring_summary, all_route_diffs_sorted)
|
|
1543
1545
|
|
|
1544
1546
|
|
|
1547
|
+
# ---------------------------------------------------------------------------
|
|
1548
|
+
# Output size limits
|
|
1549
|
+
# ---------------------------------------------------------------------------
|
|
1550
|
+
|
|
1551
|
+
# Vendor/generated dirs to skip when finding Java files and in git analysis.
|
|
1552
|
+
_VENDOR_DIRS: frozenset[str] = frozenset({
|
|
1553
|
+
"vendor", "node_modules", "dist", "target", "build",
|
|
1554
|
+
".gradle", ".mvn", "generated", "generated-sources",
|
|
1555
|
+
"generated-resources",
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def apply_ir_size_limits(
|
|
1560
|
+
ir: dict,
|
|
1561
|
+
*,
|
|
1562
|
+
max_nodes: Optional[int] = None,
|
|
1563
|
+
max_edges: Optional[int] = None,
|
|
1564
|
+
summary_only: bool = False,
|
|
1565
|
+
) -> dict:
|
|
1566
|
+
"""Apply size limits to a repo-ir output dict. Non-destructive: returns new dict.
|
|
1567
|
+
|
|
1568
|
+
Node ordering: top-ranked (by impact score) nodes are kept first.
|
|
1569
|
+
Edge priority: edges connecting two kept nodes over cross-boundary edges.
|
|
1570
|
+
"""
|
|
1571
|
+
if not max_nodes and not max_edges and not summary_only:
|
|
1572
|
+
return ir
|
|
1573
|
+
|
|
1574
|
+
out = dict(ir)
|
|
1575
|
+
graph = ir.get("graph") or {}
|
|
1576
|
+
nodes: list[dict] = list(graph.get("nodes") or [])
|
|
1577
|
+
edges: list[dict] = list(graph.get("edges") or [])
|
|
1578
|
+
ranked: list[dict] = list((ir.get("impact") or {}).get("ranked_nodes") or [])
|
|
1579
|
+
analysis: dict = ir.get("analysis") or {}
|
|
1580
|
+
|
|
1581
|
+
if summary_only:
|
|
1582
|
+
n_nodes, n_edges = len(nodes), len(edges)
|
|
1583
|
+
out["graph"] = {
|
|
1584
|
+
"nodes": [],
|
|
1585
|
+
"edges": [],
|
|
1586
|
+
"_omitted": (
|
|
1587
|
+
f"{n_nodes} nodes and {n_edges} edges omitted — "
|
|
1588
|
+
"remove --summary-only to restore full graph"
|
|
1589
|
+
),
|
|
1590
|
+
}
|
|
1591
|
+
out["reverse_graph"] = {}
|
|
1592
|
+
out["impact"] = {
|
|
1593
|
+
"global_score": (ir.get("impact") or {}).get("global_score", 0),
|
|
1594
|
+
"ranked_nodes": ranked[:20],
|
|
1595
|
+
}
|
|
1596
|
+
out["analysis"] = {
|
|
1597
|
+
"changed_entities": analysis.get("changed_entities") or [],
|
|
1598
|
+
"impacted_entities": (analysis.get("impacted_entities") or [])[:20],
|
|
1599
|
+
"isolated_changes": analysis.get("isolated_changes") or [],
|
|
1600
|
+
"validated_changes": analysis.get("validated_changes") or [],
|
|
1601
|
+
}
|
|
1602
|
+
return out
|
|
1603
|
+
|
|
1604
|
+
# Build score map from ranked_nodes (already sorted -score, fqn)
|
|
1605
|
+
score_map: dict[str, float] = {rn["entity"]: rn["score"] for rn in ranked}
|
|
1606
|
+
kept_fqns: Optional[set[str]] = None
|
|
1607
|
+
|
|
1608
|
+
if max_nodes is not None and len(nodes) > max_nodes:
|
|
1609
|
+
nodes_sorted = sorted(
|
|
1610
|
+
nodes,
|
|
1611
|
+
key=lambda n: (-score_map.get(n["fqn"], 0.0), n["fqn"]),
|
|
1612
|
+
)
|
|
1613
|
+
nodes = nodes_sorted[:max_nodes]
|
|
1614
|
+
kept_fqns = {n["fqn"] for n in nodes}
|
|
1615
|
+
ranked = [rn for rn in ranked if rn["entity"] in kept_fqns]
|
|
1616
|
+
|
|
1617
|
+
if kept_fqns is not None or max_edges is not None:
|
|
1618
|
+
if kept_fqns is not None:
|
|
1619
|
+
# Priority: edges where both endpoints are kept nodes
|
|
1620
|
+
priority = [e for e in edges if e["from"] in kept_fqns and e["to"] in kept_fqns]
|
|
1621
|
+
rest = [e for e in edges if not (e["from"] in kept_fqns and e["to"] in kept_fqns)]
|
|
1622
|
+
edges = priority + rest
|
|
1623
|
+
if max_edges is not None:
|
|
1624
|
+
edges = edges[:max_edges]
|
|
1625
|
+
|
|
1626
|
+
out["graph"] = {"nodes": nodes, "edges": edges}
|
|
1627
|
+
out["impact"] = {
|
|
1628
|
+
"global_score": (ir.get("impact") or {}).get("global_score", 0),
|
|
1629
|
+
"ranked_nodes": ranked,
|
|
1630
|
+
}
|
|
1631
|
+
return out
|
|
1632
|
+
|
|
1633
|
+
|
|
1545
1634
|
# ---------------------------------------------------------------------------
|
|
1546
1635
|
# Convenience: find Java files in a repo
|
|
1547
1636
|
# ---------------------------------------------------------------------------
|
|
1548
1637
|
|
|
1549
1638
|
def find_java_files(root: Path, *, max_files: int = 500) -> list[str]:
|
|
1550
|
-
"""Return relative paths to Java files under root, excluding test dirs."""
|
|
1639
|
+
"""Return relative paths to Java files under root, excluding test dirs and vendor."""
|
|
1551
1640
|
results: list[str] = []
|
|
1552
1641
|
for p in sorted(root.rglob("*.java")):
|
|
1553
1642
|
if len(results) >= max_files:
|
|
@@ -1556,7 +1645,12 @@ def find_java_files(root: Path, *, max_files: int = 500) -> list[str]:
|
|
|
1556
1645
|
rel = str(p.relative_to(root)).replace("\\", "/")
|
|
1557
1646
|
except ValueError:
|
|
1558
1647
|
continue
|
|
1648
|
+
parts = rel.split("/")
|
|
1649
|
+
# Skip test dirs
|
|
1559
1650
|
if "/test/" in rel or "/tests/" in rel or rel.startswith("test/"):
|
|
1560
1651
|
continue
|
|
1652
|
+
# Skip vendor/generated/build dirs
|
|
1653
|
+
if any(part in _VENDOR_DIRS for part in parts[:-1]):
|
|
1654
|
+
continue
|
|
1561
1655
|
results.append(rel)
|
|
1562
1656
|
return results
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.30.
|
|
3
|
+
Version: 1.30.19
|
|
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.19
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
---
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=INKPCnhh5ICq3yZ7MYdKW9iFC8jUinfGzMfJk94LSC0,104
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=RTNExwWPXzjgLaRueT7UuxkPj5ZEToWjGbx1j0LSZ9E,10250
|
|
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
6
|
sourcecode/classifier.py,sha256=pYve2J1LqtYssU3lYLMDz18PT-CjN5c18QYE7R_IG1Q,7507
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=an8qIJRnWekBUSyaLod6pD7VyU56V38EotFHDnXlNs8,87996
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
|
|
9
9
|
sourcecode/confidence_analyzer.py,sha256=xw_Jv8pAd0wd8t2vvQlorw8Ih0rSF3YCoFS8K-_4aXg,15762
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
11
11
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
12
12
|
sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
|
|
13
|
-
sourcecode/contract_pipeline.py,sha256=
|
|
13
|
+
sourcecode/contract_pipeline.py,sha256=w18t_MdbrkIeLcCW-VMQYeb9hlWenAOB-NMiME_Fo-Y,27652
|
|
14
14
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
15
15
|
sourcecode/dependency_analyzer.py,sha256=p4ljXhkcGBbFlhaZuPrsjOVjDXaKLTg0Gor2p4qFPP0,56208
|
|
16
16
|
sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24331
|
|
@@ -18,17 +18,17 @@ sourcecode/entrypoint_classifier.py,sha256=gvKgl0f5T8ol1r4JMmkeqGHuZTfZJiOwFOWdc
|
|
|
18
18
|
sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
|
|
19
19
|
sourcecode/file_classifier.py,sha256=48ly5Z6exkzBy8lNy1AkdP4-oJqIA1zT3LZfffuTyDo,11572
|
|
20
20
|
sourcecode/flow_analyzer.py,sha256=dSiuY4w49k29jW_EPXUOND9B5uVbuCA7kjnuHi-pIWA,28781
|
|
21
|
-
sourcecode/git_analyzer.py,sha256=
|
|
21
|
+
sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13177
|
|
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=k5pCIP6iBNwy5UYPxu47CAq-62j4E9QZZOPyL3trH80,14799
|
|
25
|
-
sourcecode/prepare_context.py,sha256=
|
|
25
|
+
sourcecode/prepare_context.py,sha256=8-xithuC0-HG12tF6qWW7-3vNtZET9UC2NRbuJkaFnE,158731
|
|
26
26
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
27
27
|
sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
|
|
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=e9TzNbfo_2Cyh3aASFZBq--rPjBMIYMuw5smIWJ5hx8,61099
|
|
32
32
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
33
33
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
34
34
|
sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
|
|
@@ -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.19.dist-info/METADATA,sha256=MNkVBrHuR2xR9onBA950wqcFlHu13VRcjAjRQKsqlAo,28956
|
|
68
|
+
sourcecode-1.30.19.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
69
|
+
sourcecode-1.30.19.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
70
|
+
sourcecode-1.30.19.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
71
|
+
sourcecode-1.30.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|