sourcecode 1.30.13__py3-none-any.whl → 1.30.14__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/flow_analyzer.py +11 -11
- sourcecode/prepare_context.py +53 -60
- sourcecode/repository_ir.py +498 -24
- {sourcecode-1.30.13.dist-info → sourcecode-1.30.14.dist-info}/METADATA +3 -3
- {sourcecode-1.30.13.dist-info → sourcecode-1.30.14.dist-info}/RECORD +9 -9
- {sourcecode-1.30.13.dist-info → sourcecode-1.30.14.dist-info}/WHEEL +0 -0
- {sourcecode-1.30.13.dist-info → sourcecode-1.30.14.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.30.13.dist-info → sourcecode-1.30.14.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/flow_analyzer.py
CHANGED
|
@@ -381,27 +381,27 @@ def _impact_descriptions(
|
|
|
381
381
|
|
|
382
382
|
if changed_type in _REPO_ARTIFACT_TYPES:
|
|
383
383
|
items.append(_impact_item(
|
|
384
|
-
f"{domain} persistence
|
|
385
|
-
f"{changed_class} classified as
|
|
384
|
+
f"{domain} persistence layer modified" if domain else "persistence layer modified",
|
|
385
|
+
f"{changed_class} classified as {changed_type} (artifact_type evidence)",
|
|
386
386
|
certainty,
|
|
387
387
|
))
|
|
388
388
|
elif changed_type in _SERVICE_ARTIFACT_TYPES:
|
|
389
389
|
if end_state == "DB write":
|
|
390
390
|
items.append(_impact_item(
|
|
391
|
-
f"{domain}
|
|
392
|
-
f"{changed_class} delegates to repository with DB write",
|
|
391
|
+
f"{domain} service layer: DB write in call path" if domain else "service layer: DB write in call path",
|
|
392
|
+
f"{changed_class} delegates to repository with DB write (code evidence)",
|
|
393
393
|
certainty,
|
|
394
394
|
))
|
|
395
395
|
else:
|
|
396
396
|
items.append(_impact_item(
|
|
397
|
-
f"{domain}
|
|
398
|
-
f"{changed_class} is a service
|
|
397
|
+
f"{domain} service layer modified" if domain else "service layer modified",
|
|
398
|
+
f"{changed_class} is a service (artifact_type={changed_type})",
|
|
399
399
|
certainty,
|
|
400
400
|
))
|
|
401
401
|
else:
|
|
402
402
|
items.append(_impact_item(
|
|
403
|
-
f"{domain}
|
|
404
|
-
f"{changed_class}
|
|
403
|
+
f"{domain} component modified" if domain else "component modified",
|
|
404
|
+
f"{changed_class} found in traced call path (artifact_type={changed_type})",
|
|
405
405
|
certainty,
|
|
406
406
|
))
|
|
407
407
|
|
|
@@ -440,14 +440,14 @@ def _impact_descriptions_for_controller(
|
|
|
440
440
|
domain = d
|
|
441
441
|
break
|
|
442
442
|
items.append(_impact_item(
|
|
443
|
-
f"{domain} persistence
|
|
444
|
-
"repository with DB write detected in path",
|
|
443
|
+
f"{domain} persistence layer: repository with DB write in call path" if domain else "persistence layer: repository with DB write in call path",
|
|
444
|
+
"repository with DB write detected in traced call path (code evidence)",
|
|
445
445
|
certainty,
|
|
446
446
|
))
|
|
447
447
|
else:
|
|
448
448
|
items.append(_impact_item(
|
|
449
|
-
"request handler behavior may change",
|
|
450
449
|
"controller entry point modified",
|
|
450
|
+
"controller entry point is in changed files (direct diff evidence)",
|
|
451
451
|
certainty,
|
|
452
452
|
))
|
|
453
453
|
|
sourcecode/prepare_context.py
CHANGED
|
@@ -539,23 +539,23 @@ def _read_code_signal_evidence(root: Path, file_path: str, artifact_type: str) -
|
|
|
539
539
|
|
|
540
540
|
|
|
541
541
|
_ARTIFACT_CHANGE_EFFECT: dict[str, str] = {
|
|
542
|
-
"entrypoint": "
|
|
543
|
-
"controller": "
|
|
544
|
-
"service": "
|
|
545
|
-
"repository": "
|
|
546
|
-
"mapper": "
|
|
547
|
-
"security": "
|
|
548
|
-
"spring_config": "
|
|
549
|
-
"spring_profile": "
|
|
550
|
-
"config": "
|
|
551
|
-
"build_manifest": "
|
|
552
|
-
"db_migration": "
|
|
553
|
-
"domain_model": "
|
|
554
|
-
"dto": "
|
|
555
|
-
"test": "
|
|
556
|
-
"documentation": "
|
|
557
|
-
"ide_noise": "IDE/tooling artifact
|
|
558
|
-
"source": "
|
|
542
|
+
"entrypoint": "application entrypoint (framework bootstrap / CLI handler)",
|
|
543
|
+
"controller": "HTTP routing layer (request-to-handler mapping)",
|
|
544
|
+
"service": "business logic layer (@Service component)",
|
|
545
|
+
"repository": "data access layer (persistence queries / ORM)",
|
|
546
|
+
"mapper": "SQL-object mapping layer (MyBatis mapper / query template)",
|
|
547
|
+
"security": "security component (authentication / access control configuration)",
|
|
548
|
+
"spring_config": "Spring @Configuration class (bean definitions / datasource wiring)",
|
|
549
|
+
"spring_profile": "Spring profile override (environment-specific configuration)",
|
|
550
|
+
"config": "configuration file (application properties / environment values)",
|
|
551
|
+
"build_manifest": "build manifest (dependency and plugin configuration)",
|
|
552
|
+
"db_migration": "database schema migration (DDL change pending execution)",
|
|
553
|
+
"domain_model": "domain entity (@Entity / value object)",
|
|
554
|
+
"dto": "data transfer object (serialization contract)",
|
|
555
|
+
"test": "test file (no production code modified)",
|
|
556
|
+
"documentation": "documentation file (no runtime impact)",
|
|
557
|
+
"ide_noise": "IDE/tooling artifact (no application impact)",
|
|
558
|
+
"source": "application source (artifact role requires annotation inspection)",
|
|
559
559
|
}
|
|
560
560
|
|
|
561
561
|
# Maps frontend symptom keywords → backend terms likely to contain the root cause.
|
|
@@ -1037,7 +1037,7 @@ class TaskContextBuilder:
|
|
|
1037
1037
|
_pr_review_hotspots = sorted(
|
|
1038
1038
|
[f for f, cls in _pr_changed_cls.items()
|
|
1039
1039
|
if not cls["is_noise"] and cls["artifact_type"] != "build_manifest"],
|
|
1040
|
-
key=lambda f: (_delta_impact_score_per_file.get(f) or {}).get("
|
|
1040
|
+
key=lambda f: (_delta_impact_score_per_file.get(f) or {}).get("_rank_score", 0.0),
|
|
1041
1041
|
reverse=True,
|
|
1042
1042
|
)[:8]
|
|
1043
1043
|
|
|
@@ -1123,33 +1123,29 @@ class TaskContextBuilder:
|
|
|
1123
1123
|
_has_code_ev = any(e["type"] in ("annotation", "import") for e in _evidence)
|
|
1124
1124
|
_has_symbol_ev = any(e["type"] == "symbol" for e in _evidence)
|
|
1125
1125
|
_role_basis = "annotation" if _has_code_ev else ("symbol" if _has_symbol_ev else "git_diff_only")
|
|
1126
|
-
_cls_conf = _f_cls["confidence"]
|
|
1127
1126
|
_role_obj = {
|
|
1128
|
-
"type": "inferred",
|
|
1129
|
-
"confidence": "medium" if (_has_code_ev or (_has_symbol_ev and _cls_conf == "high")) else "low",
|
|
1130
1127
|
"basis": _role_basis,
|
|
1128
|
+
"has_annotation_signal": _has_code_ev,
|
|
1129
|
+
"has_symbol_signal": _has_symbol_ev,
|
|
1131
1130
|
}
|
|
1132
1131
|
|
|
1133
|
-
# File confidence: from structured impact entry
|
|
1134
|
-
_impact_entry = _delta_impact_score_per_file.get(_f) or {}
|
|
1135
|
-
_f_conf = _impact_entry.get("confidence", "low")
|
|
1136
1132
|
_diff_source = (
|
|
1137
1133
|
DiffSourceType.GIT_RANGE.value if _f in _committed_set
|
|
1138
1134
|
else DiffSourceType.WORKTREE_UNSTAGED.value if _f in _uncommitted_set
|
|
1139
1135
|
else "unknown"
|
|
1140
1136
|
)
|
|
1137
|
+
_impact_entry = _delta_impact_score_per_file.get(_f) or {}
|
|
1141
1138
|
_entry = {
|
|
1142
1139
|
"path": _f,
|
|
1143
1140
|
"diff_source": _diff_source,
|
|
1144
1141
|
"role": _role_obj,
|
|
1145
|
-
"confidence": _f_conf,
|
|
1146
1142
|
"artifact_type": _f_atype,
|
|
1147
1143
|
"evidence": _evidence,
|
|
1148
1144
|
"change_effect": {
|
|
1149
|
-
"statement": _ARTIFACT_CHANGE_EFFECT.get(_f_atype, "
|
|
1150
|
-
"
|
|
1151
|
-
"confidence": _role_obj["confidence"],
|
|
1145
|
+
"statement": _ARTIFACT_CHANGE_EFFECT.get(_f_atype, "application source (artifact role requires annotation inspection)"),
|
|
1146
|
+
"classification_method": _f_cls.get("confidence", "low"),
|
|
1152
1147
|
},
|
|
1148
|
+
"evidence_completeness": _impact_entry.get("evidence", {}),
|
|
1153
1149
|
}
|
|
1154
1150
|
_pr_runtime_changes.append(_entry)
|
|
1155
1151
|
# Split committed vs uncommitted — never merged
|
|
@@ -2336,17 +2332,10 @@ class TaskContextBuilder:
|
|
|
2336
2332
|
return "service" # annotation-confirmed — not "core_service" (overclaim)
|
|
2337
2333
|
return "unclassified" # no role claim without evidence
|
|
2338
2334
|
|
|
2339
|
-
def _score_to_confidence(score: float) -> str:
|
|
2340
|
-
if score >= 0.60:
|
|
2341
|
-
return "high"
|
|
2342
|
-
if score >= 0.40:
|
|
2343
|
-
return "medium"
|
|
2344
|
-
return "low"
|
|
2345
|
-
|
|
2346
2335
|
def _structured_why(path: str, atype: str, module: str, role: str, risk_areas: list[str], cls_confidence: str = "low") -> str:
|
|
2347
2336
|
area = _classify_impact_area(path, risk_areas, atype)
|
|
2348
2337
|
prop = _PROPAGATION_RISK.get(atype, "low") if atype != "source" else "unknown"
|
|
2349
|
-
effect = _CHANGE_EFFECT.get(atype, "
|
|
2338
|
+
effect = _CHANGE_EFFECT.get(atype, "application source (artifact role requires annotation inspection)")
|
|
2350
2339
|
parts = [f"artifact_type: {atype}", f"classification_confidence: {cls_confidence}"]
|
|
2351
2340
|
# Only emit role/area/propagation when there is confirmed evidence (not source/unclassified)
|
|
2352
2341
|
if role not in ("unclassified",):
|
|
@@ -2431,8 +2420,7 @@ class TaskContextBuilder:
|
|
|
2431
2420
|
role = _role_in_system(path, atype, in_ep)
|
|
2432
2421
|
cls_conf = cls.get("confidence", "low")
|
|
2433
2422
|
why_str = _structured_why(path, atype, module, role, cls["risk_areas"], cls_conf)
|
|
2434
|
-
|
|
2435
|
-
reason = f"changed since {ref_label} | artifact: {atype} | confidence: {confidence}"
|
|
2423
|
+
reason = f"changed since {ref_label} | artifact: {atype}"
|
|
2436
2424
|
|
|
2437
2425
|
relevant.append(RelevantFile(path=path, role=role, score=round(score, 2), reason=reason, why=why_str))
|
|
2438
2426
|
why[path] = why_str
|
|
@@ -2485,7 +2473,7 @@ class TaskContextBuilder:
|
|
|
2485
2473
|
why_str = _structured_why(path, rel_atype, _extract_ddd_domain(path), role, rel_cls["risk_areas"], rel_conf)
|
|
2486
2474
|
why_str += f" | pulled_by: type-aware expansion from {ctx_type} '{ctx_val}'"
|
|
2487
2475
|
why_str += f" | triggered_by: {', '.join(triggers[:3])}"
|
|
2488
|
-
reason = f"expansion: {ctx_type} '{ctx_val}' | artifact: {rel_atype}
|
|
2476
|
+
reason = f"expansion: {ctx_type} '{ctx_val}' | artifact: {rel_atype}"
|
|
2489
2477
|
related.append((rel_score, path, RelevantFile(
|
|
2490
2478
|
path=path, role=role, score=rel_score, reason=reason, why=why_str
|
|
2491
2479
|
)))
|
|
@@ -2595,7 +2583,7 @@ class TaskContextBuilder:
|
|
|
2595
2583
|
_why_str += f" | pulled_by: hop-{_hop_num} import from {Path(_seed_path).name}"
|
|
2596
2584
|
_reason = (
|
|
2597
2585
|
f"hop-{_hop_num} import-dependent of {Path(_seed_path).name}"
|
|
2598
|
-
f" ({_seed_atype})
|
|
2586
|
+
f" ({_seed_atype})"
|
|
2599
2587
|
)
|
|
2600
2588
|
why[_dep_path] = _why_str
|
|
2601
2589
|
# Tests import production code but are not structural dependencies —
|
|
@@ -2670,18 +2658,18 @@ class TaskContextBuilder:
|
|
|
2670
2658
|
0.02
|
|
2671
2659
|
)
|
|
2672
2660
|
_raw_score = round(min(1.0, _base * 0.50 + _sec_w + _fw + _fanout + _ctw), 3)
|
|
2661
|
+
_caller_count = _downstream_count.get(_path, 0)
|
|
2673
2662
|
impact_score_per_file[_path] = {
|
|
2674
|
-
"
|
|
2675
|
-
"confidence": _score_to_confidence(_raw_score),
|
|
2676
|
-
"score_breakdown": {
|
|
2677
|
-
"artifact_base": round(_base * 0.50, 3),
|
|
2678
|
-
"security_weight": _sec_w,
|
|
2679
|
-
"framework_weight": _fw,
|
|
2680
|
-
"fanout_weight": round(_fanout, 3),
|
|
2681
|
-
"change_type_weight": _ctw,
|
|
2682
|
-
},
|
|
2663
|
+
"_rank_score": _raw_score, # internal ranking only — not a confidence claim
|
|
2683
2664
|
"change_types": sorted(_file_ctypes),
|
|
2684
2665
|
"diff_severity": _diff_sev,
|
|
2666
|
+
"evidence": {
|
|
2667
|
+
"has_reverse_edges": _caller_count > 0,
|
|
2668
|
+
"reverse_edge_count": _caller_count,
|
|
2669
|
+
"has_route_diff": _diff_sev == "api_change",
|
|
2670
|
+
"has_security_diff": _diff_sev == "security_change",
|
|
2671
|
+
"has_wiring_evidence": _atype in {"spring_config", "security"} and _cls.get("confidence") == "high",
|
|
2672
|
+
},
|
|
2685
2673
|
}
|
|
2686
2674
|
|
|
2687
2675
|
_CT_ORDER = ["security_change", "behavioral_change", "structural_change",
|
|
@@ -2714,24 +2702,29 @@ class TaskContextBuilder:
|
|
|
2714
2702
|
changed_subsystems.append(_subsys)
|
|
2715
2703
|
_seen_subsys.add(_subsys)
|
|
2716
2704
|
|
|
2717
|
-
# behavioral_changes:
|
|
2718
|
-
#
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2705
|
+
# behavioral_changes: only emitted when graph evidence exists (import edges found).
|
|
2706
|
+
# Without reverse dependency graph, we cannot make traceable structural claims.
|
|
2707
|
+
# Also gated on semantic diff evidence (api_change/security_change) per file.
|
|
2708
|
+
_has_graph_ev = bool(graph_edges)
|
|
2709
|
+
behavioral_changes: list[str] = (
|
|
2710
|
+
[
|
|
2711
|
+
f"{Path(_p).name}: {_CHANGE_EFFECT.get(_cls['artifact_type'], 'application source (artifact role requires annotation inspection)')} [diff_severity={diff_severities.get(_p)}]"
|
|
2712
|
+
for _p, _cls in classifications.items()
|
|
2713
|
+
if not _cls["is_noise"]
|
|
2714
|
+
and diff_severities.get(_p, "unknown") in ("api_change", "security_change")
|
|
2715
|
+
]
|
|
2716
|
+
if _has_graph_ev else []
|
|
2717
|
+
)
|
|
2725
2718
|
|
|
2726
2719
|
def _runtime_impact(tc: dict[str, int]) -> list[str]:
|
|
2727
2720
|
_ri: list[str] = []
|
|
2728
2721
|
# Only emit claims that are evidence-backed (annotation-confirmed roles)
|
|
2729
2722
|
if "entrypoint" in tc:
|
|
2730
|
-
_ri.append("Entrypoint-classified file modified —
|
|
2723
|
+
_ri.append("Entrypoint-classified file modified — restart required before deploying to production")
|
|
2731
2724
|
if "spring_config" in tc:
|
|
2732
|
-
_ri.append("Spring
|
|
2725
|
+
_ri.append("Spring @Configuration-classified file modified — bean context rebuild required on restart")
|
|
2733
2726
|
if "security" in tc:
|
|
2734
|
-
_ri.append("Security-classified file modified —
|
|
2727
|
+
_ri.append("Security-classified file modified — inspect authentication and access control wiring")
|
|
2735
2728
|
if "db_migration" in tc:
|
|
2736
2729
|
_ri.append("Database schema migration pending — execute before deploying application")
|
|
2737
2730
|
# service transactional claim: only if @Service annotation confirmed (not source/unclassified)
|
sourcecode/repository_ir.py
CHANGED
|
@@ -28,12 +28,21 @@ from typing import Optional
|
|
|
28
28
|
@dataclass
|
|
29
29
|
class SymbolRecord:
|
|
30
30
|
symbol: str # fully qualified: pkg.Class | pkg.Class#method | pkg.Class.field
|
|
31
|
-
type: str # class | interface | method | field
|
|
31
|
+
type: str # class | interface | method | field (backward-compat values)
|
|
32
32
|
modifiers: list[str] = field(default_factory=list)
|
|
33
33
|
annotations: list[str] = field(default_factory=list)
|
|
34
34
|
imports_used: list[str] = field(default_factory=list)
|
|
35
35
|
declaring_file: str = ""
|
|
36
36
|
confidence: str = "medium" # high | medium | low
|
|
37
|
+
# Stable identity contract — populated by _extract_symbols
|
|
38
|
+
stable_id: str = "" # deterministic across formatting/body changes
|
|
39
|
+
symbol_kind: str = "" # class|interface|enum|annotation|method|constructor|field|endpoint|bean
|
|
40
|
+
canonical_name: str = "" # pkg.Class#method(Type1,Type2) — human-readable
|
|
41
|
+
source_file: str = "" # alias for declaring_file (IR output contract)
|
|
42
|
+
signature: str = "" # (Type1,Type2)->ReturnType for methods; type for fields
|
|
43
|
+
param_types: list[str] = field(default_factory=list)
|
|
44
|
+
return_type: str = ""
|
|
45
|
+
annotation_values: dict[str, str] = field(default_factory=dict) # ann_name → raw args string
|
|
37
46
|
|
|
38
47
|
|
|
39
48
|
@dataclass
|
|
@@ -95,6 +104,7 @@ class EvidenceBundle:
|
|
|
95
104
|
_PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
|
|
96
105
|
_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
|
|
97
106
|
_ANN_RE = re.compile(r'^(@[\w.]+)')
|
|
107
|
+
_ANN_WITH_ARGS_RE = re.compile(r'^(@[\w.]+)\s*(?:\(([^)]*)\))?')
|
|
98
108
|
|
|
99
109
|
_CLASS_DECL_RE = re.compile(
|
|
100
110
|
r'(?:^|(?<=\s))'
|
|
@@ -111,10 +121,16 @@ _METHOD_DECL_RE = re.compile(
|
|
|
111
121
|
r'^(?P<modifiers>(?:(?:public|private|protected|static|final|synchronized'
|
|
112
122
|
r'|abstract|default|native|strictfp|override)\s+)*)'
|
|
113
123
|
r'(?:<[\w,\s?]+>\s+)?'
|
|
114
|
-
r'(
|
|
124
|
+
r'(?P<return_type>(?:void|boolean|byte|char|short|int|long|float|double|String|[\w.<>\[\]?,]+)\s+)'
|
|
115
125
|
r'(?P<name>[a-z_]\w*)\s*\(',
|
|
116
126
|
)
|
|
117
127
|
|
|
128
|
+
_CONSTRUCTOR_DECL_RE = re.compile(
|
|
129
|
+
r'^(?P<modifiers>(?:(?:public|private|protected)\s+)*)'
|
|
130
|
+
r'(?P<name>[A-Z]\w*)\s*\('
|
|
131
|
+
r'(?P<params>[^)]*)',
|
|
132
|
+
)
|
|
133
|
+
|
|
118
134
|
_FIELD_DECL_RE = re.compile(
|
|
119
135
|
r'^(?P<modifiers>(?:(?:private|protected|public|static|final|volatile|transient)\s+)*)'
|
|
120
136
|
r'(?P<type>[\w<>.,\[\]? ]+?)\s+'
|
|
@@ -125,6 +141,11 @@ _REQUEST_MAPPING_RE = re.compile(
|
|
|
125
141
|
r'@(?:Request|Get|Post|Put|Delete|Patch)Mapping\s*\(\s*(?:value\s*=\s*)?["\']([^"\']+)["\']'
|
|
126
142
|
)
|
|
127
143
|
|
|
144
|
+
_ENDPOINT_ANNOTATIONS: frozenset[str] = frozenset({
|
|
145
|
+
"@GetMapping", "@PostMapping", "@PutMapping", "@DeleteMapping",
|
|
146
|
+
"@PatchMapping", "@RequestMapping",
|
|
147
|
+
})
|
|
148
|
+
|
|
128
149
|
_MODIFIER_WORDS: frozenset[str] = frozenset({
|
|
129
150
|
"public", "private", "protected", "static", "final", "abstract",
|
|
130
151
|
"synchronized", "native", "strictfp", "transient", "volatile", "default",
|
|
@@ -159,6 +180,14 @@ _SPRING_OTHER: frozenset[str] = frozenset({
|
|
|
159
180
|
"@SpringBootApplication", "@EnableAutoConfiguration",
|
|
160
181
|
})
|
|
161
182
|
|
|
183
|
+
_HTTP_METHOD_MAP: dict[str, str] = {
|
|
184
|
+
"@GetMapping": "GET",
|
|
185
|
+
"@PostMapping": "POST",
|
|
186
|
+
"@PutMapping": "PUT",
|
|
187
|
+
"@DeleteMapping": "DELETE",
|
|
188
|
+
"@PatchMapping": "PATCH",
|
|
189
|
+
}
|
|
190
|
+
|
|
162
191
|
# ---------------------------------------------------------------------------
|
|
163
192
|
# Phase 5 constants
|
|
164
193
|
# ---------------------------------------------------------------------------
|
|
@@ -174,6 +203,7 @@ _IR_WEIGHT_DEFAULT: float = 0.3
|
|
|
174
203
|
# diff_intensity: method change=1.0, field/annotation=0.6, formatting=0.1
|
|
175
204
|
_DIFF_INTENSITY_MAP: dict[str, float] = {
|
|
176
205
|
"signature_change": 1.0,
|
|
206
|
+
"route_surface_change": 1.0,
|
|
177
207
|
"structural_change": 0.6,
|
|
178
208
|
"annotation_change": 0.6,
|
|
179
209
|
"unknown": 0.1,
|
|
@@ -182,6 +212,96 @@ _DIFF_INTENSITY_MAP: dict[str, float] = {
|
|
|
182
212
|
_PROPAGATION_DECAY: float = 0.5
|
|
183
213
|
_BFS_MAX_DEPTH: int = 3
|
|
184
214
|
|
|
215
|
+
# Regex to strip leading annotations from a single parameter (e.g. @NotNull @Valid String name)
|
|
216
|
+
_ANN_PREFIX_RE = re.compile(r'^(?:@\w+\s*(?:\([^)]*\))?\s*)+')
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Stable ID helpers
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
def _normalize_type_name(raw: str) -> str:
|
|
224
|
+
"""Strip annotations, final modifier, and param name; return only type.
|
|
225
|
+
|
|
226
|
+
"(Long id)" -> strip after parsing → "Long"
|
|
227
|
+
"@NotNull User user" → "User"
|
|
228
|
+
"List<String>" → "List<String>"
|
|
229
|
+
"""
|
|
230
|
+
raw = _ANN_PREFIX_RE.sub("", raw).strip()
|
|
231
|
+
raw = re.sub(r'\bfinal\s+', "", raw).strip()
|
|
232
|
+
# "Type name" → extract Type (rightmost word is the param name)
|
|
233
|
+
m = re.match(r'^([\w<>\[\].,? ]+?)\s+\w+$', raw)
|
|
234
|
+
if m:
|
|
235
|
+
return m.group(1).strip()
|
|
236
|
+
return raw.strip()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _parse_param_types(params_str: str) -> list[str]:
|
|
240
|
+
"""Parse "(Long id, @Valid String name)" → ["Long", "String"].
|
|
241
|
+
|
|
242
|
+
Handles simple param lists only (no nested generic commas).
|
|
243
|
+
For multi-line param lists callers receive an empty string → returns [].
|
|
244
|
+
"""
|
|
245
|
+
if not params_str or not params_str.strip():
|
|
246
|
+
return []
|
|
247
|
+
result: list[str] = []
|
|
248
|
+
depth = 0
|
|
249
|
+
current: list[str] = []
|
|
250
|
+
for ch in params_str:
|
|
251
|
+
if ch in ("<", "("):
|
|
252
|
+
depth += 1
|
|
253
|
+
current.append(ch)
|
|
254
|
+
elif ch in (">", ")"):
|
|
255
|
+
depth -= 1
|
|
256
|
+
current.append(ch)
|
|
257
|
+
elif ch == "," and depth == 0:
|
|
258
|
+
part = "".join(current).strip()
|
|
259
|
+
if part:
|
|
260
|
+
t = _normalize_type_name(part)
|
|
261
|
+
if t:
|
|
262
|
+
result.append(t)
|
|
263
|
+
current = []
|
|
264
|
+
else:
|
|
265
|
+
current.append(ch)
|
|
266
|
+
part = "".join(current).strip()
|
|
267
|
+
if part:
|
|
268
|
+
t = _normalize_type_name(part)
|
|
269
|
+
if t:
|
|
270
|
+
result.append(t)
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _normalize_return_type(raw: str) -> str:
|
|
275
|
+
"""Normalize return type string: strip whitespace, keep generics."""
|
|
276
|
+
return raw.strip()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _compute_stable_id(
|
|
280
|
+
package: str,
|
|
281
|
+
class_simple: str,
|
|
282
|
+
kind: str,
|
|
283
|
+
symbol_name: str,
|
|
284
|
+
param_types: Optional[list[str]] = None,
|
|
285
|
+
return_type: str = "",
|
|
286
|
+
) -> str:
|
|
287
|
+
"""Compute deterministic stable symbol identity.
|
|
288
|
+
|
|
289
|
+
Format: {package}:{class_simple}:{kind}:{symbol_name}[:{params}[:{return_type}]]
|
|
290
|
+
|
|
291
|
+
Survives: formatting, comments, body changes, imports, nearby movement.
|
|
292
|
+
Changes on: rename, param type change, class package move, kind change.
|
|
293
|
+
|
|
294
|
+
Never uses line numbers, byte offsets, or content hashes.
|
|
295
|
+
"""
|
|
296
|
+
pkg = package or "_"
|
|
297
|
+
cls = class_simple or "_"
|
|
298
|
+
parts = [pkg, cls, kind, symbol_name]
|
|
299
|
+
if param_types is not None:
|
|
300
|
+
parts.append(f"({','.join(param_types)})")
|
|
301
|
+
if return_type:
|
|
302
|
+
parts.append(_normalize_return_type(return_type))
|
|
303
|
+
return ":".join(parts)
|
|
304
|
+
|
|
185
305
|
|
|
186
306
|
# ---------------------------------------------------------------------------
|
|
187
307
|
# Helpers — Phases 1–4
|
|
@@ -262,6 +382,7 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
262
382
|
depth = 0
|
|
263
383
|
class_stack: list[tuple[str, int]] = []
|
|
264
384
|
pending_anns: list[str] = []
|
|
385
|
+
pending_ann_values: dict[str, str] = {}
|
|
265
386
|
in_block_comment = False
|
|
266
387
|
|
|
267
388
|
for line in source.splitlines():
|
|
@@ -281,11 +402,14 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
281
402
|
net = _count_net_braces(stripped)
|
|
282
403
|
|
|
283
404
|
if stripped.startswith("@"):
|
|
284
|
-
ann_m =
|
|
405
|
+
ann_m = _ANN_WITH_ARGS_RE.match(stripped)
|
|
285
406
|
if ann_m:
|
|
286
407
|
ann = ann_m.group(1)
|
|
408
|
+
ann_args = ann_m.group(2) or ""
|
|
287
409
|
if ann not in pending_anns:
|
|
288
410
|
pending_anns.append(ann)
|
|
411
|
+
if ann_args and ann in _ENDPOINT_ANNOTATIONS:
|
|
412
|
+
pending_ann_values[ann] = ann_args.strip()
|
|
289
413
|
depth += net
|
|
290
414
|
_pop_closed(class_stack, depth)
|
|
291
415
|
continue
|
|
@@ -312,6 +436,23 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
312
436
|
|
|
313
437
|
sym_type = "interface" if kind_kw == "interface" else "class"
|
|
314
438
|
|
|
439
|
+
# symbol_kind distinguishes enum/annotation from class/interface
|
|
440
|
+
if kind_kw == "enum":
|
|
441
|
+
sym_kind = "enum"
|
|
442
|
+
elif kind_kw == "@interface":
|
|
443
|
+
sym_kind = "annotation"
|
|
444
|
+
elif kind_kw == "interface":
|
|
445
|
+
sym_kind = "interface"
|
|
446
|
+
else:
|
|
447
|
+
sym_kind = "class"
|
|
448
|
+
|
|
449
|
+
_stable_id = _compute_stable_id(package, name, sym_kind, name)
|
|
450
|
+
_sig_parts = [kind_kw, name]
|
|
451
|
+
if extends_str:
|
|
452
|
+
_sig_parts.append(f"extends {extends_str}")
|
|
453
|
+
if implements_str:
|
|
454
|
+
_sig_parts.append(f"implements {implements_str}")
|
|
455
|
+
|
|
315
456
|
symbols.append(SymbolRecord(
|
|
316
457
|
symbol=fqn,
|
|
317
458
|
type=sym_type,
|
|
@@ -320,49 +461,129 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
320
461
|
imports_used=used,
|
|
321
462
|
declaring_file=rel_path,
|
|
322
463
|
confidence="high",
|
|
464
|
+
stable_id=_stable_id,
|
|
465
|
+
symbol_kind=sym_kind,
|
|
466
|
+
canonical_name=fqn,
|
|
467
|
+
source_file=rel_path,
|
|
468
|
+
signature=" ".join(_sig_parts),
|
|
469
|
+
annotation_values=dict(pending_ann_values),
|
|
323
470
|
))
|
|
324
471
|
|
|
325
472
|
class_stack.append((fqn, depth))
|
|
326
473
|
pending_anns = []
|
|
474
|
+
pending_ann_values = {}
|
|
327
475
|
depth += net
|
|
328
476
|
_pop_closed(class_stack, depth)
|
|
329
477
|
continue
|
|
330
478
|
|
|
331
479
|
if class_stack:
|
|
480
|
+
class_fqn = class_stack[-1][0]
|
|
481
|
+
# simple name of enclosing class (last segment, strip inner class paths)
|
|
482
|
+
_class_simple = class_fqn.split(".")[-1]
|
|
483
|
+
|
|
332
484
|
mth_m = _METHOD_DECL_RE.match(stripped)
|
|
333
485
|
if mth_m:
|
|
334
486
|
mname = mth_m.group("name")
|
|
335
487
|
if mname not in _JAVA_KEYWORDS:
|
|
336
|
-
class_fqn = class_stack[-1][0]
|
|
337
488
|
fqn = f"{class_fqn}#{mname}"
|
|
338
489
|
modifiers = _parse_modifier_str(mth_m.group("modifiers") or "")
|
|
339
490
|
used = _resolve_types_from_text(stripped, import_map)
|
|
340
491
|
conf = "high" if ("public" in modifiers or pending_anns) else "medium"
|
|
341
492
|
|
|
493
|
+
# Extract return type and params from matched line
|
|
494
|
+
_ret_raw = (mth_m.group("return_type") or "").strip()
|
|
495
|
+
_after_paren = stripped[mth_m.end():]
|
|
496
|
+
if ")" in _after_paren:
|
|
497
|
+
_params_str = _after_paren[:_after_paren.index(")")]
|
|
498
|
+
_param_types = _parse_param_types(_params_str)
|
|
499
|
+
else:
|
|
500
|
+
_param_types = [] # multi-line param list — deterministically empty
|
|
501
|
+
|
|
502
|
+
# Determine symbol_kind from annotations
|
|
503
|
+
_anns = sorted(set(pending_anns))
|
|
504
|
+
if "@Bean" in _anns:
|
|
505
|
+
_sym_kind = "bean"
|
|
506
|
+
elif _anns and any(a in _ENDPOINT_ANNOTATIONS for a in _anns):
|
|
507
|
+
_sym_kind = "endpoint"
|
|
508
|
+
else:
|
|
509
|
+
_sym_kind = "method"
|
|
510
|
+
|
|
511
|
+
_stable_id = _compute_stable_id(
|
|
512
|
+
package, _class_simple, _sym_kind, mname, _param_types, _ret_raw
|
|
513
|
+
)
|
|
514
|
+
_param_str = ",".join(_param_types)
|
|
515
|
+
_canonical = f"{class_fqn}#{mname}({_param_str})"
|
|
516
|
+
_signature = f"({_param_str})->{_ret_raw}"
|
|
517
|
+
|
|
342
518
|
symbols.append(SymbolRecord(
|
|
343
519
|
symbol=fqn,
|
|
344
520
|
type="method",
|
|
345
521
|
modifiers=modifiers,
|
|
346
|
-
annotations=
|
|
522
|
+
annotations=_anns,
|
|
347
523
|
imports_used=used,
|
|
348
524
|
declaring_file=rel_path,
|
|
349
525
|
confidence=conf,
|
|
526
|
+
stable_id=_stable_id,
|
|
527
|
+
symbol_kind=_sym_kind,
|
|
528
|
+
canonical_name=_canonical,
|
|
529
|
+
source_file=rel_path,
|
|
530
|
+
signature=_signature,
|
|
531
|
+
param_types=_param_types,
|
|
532
|
+
return_type=_ret_raw,
|
|
533
|
+
annotation_values=dict(pending_ann_values),
|
|
350
534
|
))
|
|
351
535
|
pending_anns = []
|
|
536
|
+
pending_ann_values = {}
|
|
352
537
|
depth += net
|
|
353
538
|
_pop_closed(class_stack, depth)
|
|
354
539
|
continue
|
|
355
540
|
|
|
541
|
+
# Constructor detection: uppercase name matching enclosing class
|
|
542
|
+
ctor_m = _CONSTRUCTOR_DECL_RE.match(stripped)
|
|
543
|
+
if ctor_m and ctor_m.group("name") == _class_simple:
|
|
544
|
+
_ctor_params_str = ctor_m.group("params")
|
|
545
|
+
_ctor_param_types = _parse_param_types(_ctor_params_str)
|
|
546
|
+
_ctor_anns = sorted(set(pending_anns))
|
|
547
|
+
_ctor_modifiers = _parse_modifier_str(ctor_m.group("modifiers") or "")
|
|
548
|
+
_ctor_fqn = f"{class_fqn}#<init>"
|
|
549
|
+
_stable_id = _compute_stable_id(
|
|
550
|
+
package, _class_simple, "constructor", _class_simple, _ctor_param_types
|
|
551
|
+
)
|
|
552
|
+
_param_str = ",".join(_ctor_param_types)
|
|
553
|
+
symbols.append(SymbolRecord(
|
|
554
|
+
symbol=_ctor_fqn,
|
|
555
|
+
type="method",
|
|
556
|
+
modifiers=_ctor_modifiers,
|
|
557
|
+
annotations=_ctor_anns,
|
|
558
|
+
imports_used=[],
|
|
559
|
+
declaring_file=rel_path,
|
|
560
|
+
confidence="high" if ("public" in _ctor_modifiers or _ctor_anns) else "medium",
|
|
561
|
+
stable_id=_stable_id,
|
|
562
|
+
symbol_kind="constructor",
|
|
563
|
+
canonical_name=f"{class_fqn}#{_class_simple}({_param_str})",
|
|
564
|
+
source_file=rel_path,
|
|
565
|
+
signature=f"({_param_str})->void",
|
|
566
|
+
param_types=_ctor_param_types,
|
|
567
|
+
return_type="void",
|
|
568
|
+
))
|
|
569
|
+
pending_anns = []
|
|
570
|
+
pending_ann_values = {}
|
|
571
|
+
depth += net
|
|
572
|
+
_pop_closed(class_stack, depth)
|
|
573
|
+
continue
|
|
574
|
+
|
|
356
575
|
if pending_anns and any(a in _INJECT_ANNOTATIONS for a in pending_anns):
|
|
357
576
|
fld_m = _FIELD_DECL_RE.match(stripped)
|
|
358
577
|
if fld_m:
|
|
359
578
|
fname = fld_m.group("name")
|
|
360
579
|
ftype = fld_m.group("type").strip()
|
|
361
580
|
if fname and ftype and fname not in _JAVA_KEYWORDS:
|
|
362
|
-
class_fqn = class_stack[-1][0]
|
|
363
581
|
fqn = f"{class_fqn}.{fname}"
|
|
364
582
|
modifiers = _parse_modifier_str(fld_m.group("modifiers") or "")
|
|
365
583
|
used = _resolve_types_from_text(ftype, import_map)
|
|
584
|
+
_stable_id = _compute_stable_id(
|
|
585
|
+
package, _class_simple, "field", fname, None, ftype
|
|
586
|
+
)
|
|
366
587
|
|
|
367
588
|
symbols.append(SymbolRecord(
|
|
368
589
|
symbol=fqn,
|
|
@@ -372,13 +593,20 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
372
593
|
imports_used=used,
|
|
373
594
|
declaring_file=rel_path,
|
|
374
595
|
confidence="high",
|
|
596
|
+
stable_id=_stable_id,
|
|
597
|
+
symbol_kind="field",
|
|
598
|
+
canonical_name=fqn,
|
|
599
|
+
source_file=rel_path,
|
|
600
|
+
signature=f"{ftype} {fname}",
|
|
375
601
|
))
|
|
376
602
|
pending_anns = []
|
|
603
|
+
pending_ann_values = {}
|
|
377
604
|
depth += net
|
|
378
605
|
_pop_closed(class_stack, depth)
|
|
379
606
|
continue
|
|
380
607
|
|
|
381
608
|
pending_anns = []
|
|
609
|
+
pending_ann_values = {}
|
|
382
610
|
depth += net
|
|
383
611
|
_pop_closed(class_stack, depth)
|
|
384
612
|
|
|
@@ -545,6 +773,20 @@ def _build_relations(
|
|
|
545
773
|
evidence={"type": "annotation", "value": f"@RequestMapping(\"{m_path}\")"},
|
|
546
774
|
))
|
|
547
775
|
|
|
776
|
+
# contained_in edges: method/field → enclosing class (structural membership)
|
|
777
|
+
_local_classes = {s.symbol for s in symbols if s.type in ("class", "interface")}
|
|
778
|
+
for sym in symbols:
|
|
779
|
+
if sym.type in ("method", "field"):
|
|
780
|
+
enclosing = _enclosing_class(sym.symbol)
|
|
781
|
+
if enclosing != sym.symbol and enclosing in _local_classes:
|
|
782
|
+
edges.append(RelationEdge(
|
|
783
|
+
from_symbol=sym.symbol,
|
|
784
|
+
to_symbol=enclosing,
|
|
785
|
+
type="contained_in",
|
|
786
|
+
confidence="high",
|
|
787
|
+
evidence={"type": "structural", "value": f"member of {enclosing}"},
|
|
788
|
+
))
|
|
789
|
+
|
|
548
790
|
seen: set[tuple[str, str, str]] = set()
|
|
549
791
|
unique: list[RelationEdge] = []
|
|
550
792
|
for e in edges:
|
|
@@ -560,8 +802,73 @@ def _build_relations(
|
|
|
560
802
|
# Phase 4 — Symbol-level diff
|
|
561
803
|
# ---------------------------------------------------------------------------
|
|
562
804
|
|
|
805
|
+
# ---------------------------------------------------------------------------
|
|
806
|
+
# Route-surface helpers
|
|
807
|
+
# ---------------------------------------------------------------------------
|
|
808
|
+
|
|
809
|
+
def _parse_route_path(args_str: str) -> str:
|
|
810
|
+
"""Extract path string from annotation args. Handles named and positional forms."""
|
|
811
|
+
if not args_str:
|
|
812
|
+
return ""
|
|
813
|
+
for key in ("value", "path"):
|
|
814
|
+
m = re.search(rf'\b{key}\s*=\s*"([^"]*)"', args_str)
|
|
815
|
+
if m:
|
|
816
|
+
return m.group(1)
|
|
817
|
+
m = re.search(r'"([^"]*)"', args_str)
|
|
818
|
+
return m.group(1) if m else ""
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _parse_route_http_method(ann_name: str, args_str: str) -> str:
|
|
822
|
+
"""Derive HTTP method from annotation name or explicit method= arg."""
|
|
823
|
+
explicit = _HTTP_METHOD_MAP.get(ann_name)
|
|
824
|
+
if explicit:
|
|
825
|
+
return explicit
|
|
826
|
+
m = re.search(r'method\s*=\s*(?:RequestMethod\.)?(\w+)', args_str or "")
|
|
827
|
+
return m.group(1).upper() if m else ""
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _parse_route_extras(args_str: str) -> dict:
|
|
831
|
+
"""Extract produces/consumes/params from annotation args."""
|
|
832
|
+
result: dict = {}
|
|
833
|
+
for key in ("produces", "consumes", "params"):
|
|
834
|
+
m = re.search(rf'\b{key}\s*=\s*(?:"([^"]*)"|{{([^}}]*)}})', args_str or "")
|
|
835
|
+
if m:
|
|
836
|
+
result[key] = m.group(1) or m.group(2) or ""
|
|
837
|
+
return result
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _is_route_symbol(sym: SymbolRecord) -> bool:
|
|
841
|
+
return bool(sym.annotation_values) and any(
|
|
842
|
+
a in _ENDPOINT_ANNOTATIONS for a in sym.annotations
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _route_annotation_name(sym: SymbolRecord) -> str:
|
|
847
|
+
for ann in sym.annotations:
|
|
848
|
+
if ann in _ENDPOINT_ANNOTATIONS:
|
|
849
|
+
return ann
|
|
850
|
+
return ""
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _enclosing_class(fqn: str) -> str:
|
|
854
|
+
if "#" in fqn:
|
|
855
|
+
return fqn.split("#")[0]
|
|
856
|
+
if "." in fqn:
|
|
857
|
+
return fqn.rsplit(".", 1)[0]
|
|
858
|
+
return fqn
|
|
859
|
+
|
|
860
|
+
|
|
563
861
|
def _symbol_fingerprint(sym: SymbolRecord) -> str:
|
|
564
|
-
|
|
862
|
+
route_val_seg = "|".join(
|
|
863
|
+
f"{a}:{sym.annotation_values.get(a, '')}"
|
|
864
|
+
for a in sorted(sym.annotations)
|
|
865
|
+
if a in _ENDPOINT_ANNOTATIONS
|
|
866
|
+
)
|
|
867
|
+
return (
|
|
868
|
+
f"{sym.type}|{','.join(sym.modifiers)}"
|
|
869
|
+
f"|{','.join(sym.annotations)}|{','.join(sym.imports_used)}"
|
|
870
|
+
f"|{route_val_seg}"
|
|
871
|
+
)
|
|
565
872
|
|
|
566
873
|
|
|
567
874
|
def _diff_symbols(
|
|
@@ -601,7 +908,11 @@ def _diff_symbols(
|
|
|
601
908
|
continue
|
|
602
909
|
|
|
603
910
|
diff_type = "unknown"
|
|
604
|
-
|
|
911
|
+
old_rvals = {a: old.annotation_values.get(a, "") for a in old.annotations if a in _ENDPOINT_ANNOTATIONS}
|
|
912
|
+
new_rvals = {a: new.annotation_values.get(a, "") for a in new.annotations if a in _ENDPOINT_ANNOTATIONS}
|
|
913
|
+
if old_rvals != new_rvals:
|
|
914
|
+
diff_type = "route_surface_change"
|
|
915
|
+
elif set(old.annotations) != set(new.annotations):
|
|
605
916
|
diff_type = "annotation_change"
|
|
606
917
|
elif set(old.modifiers) != set(new.modifiers):
|
|
607
918
|
diff_type = "structural_change"
|
|
@@ -618,6 +929,67 @@ def _diff_symbols(
|
|
|
618
929
|
return changed
|
|
619
930
|
|
|
620
931
|
|
|
932
|
+
def _diff_routes(
|
|
933
|
+
old_syms: list[SymbolRecord],
|
|
934
|
+
new_syms: list[SymbolRecord],
|
|
935
|
+
) -> list[dict]:
|
|
936
|
+
"""Detect route-surface changes between old and new symbol sets."""
|
|
937
|
+
old_map = {s.symbol: s for s in old_syms if _is_route_symbol(s)}
|
|
938
|
+
new_map = {s.symbol: s for s in new_syms if _is_route_symbol(s)}
|
|
939
|
+
|
|
940
|
+
route_diffs: list[dict] = []
|
|
941
|
+
for fqn in sorted(set(old_map) & set(new_map)):
|
|
942
|
+
old_sym = old_map[fqn]
|
|
943
|
+
new_sym = new_map[fqn]
|
|
944
|
+
|
|
945
|
+
old_ann = _route_annotation_name(old_sym)
|
|
946
|
+
new_ann = _route_annotation_name(new_sym)
|
|
947
|
+
old_args = old_sym.annotation_values.get(old_ann, "")
|
|
948
|
+
new_args = new_sym.annotation_values.get(new_ann, "")
|
|
949
|
+
|
|
950
|
+
old_path = _parse_route_path(old_args)
|
|
951
|
+
new_path = _parse_route_path(new_args)
|
|
952
|
+
old_http = _parse_route_http_method(old_ann, old_args)
|
|
953
|
+
new_http = _parse_route_http_method(new_ann, new_args)
|
|
954
|
+
old_extras = _parse_route_extras(old_args)
|
|
955
|
+
new_extras = _parse_route_extras(new_args)
|
|
956
|
+
|
|
957
|
+
if old_path == new_path and old_http == new_http and old_ann == new_ann and old_extras == new_extras:
|
|
958
|
+
continue
|
|
959
|
+
|
|
960
|
+
evidence: dict = {
|
|
961
|
+
"annotation_value_changed": old_path != new_path,
|
|
962
|
+
"mapping_annotation": new_ann.lstrip("@"),
|
|
963
|
+
"old_value": old_path,
|
|
964
|
+
"new_value": new_path,
|
|
965
|
+
}
|
|
966
|
+
if old_http != new_http:
|
|
967
|
+
evidence["http_method_changed"] = True
|
|
968
|
+
evidence["old_http_method"] = old_http
|
|
969
|
+
evidence["new_http_method"] = new_http
|
|
970
|
+
if old_ann != new_ann:
|
|
971
|
+
evidence["annotation_changed"] = True
|
|
972
|
+
evidence["old_annotation"] = old_ann
|
|
973
|
+
evidence["new_annotation"] = new_ann
|
|
974
|
+
for key in ("produces", "consumes", "params"):
|
|
975
|
+
if old_extras.get(key) != new_extras.get(key):
|
|
976
|
+
evidence[f"{key}_changed"] = True
|
|
977
|
+
evidence[f"old_{key}"] = old_extras.get(key, "")
|
|
978
|
+
evidence[f"new_{key}"] = new_extras.get(key, "")
|
|
979
|
+
|
|
980
|
+
route_diffs.append({
|
|
981
|
+
"symbol": fqn,
|
|
982
|
+
"controller": _enclosing_class(fqn),
|
|
983
|
+
"route_surface_changed": True,
|
|
984
|
+
"old_route": old_path,
|
|
985
|
+
"new_route": new_path,
|
|
986
|
+
"stable_id": new_sym.stable_id,
|
|
987
|
+
"evidence": evidence,
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
return sorted(route_diffs, key=lambda d: d["symbol"])
|
|
991
|
+
|
|
992
|
+
|
|
621
993
|
def _get_git_old_content(git_root: Path, rel_path: str, since: str) -> Optional[str]:
|
|
622
994
|
try:
|
|
623
995
|
result = subprocess.run(
|
|
@@ -746,38 +1118,101 @@ def _detect_subsystems(all_fqns: list[str], relations: list[RelationEdge]) -> li
|
|
|
746
1118
|
return [sorted(v) for v in sorted(components.values())]
|
|
747
1119
|
|
|
748
1120
|
|
|
749
|
-
|
|
1121
|
+
_EDGE_REASON_TEMPLATES: dict[str, str] = {
|
|
1122
|
+
"imports": "{from_sym} depends on {to_sym} (import)",
|
|
1123
|
+
"injects": "{from_sym} injects {to_sym}",
|
|
1124
|
+
"implements": "{from_sym} implements {to_sym}",
|
|
1125
|
+
"extends": "{from_sym} extends {to_sym}",
|
|
1126
|
+
"contained_in": "{from_sym} is a member of {to_sym}",
|
|
1127
|
+
"annotated_with": "{from_sym} is annotated with {to_sym}",
|
|
1128
|
+
"mapped_to": "Route {to_sym} depends on {from_sym}",
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
# Edge types to exclude from reverse impact traversal (too noisy / non-dependency semantics)
|
|
1132
|
+
_REVERSE_EXCLUDE: frozenset[str] = frozenset({"annotated_with", "mapped_to"})
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def _edge_reason(edge_type: str, from_sym: str, to_sym: str) -> str:
|
|
1136
|
+
tmpl = _EDGE_REASON_TEMPLATES.get(
|
|
1137
|
+
edge_type, "{from_sym} → {to_sym} [{edge_type}]"
|
|
1138
|
+
)
|
|
1139
|
+
return tmpl.format(from_sym=from_sym, to_sym=to_sym, edge_type=edge_type)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _build_reverse_adjacency(
|
|
1143
|
+
relations: list[RelationEdge],
|
|
1144
|
+
all_fqns: set[str],
|
|
1145
|
+
) -> dict[str, list[RelationEdge]]:
|
|
1146
|
+
"""Invert the relation graph: target → edges pointing to it (known symbols only)."""
|
|
1147
|
+
reverse: dict[str, list[RelationEdge]] = {}
|
|
1148
|
+
for edge in relations:
|
|
1149
|
+
if edge.type in _REVERSE_EXCLUDE:
|
|
1150
|
+
continue
|
|
1151
|
+
if edge.to_symbol in all_fqns:
|
|
1152
|
+
reverse.setdefault(edge.to_symbol, []).append(edge)
|
|
1153
|
+
return reverse
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def _bfs_impact_with_paths(
|
|
750
1157
|
changed_fqns: set[str],
|
|
751
1158
|
changed_scores: dict[str, float],
|
|
752
|
-
|
|
1159
|
+
reverse_adj: dict[str, list[RelationEdge]],
|
|
753
1160
|
all_fqns: set[str],
|
|
754
1161
|
max_depth: int = _BFS_MAX_DEPTH,
|
|
1162
|
+
enclosing_seeds: set[str] | None = None,
|
|
755
1163
|
) -> list[dict]:
|
|
756
|
-
"""BFS
|
|
1164
|
+
"""BFS on reverse graph: propagates impact from changed symbols to dependents.
|
|
1165
|
+
|
|
1166
|
+
Each impacted entry carries included_because: explicit graph path explaining inclusion.
|
|
1167
|
+
No graph path → no impact (deterministic guarantee).
|
|
1168
|
+
|
|
1169
|
+
enclosing_seeds: set of extra seeds that are enclosing classes (not directly changed).
|
|
1170
|
+
contained_in edges are skipped when traversing FROM these seeds to avoid pulling in
|
|
1171
|
+
sibling members of the actually-changed symbol.
|
|
1172
|
+
"""
|
|
1173
|
+
_enclosing = enclosing_seeds or set()
|
|
757
1174
|
impacted: dict[str, dict] = {}
|
|
758
|
-
# (node,
|
|
759
|
-
queue: deque[tuple[str, str, int, float]] = deque()
|
|
1175
|
+
# (node, via_fqn, depth, score, path, reasons)
|
|
1176
|
+
queue: deque[tuple[str, str, int, float, list[str], list[str]]] = deque()
|
|
760
1177
|
|
|
761
1178
|
for fqn in sorted(changed_fqns):
|
|
762
1179
|
base = changed_scores.get(fqn, 0.0)
|
|
763
|
-
|
|
1180
|
+
skip_contained = fqn in _enclosing
|
|
1181
|
+
for edge in sorted(reverse_adj.get(fqn, []), key=lambda e: e.from_symbol):
|
|
1182
|
+
if skip_contained and edge.type == "contained_in":
|
|
1183
|
+
continue
|
|
1184
|
+
neighbor = edge.from_symbol
|
|
764
1185
|
if neighbor not in changed_fqns and neighbor in all_fqns:
|
|
765
1186
|
score = round(base * _PROPAGATION_DECAY, 4)
|
|
766
1187
|
if score > 0:
|
|
767
|
-
|
|
1188
|
+
reason = _edge_reason(edge.type, neighbor, fqn)
|
|
1189
|
+
queue.append((neighbor, fqn, 1, score, [fqn, neighbor], [reason]))
|
|
768
1190
|
|
|
769
1191
|
while queue:
|
|
770
|
-
node, via, depth, score = queue.popleft()
|
|
1192
|
+
node, via, depth, score, path, reasons = queue.popleft()
|
|
771
1193
|
existing = impacted.get(node)
|
|
772
1194
|
if existing and existing["impact_score"] >= score:
|
|
773
1195
|
continue
|
|
774
|
-
impacted[node] = {
|
|
1196
|
+
impacted[node] = {
|
|
1197
|
+
"entity": node,
|
|
1198
|
+
"depth": depth,
|
|
1199
|
+
"impact_score": score,
|
|
1200
|
+
"via": via,
|
|
1201
|
+
"graph_path": path,
|
|
1202
|
+
"included_because": reasons,
|
|
1203
|
+
}
|
|
775
1204
|
if depth < max_depth:
|
|
776
|
-
for
|
|
1205
|
+
for edge in sorted(reverse_adj.get(node, []), key=lambda e: e.from_symbol):
|
|
1206
|
+
neighbor = edge.from_symbol
|
|
777
1207
|
if neighbor not in changed_fqns and neighbor in all_fqns:
|
|
778
1208
|
next_score = round(score * _PROPAGATION_DECAY, 4)
|
|
779
1209
|
if next_score > 0:
|
|
780
|
-
|
|
1210
|
+
reason = _edge_reason(edge.type, neighbor, node)
|
|
1211
|
+
queue.append((
|
|
1212
|
+
neighbor, node, depth + 1, next_score,
|
|
1213
|
+
path + [neighbor],
|
|
1214
|
+
reasons + [reason],
|
|
1215
|
+
))
|
|
781
1216
|
|
|
782
1217
|
return sorted(impacted.values(), key=lambda x: (-x["impact_score"], x["entity"]))
|
|
783
1218
|
|
|
@@ -801,6 +1236,7 @@ def _assemble(
|
|
|
801
1236
|
relations: list[RelationEdge],
|
|
802
1237
|
changed_symbols: list[ChangedSymbol],
|
|
803
1238
|
spring_summary: dict, # noqa: ARG001 — used internally via _spring_role on symbols
|
|
1239
|
+
route_diffs: list[dict] | None = None,
|
|
804
1240
|
) -> dict:
|
|
805
1241
|
"""Phase 5: Final assembly — single deterministic output contract."""
|
|
806
1242
|
sorted_syms = sorted(symbols, key=lambda s: s.symbol)
|
|
@@ -912,11 +1348,29 @@ def _assemble(
|
|
|
912
1348
|
"evidence_bundle": bundle.to_dict() if bundle else None,
|
|
913
1349
|
})
|
|
914
1350
|
|
|
915
|
-
# ---
|
|
1351
|
+
# --- Reverse graph: target → dependents (for impact propagation + agent queries) ---
|
|
1352
|
+
reverse_adj = _build_reverse_adjacency(sorted_rels, all_fqns_set)
|
|
1353
|
+
|
|
1354
|
+
# --- Impact propagation (BFS on reverse graph — finds who depends on changed symbol) ---
|
|
916
1355
|
changed_with_graph = {e["entity"] for e in changed_entities_out}
|
|
917
1356
|
changed_scores_map = {fqn: node_scores.get(fqn, 0.0) for fqn in changed_with_graph}
|
|
918
|
-
|
|
919
|
-
|
|
1357
|
+
|
|
1358
|
+
# Method/field change → also propagate from enclosing class (class is effectively changed).
|
|
1359
|
+
# These are "enclosing seeds" — contained_in edges are skipped from them to avoid
|
|
1360
|
+
# pulling in sibling members of the actually-changed symbol.
|
|
1361
|
+
_enclosing_seeds: set[str] = set()
|
|
1362
|
+
_extra_seeds: dict[str, float] = {}
|
|
1363
|
+
for fqn, score in list(changed_scores_map.items()):
|
|
1364
|
+
enclosing = _enclosing_class(fqn)
|
|
1365
|
+
if enclosing != fqn and enclosing in all_fqns_set and enclosing not in changed_scores_map:
|
|
1366
|
+
_extra_seeds[enclosing] = max(_extra_seeds.get(enclosing, 0.0), score)
|
|
1367
|
+
_enclosing_seeds.add(enclosing)
|
|
1368
|
+
changed_with_graph.update(_extra_seeds)
|
|
1369
|
+
changed_scores_map.update(_extra_seeds)
|
|
1370
|
+
|
|
1371
|
+
impacted_entities_out = _bfs_impact_with_paths(
|
|
1372
|
+
changed_with_graph, changed_scores_map, reverse_adj, all_fqns_set,
|
|
1373
|
+
enclosing_seeds=_enclosing_seeds,
|
|
920
1374
|
)
|
|
921
1375
|
|
|
922
1376
|
# --- Subsystem detection (connected components, graph-only) ---
|
|
@@ -942,6 +1396,11 @@ def _assemble(
|
|
|
942
1396
|
graph_nodes = [
|
|
943
1397
|
{
|
|
944
1398
|
"fqn": s.symbol,
|
|
1399
|
+
"stable_id": s.stable_id,
|
|
1400
|
+
"symbol_kind": s.symbol_kind,
|
|
1401
|
+
"canonical_name": s.canonical_name or s.symbol,
|
|
1402
|
+
"source_file": s.declaring_file,
|
|
1403
|
+
"signature": s.signature,
|
|
945
1404
|
"type": s.type,
|
|
946
1405
|
"role": spring_role_map.get(s.symbol, "other"),
|
|
947
1406
|
"in_degree": in_deg.get(s.symbol, 0),
|
|
@@ -951,12 +1410,21 @@ def _assemble(
|
|
|
951
1410
|
]
|
|
952
1411
|
graph_edges = [_edge_to_dict(e) for e in sorted_rels]
|
|
953
1412
|
|
|
1413
|
+
# Reverse graph index: target_fqn → {edge_type → [from_fqn, ...]} for agent queries
|
|
1414
|
+
reverse_graph_out: dict[str, dict[str, list[str]]] = {}
|
|
1415
|
+
for target, edges_in in sorted(reverse_adj.items()):
|
|
1416
|
+
by_type: dict[str, list[str]] = {}
|
|
1417
|
+
for e in sorted(edges_in, key=lambda x: x.from_symbol):
|
|
1418
|
+
by_type.setdefault(e.type, []).append(e.from_symbol)
|
|
1419
|
+
reverse_graph_out[target] = by_type
|
|
1420
|
+
|
|
954
1421
|
return {
|
|
955
1422
|
"schema_version": "final-v1",
|
|
956
1423
|
"graph": {
|
|
957
1424
|
"nodes": graph_nodes,
|
|
958
1425
|
"edges": graph_edges,
|
|
959
1426
|
},
|
|
1427
|
+
"reverse_graph": reverse_graph_out,
|
|
960
1428
|
"analysis": {
|
|
961
1429
|
"changed_entities": changed_entities_out,
|
|
962
1430
|
"impacted_entities": impacted_entities_out,
|
|
@@ -969,6 +1437,7 @@ def _assemble(
|
|
|
969
1437
|
},
|
|
970
1438
|
"subsystems": subsystems,
|
|
971
1439
|
"change_set": change_set_out,
|
|
1440
|
+
"route_surface": route_diffs or [],
|
|
972
1441
|
"audit": {
|
|
973
1442
|
"dropped_fields": dropped_fields,
|
|
974
1443
|
},
|
|
@@ -999,11 +1468,13 @@ def extract_file_ir(
|
|
|
999
1468
|
spring_summary = _build_spring_summary(symbols)
|
|
1000
1469
|
|
|
1001
1470
|
changed_symbols: list[ChangedSymbol] = []
|
|
1471
|
+
route_diffs: list[dict] = []
|
|
1002
1472
|
if old_source is not None:
|
|
1003
1473
|
_, old_symbols, _ = _extract_symbols(old_source, rel_path)
|
|
1004
1474
|
changed_symbols = _diff_symbols(old_symbols, symbols)
|
|
1475
|
+
route_diffs = _diff_routes(old_symbols, symbols)
|
|
1005
1476
|
|
|
1006
|
-
return _assemble(symbols, relations, changed_symbols, spring_summary)
|
|
1477
|
+
return _assemble(symbols, relations, changed_symbols, spring_summary, route_diffs)
|
|
1007
1478
|
|
|
1008
1479
|
|
|
1009
1480
|
def build_repo_ir(
|
|
@@ -1024,6 +1495,7 @@ def build_repo_ir(
|
|
|
1024
1495
|
all_symbols: list[SymbolRecord] = []
|
|
1025
1496
|
all_relations: list[RelationEdge] = []
|
|
1026
1497
|
all_changed: list[ChangedSymbol] = []
|
|
1498
|
+
all_route_diffs: list[dict] = []
|
|
1027
1499
|
|
|
1028
1500
|
for rel_path in sorted(file_paths):
|
|
1029
1501
|
abs_path = root / rel_path
|
|
@@ -1042,6 +1514,7 @@ def build_repo_ir(
|
|
|
1042
1514
|
if old_source is not None:
|
|
1043
1515
|
_, old_symbols, _ = _extract_symbols(old_source, rel_path)
|
|
1044
1516
|
all_changed.extend(_diff_symbols(old_symbols, symbols))
|
|
1517
|
+
all_route_diffs.extend(_diff_routes(old_symbols, symbols))
|
|
1045
1518
|
elif since:
|
|
1046
1519
|
for sym in symbols:
|
|
1047
1520
|
all_changed.append(ChangedSymbol(
|
|
@@ -1065,7 +1538,8 @@ def build_repo_ir(
|
|
|
1065
1538
|
seen.add(key)
|
|
1066
1539
|
unique_relations.append(e)
|
|
1067
1540
|
|
|
1068
|
-
|
|
1541
|
+
all_route_diffs_sorted = sorted(all_route_diffs, key=lambda d: d["symbol"])
|
|
1542
|
+
return _assemble(all_symbols, unique_relations, all_changed, spring_summary, all_route_diffs_sorted)
|
|
1069
1543
|
|
|
1070
1544
|
|
|
1071
1545
|
# ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.30.
|
|
3
|
+
Version: 1.30.14
|
|
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.14
|
|
261
261
|
```
|
|
262
262
|
|
|
263
263
|
---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=J6N6z5RQFo5wJ4DBIDbgaGji2fockYUeA6wyuudLbU4,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
|
|
@@ -17,17 +17,17 @@ sourcecode/doc_analyzer.py,sha256=afA4uJFwXZ_uR2l4J0pQwbeTkRkGmKdN9KhRVYePBUw,24
|
|
|
17
17
|
sourcecode/entrypoint_classifier.py,sha256=gvKgl0f5T8ol1r4JMmkeqGHuZTfZJiOwFOWdc7EYwYw,4061
|
|
18
18
|
sourcecode/env_analyzer.py,sha256=GxCidahAAIptTdDFIlVB6URd4HBnBlIX_SqUov3MBRQ,22076
|
|
19
19
|
sourcecode/file_classifier.py,sha256=48ly5Z6exkzBy8lNy1AkdP4-oJqIA1zT3LZfffuTyDo,11572
|
|
20
|
-
sourcecode/flow_analyzer.py,sha256=
|
|
20
|
+
sourcecode/flow_analyzer.py,sha256=1XczDeeIjOplAAO6QLprwBEGFgHk-Qf4T8ZLeyKzgWY,27603
|
|
21
21
|
sourcecode/git_analyzer.py,sha256=_pCg2V4d2aa17k9hayTzpexAj8syvyk4y9NYNvvgOAI,12802
|
|
22
22
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
23
23
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
24
|
-
sourcecode/prepare_context.py,sha256=
|
|
24
|
+
sourcecode/prepare_context.py,sha256=OD5dYHiPSk42XSFj0WNIeFS_HFdb4BmN2eY5lxn8vJ8,149941
|
|
25
25
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
26
26
|
sourcecode/ranking_engine.py,sha256=virVglafZufioHpZpwktjMvUiL0TZELWQCQnQNV8dFo,9360
|
|
27
27
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
28
28
|
sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
|
|
29
29
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
30
|
-
sourcecode/repository_ir.py,sha256=
|
|
30
|
+
sourcecode/repository_ir.py,sha256=uwi6-mJPh1E2-RYjf-ndqUXTvN32-7Nfv6oFqwHQi1c,57566
|
|
31
31
|
sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
|
|
32
32
|
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
33
33
|
sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
|
|
@@ -63,8 +63,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
63
63
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
64
64
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
65
65
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
66
|
-
sourcecode-1.30.
|
|
67
|
-
sourcecode-1.30.
|
|
68
|
-
sourcecode-1.30.
|
|
69
|
-
sourcecode-1.30.
|
|
70
|
-
sourcecode-1.30.
|
|
66
|
+
sourcecode-1.30.14.dist-info/METADATA,sha256=OyIxTxe6JrHhKVJJSCThUcol7s0ydMbdAls1m0q1ePE,26773
|
|
67
|
+
sourcecode-1.30.14.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
68
|
+
sourcecode-1.30.14.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
69
|
+
sourcecode-1.30.14.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
70
|
+
sourcecode-1.30.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|