sourcecode 2.5.2__py3-none-any.whl → 2.5.4__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/architecture_analyzer.py +101 -21
- sourcecode/canonical_ir.py +3 -2
- sourcecode/classifier.py +4 -2
- sourcecode/detectors/java.py +27 -10
- sourcecode/reconciliation.py +201 -0
- sourcecode/repository_ir.py +443 -66
- sourcecode/semantic_impact_engine.py +8 -5
- sourcecode/semantic_services.py +5 -3
- sourcecode/spring_impact.py +213 -24
- sourcecode/summarizer.py +35 -14
- {sourcecode-2.5.2.dist-info → sourcecode-2.5.4.dist-info}/METADATA +1 -1
- {sourcecode-2.5.2.dist-info → sourcecode-2.5.4.dist-info}/RECORD +16 -15
- {sourcecode-2.5.2.dist-info → sourcecode-2.5.4.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.2.dist-info → sourcecode-2.5.4.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.2.dist-info → sourcecode-2.5.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
from dataclasses import dataclass
|
|
29
29
|
from typing import TYPE_CHECKING, Optional
|
|
30
30
|
|
|
31
|
+
from sourcecode.repository_ir import _resolve_declared_type
|
|
31
32
|
from sourcecode.semantic_services import SemanticServices
|
|
32
33
|
|
|
33
34
|
if TYPE_CHECKING: # avoid import cycle
|
|
@@ -236,13 +237,15 @@ class SemanticImpactEngine:
|
|
|
236
237
|
"""(field_name, type_fqn) for fields whose declared type resolves to a role
|
|
237
238
|
in `roles` — the plain setter/XML-injected dependencies the injection graph
|
|
238
239
|
cannot see (e.g. openmrs `private TemplateDAO templateDAO;`). Uses field-type
|
|
239
|
-
facts (#10)
|
|
240
|
+
facts (#10), which carry the owning file's import-resolved FQN when it had
|
|
241
|
+
one; a bare simple name still resolves only while unambiguous."""
|
|
240
242
|
by_simple = self.s.types_by_simple()
|
|
243
|
+
known = {f for fqns in by_simple.values() for f in fqns}
|
|
241
244
|
out: list[tuple[str, str]] = []
|
|
242
|
-
for fname,
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
-
out.append((fname,
|
|
245
|
+
for fname, declared in sorted(self.s.field_types_of(owner_fqn).items()):
|
|
246
|
+
fqn = _resolve_declared_type(declared, owner_fqn, by_simple, known)
|
|
247
|
+
if fqn and self._role(fqn) in roles:
|
|
248
|
+
out.append((fname, fqn))
|
|
246
249
|
return out
|
|
247
250
|
|
|
248
251
|
def _called_via_receiver(self, owner_fqn: str, receiver: str) -> Optional[str]:
|
sourcecode/semantic_services.py
CHANGED
|
@@ -209,9 +209,11 @@ class SemanticServices:
|
|
|
209
209
|
return self._g.annotation_arg_values_in(fqn)
|
|
210
210
|
|
|
211
211
|
def field_types_of(self, class_fqn: str) -> dict[str, str]:
|
|
212
|
-
"""{field_name ->
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
"""{field_name -> declared_type} for a class's fields (annotated or plain).
|
|
213
|
+
Resolves a body invocation's receiver to its typed dependency — the fact
|
|
214
|
+
injection edges miss for plain setter/XML-injected fields. The type is the
|
|
215
|
+
FQN when the declaring file imported it explicitly, else a simple name;
|
|
216
|
+
`repository_ir._resolve_declared_type` handles both."""
|
|
215
217
|
return self._g.field_types_in(class_fqn)
|
|
216
218
|
|
|
217
219
|
def return_type_index(self) -> dict[str, set[str]]:
|
sourcecode/spring_impact.py
CHANGED
|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
from typing import TYPE_CHECKING, Optional
|
|
27
27
|
|
|
28
28
|
from sourcecode.fqn_utils import normalize_owner_fqn
|
|
29
|
+
from sourcecode.reconciliation import http_handler_classes, reconcile_impact_evidence
|
|
29
30
|
from sourcecode.spring_findings import SEVERITY_ORDER, SpringFinding
|
|
30
31
|
from sourcecode.spring_model import SpringSemanticModel
|
|
31
32
|
|
|
@@ -340,6 +341,22 @@ def _recover_external_iface_callers(
|
|
|
340
341
|
# Symbol resolution
|
|
341
342
|
# ---------------------------------------------------------------------------
|
|
342
343
|
|
|
344
|
+
def _parse_coverage_gap(cir) -> Optional[dict]:
|
|
345
|
+
"""The parse_coverage analysis gap from CIR metadata, if any (P1-E).
|
|
346
|
+
|
|
347
|
+
Written by the extraction-time reconciliation rule (reconciliation.py):
|
|
348
|
+
files that declare a type but produced no symbols. Impact answers built on
|
|
349
|
+
a graph with invisible files must say so instead of asserting completeness.
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
for g in (getattr(cir, "metadata", None) or {}).get("analysis_gaps") or []:
|
|
353
|
+
if isinstance(g, dict) and g.get("area") == "parse_coverage":
|
|
354
|
+
return g
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
|
|
343
360
|
def _class_of(fqn: str) -> str:
|
|
344
361
|
"""Extract class FQN from a method FQN. pkg.Class#method → pkg.Class."""
|
|
345
362
|
if "#" in fqn:
|
|
@@ -581,6 +598,32 @@ def _bfs_callers(
|
|
|
581
598
|
return direct, indirect, was_truncated, self_excluded
|
|
582
599
|
|
|
583
600
|
|
|
601
|
+
def _dedupe_caller_granularity(callers: list[str]) -> list[str]:
|
|
602
|
+
"""Drop a class-level caller entry when a method entry of the same class is
|
|
603
|
+
present in the same list (P1-B3). Order preserved.
|
|
604
|
+
|
|
605
|
+
A static call binds TWO edges: the coarse class-level one from the per-file
|
|
606
|
+
scan (`X --calls--> T`, which cannot name the callee) and, since P1-B2, the
|
|
607
|
+
precise method-level one (`X#m --calls--> T#callee`). They are additive on
|
|
608
|
+
purpose — a static call in a field initializer or static block produces no
|
|
609
|
+
body fact, so only the class edge carries it and dropping it erases real
|
|
610
|
+
coupling (measured: BroadleafCurrencyUtils lost 1325→561 transitive reach and
|
|
611
|
+
all 20 affected endpoints). The cost of keeping both is that one caller is
|
|
612
|
+
reported at two granularities: `X` and `X#m` read as two callers, and
|
|
613
|
+
`len(direct_callers)` double-counts them into `_compute_risk`.
|
|
614
|
+
|
|
615
|
+
Collapsing is safe HERE and only here, because it is a view: the method entry
|
|
616
|
+
already names the class, so the set of caller *classes* is identical and no
|
|
617
|
+
edge is deleted. A class entry with no method sibling (the field-initializer
|
|
618
|
+
case) has no precise entry to defer to and is kept. Callers of both
|
|
619
|
+
granularities remain the input to endpoint and finding mapping — a class node
|
|
620
|
+
reaches every endpoint of a controller, a method node only its own — so this
|
|
621
|
+
is applied to the reported/scored lists, never to the reach computation.
|
|
622
|
+
"""
|
|
623
|
+
classes_with_methods = {_class_of(c) for c in callers if "#" in c}
|
|
624
|
+
return [c for c in callers if "#" in c or c not in classes_with_methods]
|
|
625
|
+
|
|
626
|
+
|
|
584
627
|
# ---------------------------------------------------------------------------
|
|
585
628
|
# Endpoint mapping
|
|
586
629
|
# ---------------------------------------------------------------------------
|
|
@@ -744,10 +787,21 @@ def _build_chain_explanation(
|
|
|
744
787
|
endpoints_affected: int,
|
|
745
788
|
findings: int,
|
|
746
789
|
confidence: str,
|
|
790
|
+
blind_spot: bool = False,
|
|
747
791
|
) -> str:
|
|
748
792
|
"""Human-readable rationale, phrased to match the `impact` command's
|
|
749
|
-
explanation so both surfaces read consistently.
|
|
793
|
+
explanation so both surfaces read consistently.
|
|
794
|
+
|
|
795
|
+
`blind_spot` says a positively-detected gap explains the empty result, so it must
|
|
796
|
+
not be narrated as an isolated change: the warnings carry the diagnosis, and this
|
|
797
|
+
sentence is what an agent quotes."""
|
|
750
798
|
if not direct_callers and not indirect_callers and not endpoints_affected:
|
|
799
|
+
if blind_spot:
|
|
800
|
+
return (
|
|
801
|
+
"No callers or endpoints resolved, but this analysis has a known blind "
|
|
802
|
+
"spot (see analysis_warnings) — an empty result here is NOT evidence of "
|
|
803
|
+
"an isolated or low-risk change."
|
|
804
|
+
)
|
|
751
805
|
return "No callers or endpoints found in the impact chain. Low-risk isolated change."
|
|
752
806
|
parts: list[str] = []
|
|
753
807
|
if direct_callers:
|
|
@@ -840,10 +894,22 @@ class ImpactOrchestrator:
|
|
|
840
894
|
warnings.extend(sym_warnings)
|
|
841
895
|
|
|
842
896
|
if resolution == "not_found" or not seed_fqns:
|
|
897
|
+
if not warnings:
|
|
898
|
+
warnings = [f"Symbol '{symbol}' not found in CIR."]
|
|
899
|
+
# P1-E: a not-found verdict is weaker when files failed extraction —
|
|
900
|
+
# the queried symbol may be defined in one of them.
|
|
901
|
+
_nf_gap = _parse_coverage_gap(cir)
|
|
902
|
+
if _nf_gap:
|
|
903
|
+
_nf_n = len(_nf_gap.get("files") or [])
|
|
904
|
+
warnings.append(
|
|
905
|
+
f"Parse-coverage gap (P1-E): {_nf_n} Java file(s) declare a type "
|
|
906
|
+
"but produced no symbols — the queried symbol may be defined in "
|
|
907
|
+
"one of them (see analysis_gaps)."
|
|
908
|
+
)
|
|
843
909
|
return ImpactChainResult(
|
|
844
910
|
symbol=symbol,
|
|
845
911
|
resolution="not_found",
|
|
846
|
-
analysis_warnings=warnings
|
|
912
|
+
analysis_warnings=warnings,
|
|
847
913
|
risk_level="unknown",
|
|
848
914
|
confidence="low",
|
|
849
915
|
explanation=f"Symbol {symbol!r} not found in the IR.",
|
|
@@ -942,10 +1008,17 @@ class ImpactOrchestrator:
|
|
|
942
1008
|
)
|
|
943
1009
|
|
|
944
1010
|
# ── 2. BFS through reverse graph ─────────────────────────────────
|
|
945
|
-
|
|
1011
|
+
direct_callers_raw, indirect_callers_raw, truncated, self_excluded = _bfs_callers(
|
|
946
1012
|
seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph,
|
|
947
1013
|
method_scoped=_method_scoped,
|
|
948
1014
|
)
|
|
1015
|
+
# P1-B3: the reported/scored caller lists collapse the class-vs-method
|
|
1016
|
+
# granularity duplicate that additive static edges (P1-B2) produce. The RAW
|
|
1017
|
+
# lists stay the input to endpoint/finding mapping below: a class-level node
|
|
1018
|
+
# carries reach a method node does not (all handlers of a controller), so
|
|
1019
|
+
# deduping there would erase endpoints. See _dedupe_caller_granularity.
|
|
1020
|
+
direct_callers = _dedupe_caller_granularity(direct_callers_raw)
|
|
1021
|
+
indirect_callers = _dedupe_caller_granularity(indirect_callers_raw)
|
|
949
1022
|
if self_excluded:
|
|
950
1023
|
if _method_scoped:
|
|
951
1024
|
warnings.append(
|
|
@@ -967,7 +1040,7 @@ class ImpactOrchestrator:
|
|
|
967
1040
|
confidence_reducing = True # capped traversal → result is incomplete
|
|
968
1041
|
|
|
969
1042
|
# ── 3. Endpoints affected ─────────────────────────────────────────
|
|
970
|
-
all_callers =
|
|
1043
|
+
all_callers = direct_callers_raw + indirect_callers_raw
|
|
971
1044
|
endpoints_affected = _collect_endpoints(all_callers, seed_fqns, model)
|
|
972
1045
|
|
|
973
1046
|
# ── 4. TX boundary for the target symbol ─────────────────────────
|
|
@@ -994,7 +1067,8 @@ class ImpactOrchestrator:
|
|
|
994
1067
|
all_findings = _run_audit_for_chain(cir, model, root)
|
|
995
1068
|
|
|
996
1069
|
impact_findings_raw = _filter_findings(
|
|
997
|
-
all_findings, seed_fqns,
|
|
1070
|
+
all_findings, seed_fqns, direct_callers_raw, indirect_callers_raw,
|
|
1071
|
+
endpoints_affected,
|
|
998
1072
|
)
|
|
999
1073
|
# Sort by severity, then symbol
|
|
1000
1074
|
impact_findings_raw.sort(
|
|
@@ -1041,19 +1115,21 @@ class ImpactOrchestrator:
|
|
|
1041
1115
|
)
|
|
1042
1116
|
di_binding_ambiguous = _max_impl > 1
|
|
1043
1117
|
# Keep only genuinely new caller classes (not seeds / already found).
|
|
1044
|
-
_known = set(seed_fqns) | set(
|
|
1118
|
+
_known = set(seed_fqns) | set(direct_callers_raw) | set(indirect_callers_raw)
|
|
1045
1119
|
di_recovered_callers = [c for c in di_recovered_callers if c not in _known]
|
|
1046
1120
|
|
|
1047
1121
|
di_recovered = bool(di_recovered_callers)
|
|
1048
1122
|
if di_recovered:
|
|
1049
1123
|
# Merge recovered wiring sites into the chain and recompute the views
|
|
1050
1124
|
# that depend on the caller set (endpoints, findings, surfaces, risk).
|
|
1125
|
+
direct_callers_raw = direct_callers_raw + di_recovered_callers
|
|
1051
1126
|
direct_callers = direct_callers + di_recovered_callers
|
|
1052
1127
|
empty_blast = False
|
|
1053
|
-
all_callers =
|
|
1128
|
+
all_callers = direct_callers_raw + indirect_callers_raw
|
|
1054
1129
|
endpoints_affected = _collect_endpoints(all_callers, seed_fqns, model)
|
|
1055
1130
|
impact_findings_raw = _filter_findings(
|
|
1056
|
-
all_findings, seed_fqns,
|
|
1131
|
+
all_findings, seed_fqns, direct_callers_raw, indirect_callers_raw,
|
|
1132
|
+
endpoints_affected,
|
|
1057
1133
|
)
|
|
1058
1134
|
impact_findings_raw.sort(
|
|
1059
1135
|
key=lambda f: (SEVERITY_ORDER.get(f.severity, 9), f.symbol)
|
|
@@ -1130,6 +1206,107 @@ class ImpactOrchestrator:
|
|
|
1130
1206
|
"proof this symbol is unused."
|
|
1131
1207
|
)
|
|
1132
1208
|
|
|
1209
|
+
# P1-B: every guard above needs a class-level seed, so a `Class#method` query
|
|
1210
|
+
# never reached one — an empty method blast was reported as a high-confidence
|
|
1211
|
+
# "low-risk isolated change" no matter what was known about the class. When the
|
|
1212
|
+
# declaring class has inbound DI/import edges, the class-level query resolves
|
|
1213
|
+
# those callers correctly (they land on the class node), so the tool used to
|
|
1214
|
+
# contradict itself across granularity — "isolated" for the method, "critical"
|
|
1215
|
+
# for its class — with no signal that the narrower, more natural query was the
|
|
1216
|
+
# less trustworthy one. The method may still be genuinely uncalled; what is
|
|
1217
|
+
# unproven is the *emptiness*, so this downgrades confidence and warns rather
|
|
1218
|
+
# than inventing callers.
|
|
1219
|
+
method_scope_blind_spot = False
|
|
1220
|
+
if empty_blast and not class_level_seed and resolution != "not_found":
|
|
1221
|
+
_crev = cir.reverse_graph.get(_class_of(resolved_symbol)) or {}
|
|
1222
|
+
method_scope_inbound = sorted(
|
|
1223
|
+
{c for et in ("injects", "imports") for c in (_crev.get(et) or [])}
|
|
1224
|
+
)
|
|
1225
|
+
if method_scope_inbound:
|
|
1226
|
+
method_scope_blind_spot = True
|
|
1227
|
+
warnings.append(
|
|
1228
|
+
f"Method-scoped empty blast radius is unproven (P1-B): no call edge "
|
|
1229
|
+
f"resolves to this method, but {len(method_scope_inbound)} in-repo "
|
|
1230
|
+
f"class(es) inject or import the declaring class "
|
|
1231
|
+
f"{_class_of(resolved_symbol)}. The call may travel an edge the "
|
|
1232
|
+
"graph does not model (wildcard-imported or generic receiver, "
|
|
1233
|
+
"reflection, method reference, framework callback). 0 callers is "
|
|
1234
|
+
"NOT proof this method is unused — re-query at class level (drop "
|
|
1235
|
+
"the '#method') to see the declaring class's callers."
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
# P1-E: an empty blast radius computed over a graph with provably
|
|
1239
|
+
# invisible files is unproven — a caller could live in a file that
|
|
1240
|
+
# declared a type but produced no symbols (parse_coverage gap written by
|
|
1241
|
+
# the extraction-time reconciliation rule). Fires at any seed
|
|
1242
|
+
# granularity: unlike CH-003/CH-005/G-2 (class-level) and P1-B
|
|
1243
|
+
# (method-level), file invisibility undermines both equally.
|
|
1244
|
+
parse_coverage_blind_spot = False
|
|
1245
|
+
if empty_blast:
|
|
1246
|
+
_pc_gap = _parse_coverage_gap(cir)
|
|
1247
|
+
if _pc_gap:
|
|
1248
|
+
parse_coverage_blind_spot = True
|
|
1249
|
+
_pc_n = len(_pc_gap.get("files") or [])
|
|
1250
|
+
warnings.append(
|
|
1251
|
+
f"Parse-coverage gap (P1-E): {_pc_n} Java file(s) declare a type "
|
|
1252
|
+
"but produced no symbols — a caller could live in an unmodeled "
|
|
1253
|
+
"file, so an empty blast radius is NOT proof this symbol is "
|
|
1254
|
+
"unused (see analysis_gaps for the file list)."
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
# F-1 (Evidence Reconciliation Layer) / SIM-2: the call graph and the
|
|
1258
|
+
# endpoint index are independent evidence views and can contradict each
|
|
1259
|
+
# other. keycloak simulacro: the verified caller chain contained
|
|
1260
|
+
# AuthorizationEndpoint#process while the endpoint index had NO route for
|
|
1261
|
+
# that class (a JAX-RS sub-resource locator chain the route extractor
|
|
1262
|
+
# cannot compose) — "2 endpoints exposed" was narrated at confidence=high
|
|
1263
|
+
# while the real OAuth surface was invisible. Unlike the guards above,
|
|
1264
|
+
# this fires on PARTIAL results, not just empty ones: a chain class that
|
|
1265
|
+
# provably declares HTTP handlers but has no modeled route is a
|
|
1266
|
+
# positively-detected under-report of endpoints_affected.
|
|
1267
|
+
endpoint_coverage_blind_spot = False
|
|
1268
|
+
reconciliation_findings: list[dict] = []
|
|
1269
|
+
try:
|
|
1270
|
+
_chain_classes = frozenset(
|
|
1271
|
+
_class_of(f)
|
|
1272
|
+
for f in set(seed_fqns) | set(direct_callers_raw) | set(indirect_callers_raw)
|
|
1273
|
+
)
|
|
1274
|
+
_rec = reconcile_impact_evidence(
|
|
1275
|
+
chain_classes=_chain_classes,
|
|
1276
|
+
handler_classes=http_handler_classes(cir),
|
|
1277
|
+
modeled_classes=frozenset(model.endpoint_index.controller_fqns),
|
|
1278
|
+
)
|
|
1279
|
+
reconciliation_findings = [f.to_dict() for f in _rec]
|
|
1280
|
+
_unmapped: list[str] = []
|
|
1281
|
+
for _f in _rec:
|
|
1282
|
+
if _f.rule == "endpoint_coverage":
|
|
1283
|
+
endpoint_coverage_blind_spot = True
|
|
1284
|
+
_unmapped = list(_f.symbols)
|
|
1285
|
+
if endpoint_coverage_blind_spot:
|
|
1286
|
+
warnings.append(
|
|
1287
|
+
f"Endpoint-coverage contradiction (evidence reconciliation): {len(_unmapped)} "
|
|
1288
|
+
"class(es) in the call chain declare HTTP handler methods but the "
|
|
1289
|
+
"endpoint index has no modeled route for them ["
|
|
1290
|
+
+ ", ".join(_unmapped[:5])
|
|
1291
|
+
+ (", …" if len(_unmapped) > 5 else "")
|
|
1292
|
+
+ "]. endpoints_affected UNDER-REPORTS real HTTP exposure (routes "
|
|
1293
|
+
"the extractor could not compose, e.g. JAX-RS sub-resource "
|
|
1294
|
+
"locator chains). Callers remain reliable; verify these classes' "
|
|
1295
|
+
"exposure before treating the endpoint list as complete."
|
|
1296
|
+
)
|
|
1297
|
+
except Exception:
|
|
1298
|
+
pass
|
|
1299
|
+
|
|
1300
|
+
blind_spots = (
|
|
1301
|
+
# framework_di stops being a blind spot once CH-007 recovers callers
|
|
1302
|
+
(["framework_di"] if framework_di_blind_spot and not di_recovered else [])
|
|
1303
|
+
+ (["value_type"] if value_type_blind_spot else [])
|
|
1304
|
+
+ (["unresolved_refs"] if unresolved_ref_blind_spot else [])
|
|
1305
|
+
+ (["method_scope_unproven"] if method_scope_blind_spot else [])
|
|
1306
|
+
+ (["parse_coverage"] if parse_coverage_blind_spot else [])
|
|
1307
|
+
+ (["endpoint_coverage_partial"] if endpoint_coverage_blind_spot else [])
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1133
1310
|
confidence: str
|
|
1134
1311
|
if resolution == "not_found":
|
|
1135
1312
|
confidence = "low"
|
|
@@ -1137,15 +1314,38 @@ class ImpactOrchestrator:
|
|
|
1137
1314
|
# Callers recovered structurally via the external-interface DI bridge.
|
|
1138
1315
|
# Unambiguous binding (single in-repo impl) → medium; ambiguous → low.
|
|
1139
1316
|
confidence = "low" if di_binding_ambiguous else "medium"
|
|
1140
|
-
elif
|
|
1317
|
+
elif (
|
|
1318
|
+
framework_di_blind_spot
|
|
1319
|
+
or value_type_blind_spot
|
|
1320
|
+
or unresolved_ref_blind_spot
|
|
1321
|
+
or method_scope_blind_spot
|
|
1322
|
+
or parse_coverage_blind_spot
|
|
1323
|
+
):
|
|
1141
1324
|
confidence = "low"
|
|
1142
|
-
elif resolution == "partial" or confidence_reducing:
|
|
1325
|
+
elif resolution == "partial" or confidence_reducing or endpoint_coverage_blind_spot:
|
|
1326
|
+
# endpoint_coverage: the callers are still trustworthy — only the
|
|
1327
|
+
# endpoint axis is provably incomplete, so cap at medium, not low.
|
|
1143
1328
|
confidence = "medium"
|
|
1144
1329
|
else:
|
|
1145
1330
|
confidence = "high"
|
|
1146
1331
|
|
|
1147
1332
|
elapsed_ms = round((time.monotonic() - t0) * 1000, 2)
|
|
1148
1333
|
|
|
1334
|
+
explanation = _build_chain_explanation(
|
|
1335
|
+
risk_level,
|
|
1336
|
+
len(direct_callers),
|
|
1337
|
+
len(indirect_callers),
|
|
1338
|
+
len(endpoints_affected),
|
|
1339
|
+
len(impact_findings),
|
|
1340
|
+
confidence,
|
|
1341
|
+
blind_spot=bool(blind_spots),
|
|
1342
|
+
)
|
|
1343
|
+
if endpoint_coverage_blind_spot:
|
|
1344
|
+
explanation += (
|
|
1345
|
+
" HTTP exposure is under-reported: handler class(es) in the chain "
|
|
1346
|
+
"have no modeled routes (blind_spots=endpoint_coverage_partial)."
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1149
1349
|
return ImpactChainResult(
|
|
1150
1350
|
symbol=resolved_symbol,
|
|
1151
1351
|
resolution=resolution,
|
|
@@ -1161,14 +1361,7 @@ class ImpactOrchestrator:
|
|
|
1161
1361
|
risk_level=risk_level,
|
|
1162
1362
|
risk_score=risk_score,
|
|
1163
1363
|
confidence=confidence,
|
|
1164
|
-
explanation=
|
|
1165
|
-
risk_level,
|
|
1166
|
-
len(direct_callers),
|
|
1167
|
-
len(indirect_callers),
|
|
1168
|
-
len(endpoints_affected),
|
|
1169
|
-
len(impact_findings),
|
|
1170
|
-
confidence,
|
|
1171
|
-
),
|
|
1364
|
+
explanation=explanation,
|
|
1172
1365
|
metadata={
|
|
1173
1366
|
"analysis_depth": depth,
|
|
1174
1367
|
"callers_total": len(direct_callers) + len(indirect_callers),
|
|
@@ -1177,12 +1370,8 @@ class ImpactOrchestrator:
|
|
|
1177
1370
|
"risk_score": risk_score,
|
|
1178
1371
|
"model_build_time_ms": model.build_time_ms,
|
|
1179
1372
|
"query_time_ms": elapsed_ms,
|
|
1180
|
-
"blind_spots":
|
|
1181
|
-
|
|
1182
|
-
(["framework_di"] if framework_di_blind_spot and not di_recovered else [])
|
|
1183
|
-
+ (["value_type"] if value_type_blind_spot else [])
|
|
1184
|
-
+ (["unresolved_refs"] if unresolved_ref_blind_spot else [])
|
|
1185
|
-
),
|
|
1373
|
+
"blind_spots": blind_spots,
|
|
1374
|
+
"reconciliation": reconciliation_findings,
|
|
1186
1375
|
"external_supertypes": external_supertypes,
|
|
1187
1376
|
"external_iface_callers_recovered": len(di_recovered_callers),
|
|
1188
1377
|
"external_iface_binding_ambiguous": di_binding_ambiguous,
|
sourcecode/summarizer.py
CHANGED
|
@@ -8,6 +8,7 @@ No API calls — templates applied directly over SourceMap.
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
+
from sourcecode.architecture_analyzer import _LAYER_MIN_SHARE
|
|
11
12
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
12
13
|
from sourcecode.tree_utils import safe_read_text
|
|
13
14
|
from sourcecode.entrypoint_classifier import is_production_entry_point
|
|
@@ -48,11 +49,15 @@ _ARCH_LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
48
49
|
"model": ["models", "entities", "domain"],
|
|
49
50
|
"view": ["views", "templates", "pages", "components"],
|
|
50
51
|
},
|
|
52
|
+
# P1-D: keep in step with architecture_analyzer.LAYER_PATTERNS — a keyword may
|
|
53
|
+
# only name a layer it attests. "api" (a public API package, not an HTTP
|
|
54
|
+
# controller layer), "store" and "db"/"database" are naming conventions that
|
|
55
|
+
# fabricated the "layered" claim in this very headline for neo4j and openmrs.
|
|
51
56
|
"layered": {
|
|
52
|
-
"controller": ["controllers", "
|
|
57
|
+
"controller": ["controllers", "routes", "handlers", "endpoints"],
|
|
53
58
|
"service": ["services", "usecases", "application"],
|
|
54
|
-
"repository": ["repositories", "repos", "
|
|
55
|
-
"infrastructure": ["infra", "infrastructure", "persistence"
|
|
59
|
+
"repository": ["repositories", "repos", "dao"],
|
|
60
|
+
"infrastructure": ["infra", "infrastructure", "persistence"],
|
|
56
61
|
},
|
|
57
62
|
"hexagonal": {
|
|
58
63
|
"port": ["ports", "interfaces"],
|
|
@@ -61,7 +66,11 @@ _ARCH_LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
|
61
66
|
},
|
|
62
67
|
"fullstack": {
|
|
63
68
|
"frontend": ["frontend", "client", "web", "ui", "pages", "components"],
|
|
64
|
-
|
|
69
|
+
# P1-D: "api" dropped — a `foo/api/` package is a public API surface, not
|
|
70
|
+
# evidence of a backend TIER. With it, openmrs (94.8% `org/openmrs/api/`)
|
|
71
|
+
# and jobrunr fell straight from a fabricated "layered" into a fabricated
|
|
72
|
+
# "fullstack" instead. Same conflation, same fix.
|
|
73
|
+
"backend": ["backend", "server", "services"],
|
|
65
74
|
},
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -390,20 +399,32 @@ class ProjectSummarizer:
|
|
|
390
399
|
return ". ".join(parts) + "."
|
|
391
400
|
|
|
392
401
|
def _detect_architecture_pattern(self, file_paths: list[str]) -> str | None:
|
|
393
|
-
"""Infer architecture pattern from directory names in file paths.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
402
|
+
"""Infer architecture pattern from directory names in file paths.
|
|
403
|
+
|
|
404
|
+
P1-D: a layer must hold a real share of the code. Scoring on the mere
|
|
405
|
+
PRESENCE of a directory name meant one incidental dir asserted a whole
|
|
406
|
+
layer, and the claim landed in the headline: neo4j — a graph engine with no
|
|
407
|
+
controller, repository or infrastructure directory anywhere — read "Library
|
|
408
|
+
in Java (JAX-RS), layered". Presence does not scale, so the largest repos
|
|
409
|
+
were the most confidently mislabelled. Same gate and threshold as
|
|
410
|
+
ArchitectureAnalyzer, which answers this question for the architecture prose.
|
|
411
|
+
"""
|
|
412
|
+
dir_files: dict[str, set[int]] = {}
|
|
413
|
+
for i, p in enumerate(file_paths):
|
|
414
|
+
for seg in p.replace("\\", "/").split("/")[:-1]:
|
|
415
|
+
dir_files.setdefault(seg.lower(), set()).add(i)
|
|
416
|
+
total = len(file_paths)
|
|
417
|
+
|
|
418
|
+
def _has_mass(keywords: list[str]) -> bool:
|
|
419
|
+
files: set[int] = set()
|
|
420
|
+
for d in keywords:
|
|
421
|
+
files |= dir_files.get(d, set())
|
|
422
|
+
return bool(files) and (not total or len(files) / total >= _LAYER_MIN_SHARE)
|
|
399
423
|
|
|
400
424
|
best_pattern: str | None = None
|
|
401
425
|
best_score = 0
|
|
402
426
|
for pattern_name, layer_keys in _ARCH_LAYER_PATTERNS.items():
|
|
403
|
-
score = sum(
|
|
404
|
-
1 for keywords in layer_keys.values()
|
|
405
|
-
if any(d in keywords for d in dir_names)
|
|
406
|
-
)
|
|
427
|
+
score = sum(1 for keywords in layer_keys.values() if _has_mass(keywords))
|
|
407
428
|
if score > best_score:
|
|
408
429
|
best_score = score
|
|
409
430
|
best_pattern = pattern_name
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=XumauaLNdzXEwNGsJupjZthcLY2Qo8ud3CgsBguv8-o,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
|
|
4
|
-
sourcecode/architecture_analyzer.py,sha256=
|
|
4
|
+
sourcecode/architecture_analyzer.py,sha256=CoxlEDWn1axufjIT4fpNHd_2HNUKgqBClq9z9k6jp4M,51684
|
|
5
5
|
sourcecode/architecture_summary.py,sha256=pkl6lIOnSy-poa9EzW43U0MkXhx2FDfAFh8pDXLpcTo,26630
|
|
6
6
|
sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
|
|
7
7
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
8
8
|
sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
|
|
9
9
|
sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,2574
|
|
10
|
-
sourcecode/canonical_ir.py,sha256=
|
|
10
|
+
sourcecode/canonical_ir.py,sha256=5hLAhhSpXNOdZAvPt-J-rXccJE6M3A6gVhmhU0Tym6Y,29269
|
|
11
11
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
12
|
-
sourcecode/classifier.py,sha256=
|
|
12
|
+
sourcecode/classifier.py,sha256=9olDN5joeKHcwG9vf73X-t_son16hLVNib0rBbqk1vc,18865
|
|
13
13
|
sourcecode/cli.py,sha256=46Chp0JsEWD_j0HUWOx4tOhrsXSWgWV8JhL5G8SyW4I,322078
|
|
14
14
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
15
15
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
@@ -51,11 +51,12 @@ sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
|
51
51
|
sourcecode/prepare_context.py,sha256=PYygAKL_jwz8gGElgM9mroUSih18UKSSjv08ndfl4aA,222877
|
|
52
52
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
53
53
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
54
|
+
sourcecode/reconciliation.py,sha256=kW8PyB_R7J2Ig0N_QP9Jdm1fQXM1Ebf3PZbaTc80ULA,7941
|
|
54
55
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
55
56
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
56
57
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
57
58
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
58
|
-
sourcecode/repository_ir.py,sha256=
|
|
59
|
+
sourcecode/repository_ir.py,sha256=F6lRgPoP8oTtnGpRgjoLw-dlxZRXH7XOcehtcNwufEY,309550
|
|
59
60
|
sourcecode/ris.py,sha256=aG2D4159gtpg968yD3PylYbIXhGFOwIlFBI0DSr84yY,20412
|
|
60
61
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
61
62
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -63,18 +64,18 @@ sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
|
63
64
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
64
65
|
sourcecode/security_posture.py,sha256=CAJw4oJD0aAW2yRewvd_fonkzi9_HpUjBZlIDZle00s,26034
|
|
65
66
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
66
|
-
sourcecode/semantic_impact_engine.py,sha256=
|
|
67
|
+
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
67
68
|
sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
|
|
68
|
-
sourcecode/semantic_services.py,sha256=
|
|
69
|
+
sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
|
|
69
70
|
sourcecode/serializer.py,sha256=FmQo7eojA_z3ptLW-Vgze5mrEJ6FabcZqiwbQWWTgFI,130056
|
|
70
71
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
71
72
|
sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
|
|
72
|
-
sourcecode/spring_impact.py,sha256=
|
|
73
|
+
sourcecode/spring_impact.py,sha256=ayZtHZ2YCRSlZpKBACoM9M7tW0K1W_C0r8SGj3R-4vU,70603
|
|
73
74
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
74
75
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
75
76
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
76
77
|
sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
|
|
77
|
-
sourcecode/summarizer.py,sha256=
|
|
78
|
+
sourcecode/summarizer.py,sha256=BaBO0WKQi4fX2b2Xjip-GLEZwI3PYiV_U9jmBQlrfFI,25060
|
|
78
79
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
79
80
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
80
81
|
sourcecode/validation_inference.py,sha256=UtmYv-d4TEzT1ihGEipAhtXDrwq0XtVtjMpb3i7NsNc,10815
|
|
@@ -90,7 +91,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
90
91
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
91
92
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
92
93
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
93
|
-
sourcecode/detectors/java.py,sha256=
|
|
94
|
+
sourcecode/detectors/java.py,sha256=ut8czxGzhFUxRAS5LeG78wev8kJolC7LG8IESDJZTNk,42349
|
|
94
95
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
95
96
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
96
97
|
sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
|
|
@@ -136,8 +137,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
136
137
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
137
138
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
138
139
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
139
|
-
sourcecode-2.5.
|
|
140
|
-
sourcecode-2.5.
|
|
141
|
-
sourcecode-2.5.
|
|
142
|
-
sourcecode-2.5.
|
|
143
|
-
sourcecode-2.5.
|
|
140
|
+
sourcecode-2.5.4.dist-info/METADATA,sha256=vFyQq0a39iemrb04OqC6TwsnM_xA7j6PTBz_lbEccTM,10851
|
|
141
|
+
sourcecode-2.5.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
142
|
+
sourcecode-2.5.4.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
143
|
+
sourcecode-2.5.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
144
|
+
sourcecode-2.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|