sourcecode 2.1.0__py3-none-any.whl → 2.2.0__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/caller_metrics.py +42 -0
- sourcecode/cli.py +118 -29
- sourcecode/detectors/java.py +29 -1
- sourcecode/endpoint_metrics.py +42 -0
- sourcecode/explain.py +2 -0
- sourcecode/path_filters.py +2 -1
- sourcecode/repository_ir.py +50 -14
- sourcecode/semantic_integration_engine.py +18 -2
- sourcecode/serializer.py +23 -3
- sourcecode/spring_semantic.py +18 -5
- sourcecode/spring_tx_analyzer.py +21 -6
- {sourcecode-2.1.0.dist-info → sourcecode-2.2.0.dist-info}/METADATA +4 -4
- {sourcecode-2.1.0.dist-info → sourcecode-2.2.0.dist-info}/RECORD +17 -15
- {sourcecode-2.1.0.dist-info → sourcecode-2.2.0.dist-info}/WHEEL +0 -0
- {sourcecode-2.1.0.dist-info → sourcecode-2.2.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.1.0.dist-info → sourcecode-2.2.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Single source of truth for the caller / fan-in metric reconciliation note.
|
|
2
|
+
|
|
3
|
+
Three commands report a "how many things reference this class" number, each
|
|
4
|
+
derived from a different graph projection. Read side by side they can look
|
|
5
|
+
contradictory (e.g. 215 vs 103 for the same class) — they are not. They count
|
|
6
|
+
different populations:
|
|
7
|
+
|
|
8
|
+
modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count
|
|
9
|
+
|
|
10
|
+
- ``modernize.in_degree`` counts graph *edges* (all edge types, symbol level, not
|
|
11
|
+
deduplicated per class) — a blast-radius ranking weight, always the largest, and
|
|
12
|
+
not a count of classes.
|
|
13
|
+
- ``explain.incoming_callers`` and ``impact.stats.direct_caller_count`` both count
|
|
14
|
+
*distinct dependent classes*. They normally match and are always the same order
|
|
15
|
+
of magnitude, but can differ by a small margin because each includes a slightly
|
|
16
|
+
different edge set (import-only references, DI-interface resolution). The
|
|
17
|
+
truncated ``impact.direct_callers`` array is a display sample, not the count.
|
|
18
|
+
|
|
19
|
+
Empirically verified across repositories (Broadleaf ExtensionManager: 215 / 103 /
|
|
20
|
+
103; petclinic entities & repositories: equal; occasional margin-of-one divergence
|
|
21
|
+
on classes with import-only references). The strict-equality claim was deliberately
|
|
22
|
+
NOT made — the note asserts only the robust ordering and the "same distinct-class
|
|
23
|
+
metric" relationship.
|
|
24
|
+
|
|
25
|
+
Every command that emits one of these numbers references this same string so the
|
|
26
|
+
explanation can never drift or contradict itself between commands.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
CALLER_METRIC_RECONCILIATION: str = (
|
|
30
|
+
"Fan-in / caller counts differ across commands BY DESIGN and are not "
|
|
31
|
+
"contradictory. For the same class the expected relationship is: "
|
|
32
|
+
"modernize.in_degree >= explain.incoming_callers ~= impact.stats.direct_caller_count. "
|
|
33
|
+
"modernize.in_degree = raw count of ALL incoming graph edges (imports, injects, "
|
|
34
|
+
"extends/implements, references, annotations), counted at symbol level and NOT "
|
|
35
|
+
"deduplicated per class — a blast-radius ranking weight, not a count of classes; "
|
|
36
|
+
"it is always the largest of the three. explain.incoming_callers and "
|
|
37
|
+
"impact.stats.direct_caller_count both count DISTINCT dependent classes "
|
|
38
|
+
"(deduplicated to class level); they normally match but can differ by a small "
|
|
39
|
+
"margin because each includes a slightly different edge set (e.g. import-only "
|
|
40
|
+
"references or DI-interface resolution). The top-level impact.direct_callers array "
|
|
41
|
+
"may be truncated for output size — use stats.direct_caller_count for the true total."
|
|
42
|
+
)
|
sourcecode/cli.py
CHANGED
|
@@ -15,6 +15,7 @@ from sourcecode import __version__
|
|
|
15
15
|
from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
16
16
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
17
17
|
from sourcecode.progress import Progress
|
|
18
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
18
19
|
from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
|
|
19
20
|
|
|
20
21
|
|
|
@@ -700,7 +701,8 @@ _WELCOME_CMDS = (
|
|
|
700
701
|
|
|
701
702
|
def _print_welcome_plain(tier: str) -> None:
|
|
702
703
|
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
703
|
-
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
704
|
+
lines = ["", f" ASK Engine {__version__} · {tier} · CLI: ask",
|
|
705
|
+
" Actionable Software Knowledge Engine", "",
|
|
704
706
|
" AI coding-agent context, instant.", "", " Get started:"]
|
|
705
707
|
for cmd, desc in _WELCOME_CMDS:
|
|
706
708
|
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
@@ -747,8 +749,9 @@ def _print_welcome() -> None:
|
|
|
747
749
|
t.append(" ")
|
|
748
750
|
t.append(tier, style=tier_style)
|
|
749
751
|
t.append("\n")
|
|
750
|
-
# "ENGINE" label beneath the wordmark.
|
|
751
|
-
t.append(_WELCOME_ENGINE
|
|
752
|
+
# "ENGINE" label beneath the wordmark, with the acronym expansion.
|
|
753
|
+
t.append(_WELCOME_ENGINE, style="bold dim cyan")
|
|
754
|
+
t.append(" Actionable Software Knowledge\n", style="dim")
|
|
752
755
|
|
|
753
756
|
t.append("\nAI coding-agent context, instant.\n\n", style="white")
|
|
754
757
|
|
|
@@ -3911,7 +3914,12 @@ def endpoints_cmd(
|
|
|
3911
3914
|
)
|
|
3912
3915
|
raise typer.Exit(code=1)
|
|
3913
3916
|
|
|
3914
|
-
|
|
3917
|
+
_prog = Progress()
|
|
3918
|
+
_prog.start("scanning endpoints")
|
|
3919
|
+
try:
|
|
3920
|
+
data = _extract_java_endpoints(target)
|
|
3921
|
+
finally:
|
|
3922
|
+
_prog.finish()
|
|
3915
3923
|
|
|
3916
3924
|
# Update RIS api_surface section (non-fatal side-effect).
|
|
3917
3925
|
try:
|
|
@@ -4358,6 +4366,9 @@ def export_cmd(
|
|
|
4358
4366
|
)
|
|
4359
4367
|
raise typer.Exit(1)
|
|
4360
4368
|
|
|
4369
|
+
_prog = Progress()
|
|
4370
|
+
_prog.start("scanning repository")
|
|
4371
|
+
|
|
4361
4372
|
file_list = [
|
|
4362
4373
|
f for f in find_java_files(root)
|
|
4363
4374
|
if "/test/" not in f and "/tests/" not in f
|
|
@@ -4384,6 +4395,7 @@ def export_cmd(
|
|
|
4384
4395
|
_integrations,
|
|
4385
4396
|
endpoint_meta=_ep_data,
|
|
4386
4397
|
)
|
|
4398
|
+
_prog.finish()
|
|
4387
4399
|
output = _serialize_dict(data, format)
|
|
4388
4400
|
_emit_command_output(output, output_path, copy,
|
|
4389
4401
|
success_msg=f"C4 architecture export written to {output_path}")
|
|
@@ -4413,6 +4425,7 @@ def export_cmd(
|
|
|
4413
4425
|
ContextGraph.build(file_list, root)
|
|
4414
4426
|
).as_report(len(file_list))
|
|
4415
4427
|
|
|
4428
|
+
_prog.finish()
|
|
4416
4429
|
output = _serialize_dict(data, format)
|
|
4417
4430
|
_emit_command_output(output, output_path, copy,
|
|
4418
4431
|
success_msg=f"Export written to {output_path}")
|
|
@@ -4495,6 +4508,8 @@ def validation_cmd(
|
|
|
4495
4508
|
|
|
4496
4509
|
from sourcecode.context_graph import ContextGraph
|
|
4497
4510
|
from sourcecode.validation_surface import build_validation_surface
|
|
4511
|
+
_prog = Progress()
|
|
4512
|
+
_prog.start("mapping validation surface")
|
|
4498
4513
|
# Structural facts come from the ContextGraph — the single access layer.
|
|
4499
4514
|
_graph = ContextGraph.build_from_root(target)
|
|
4500
4515
|
data = build_validation_surface(target, graph=_graph)
|
|
@@ -4527,6 +4542,7 @@ def validation_cmd(
|
|
|
4527
4542
|
if _note:
|
|
4528
4543
|
data["note"] = _note
|
|
4529
4544
|
|
|
4545
|
+
_prog.finish()
|
|
4530
4546
|
output = _serialize_dict(data, format)
|
|
4531
4547
|
_summary = data.get("summary", {})
|
|
4532
4548
|
# Human-facing heads-up when the result is empty purely because no OpenAPI
|
|
@@ -4750,6 +4766,8 @@ def spring_audit_cmd(
|
|
|
4750
4766
|
success_msg=f"Spring audit written to {output_path}")
|
|
4751
4767
|
return
|
|
4752
4768
|
|
|
4769
|
+
_prog = Progress()
|
|
4770
|
+
_prog.start(f"auditing {len(file_list)} Java files")
|
|
4753
4771
|
cir = ContextGraph.build(file_list, target).cir
|
|
4754
4772
|
_model = SpringSemanticModel.build(cir)
|
|
4755
4773
|
|
|
@@ -4802,6 +4820,7 @@ def spring_audit_cmd(
|
|
|
4802
4820
|
except Exception:
|
|
4803
4821
|
pass
|
|
4804
4822
|
|
|
4823
|
+
_prog.finish()
|
|
4805
4824
|
if format == "github-comment":
|
|
4806
4825
|
output = _render_spring_audit_github_comment(combined, min_severity)
|
|
4807
4826
|
else:
|
|
@@ -4920,7 +4939,12 @@ def migrate_check_cmd(
|
|
|
4920
4939
|
|
|
4921
4940
|
_file_limitations: list[str] = []
|
|
4922
4941
|
file_list = find_java_files(target, limitations=_file_limitations)
|
|
4923
|
-
|
|
4942
|
+
_prog = Progress()
|
|
4943
|
+
_prog.start(f"checking migration ({len(file_list)} files)")
|
|
4944
|
+
try:
|
|
4945
|
+
report = run_migrate_check(file_list, target, min_severity=min_severity)
|
|
4946
|
+
finally:
|
|
4947
|
+
_prog.finish()
|
|
4924
4948
|
if _file_limitations:
|
|
4925
4949
|
report.limitations.extend(_file_limitations)
|
|
4926
4950
|
|
|
@@ -5079,6 +5103,8 @@ def impact_chain_cmd(
|
|
|
5079
5103
|
success_msg=f"Impact chain written to {output_path}")
|
|
5080
5104
|
return
|
|
5081
5105
|
|
|
5106
|
+
_prog = Progress()
|
|
5107
|
+
_prog.start(f"analyzing impact ({len(file_list)} files)")
|
|
5082
5108
|
cir = ContextGraph.build(file_list, target).cir
|
|
5083
5109
|
_model = SpringSemanticModel.build(cir)
|
|
5084
5110
|
|
|
@@ -5086,6 +5112,7 @@ def impact_chain_cmd(
|
|
|
5086
5112
|
from sourcecode.spring_event_topology import run_event_topology
|
|
5087
5113
|
evt_result = run_event_topology(cir, symbol, model=_model)
|
|
5088
5114
|
data = evt_result.to_dict()
|
|
5115
|
+
_prog.finish()
|
|
5089
5116
|
output = _serialize_dict(data, format)
|
|
5090
5117
|
_emit_command_output(
|
|
5091
5118
|
output, output_path, copy,
|
|
@@ -5101,6 +5128,7 @@ def impact_chain_cmd(
|
|
|
5101
5128
|
result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
|
|
5102
5129
|
|
|
5103
5130
|
data = result.to_dict()
|
|
5131
|
+
_prog.finish()
|
|
5104
5132
|
output = _serialize_dict(data, format)
|
|
5105
5133
|
_emit_command_output(
|
|
5106
5134
|
output, output_path, copy,
|
|
@@ -5229,9 +5257,14 @@ def pr_impact_cmd(
|
|
|
5229
5257
|
success_msg=f"PR impact report written to {output_path}")
|
|
5230
5258
|
return
|
|
5231
5259
|
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5260
|
+
_prog = Progress()
|
|
5261
|
+
_prog.start(f"analyzing PR impact ({len(file_list)} files)")
|
|
5262
|
+
try:
|
|
5263
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5264
|
+
model = SpringSemanticModel.build(cir)
|
|
5265
|
+
report = run_pr_impact(cir, changed_files, root=target, model=model)
|
|
5266
|
+
finally:
|
|
5267
|
+
_prog.finish()
|
|
5235
5268
|
|
|
5236
5269
|
output = _serialize_dict(report.to_dict(), "json") if format == "json" else report.render_text()
|
|
5237
5270
|
_emit_command_output(
|
|
@@ -5348,9 +5381,14 @@ def explain_cmd(
|
|
|
5348
5381
|
)
|
|
5349
5382
|
raise typer.Exit(code=1)
|
|
5350
5383
|
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5384
|
+
_prog = Progress()
|
|
5385
|
+
_prog.start(f"explaining {class_name} ({len(file_list)} files)")
|
|
5386
|
+
try:
|
|
5387
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
5388
|
+
model = SpringSemanticModel.build(cir)
|
|
5389
|
+
explanation = explain_class(class_name, cir, model)
|
|
5390
|
+
finally:
|
|
5391
|
+
_prog.finish()
|
|
5354
5392
|
|
|
5355
5393
|
if format == "json":
|
|
5356
5394
|
output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
|
|
@@ -5663,7 +5701,56 @@ def _partition_static_unreferenced(nodes: list[dict], root: Path) -> tuple[list[
|
|
|
5663
5701
|
if files_scanned >= _CONFIG_SCAN_MAX_FILES or not unresolved_simple:
|
|
5664
5702
|
break
|
|
5665
5703
|
|
|
5666
|
-
|
|
5704
|
+
# 3. Nested-type qualified-reference scan (SC-2 residual). The static call-graph
|
|
5705
|
+
# does not emit an edge for a nested-type member access in ordinary method-body
|
|
5706
|
+
# code — e.g. `PropertyType.AdminGroupPresentation.NAME` credits the enclosing
|
|
5707
|
+
# type, not the nested holder — so a referenced nested class can look
|
|
5708
|
+
# zero-degree. When a candidate is a nested type (its second-to-last FQN segment
|
|
5709
|
+
# is an UpperCamel enclosing type) and its qualified `Outer.Nested` form appears
|
|
5710
|
+
# in some OTHER Java source, it is statically referenced: neither dead nor
|
|
5711
|
+
# framework-dispatched. Structural, name-agnostic (pattern derived from the
|
|
5712
|
+
# candidate's own FQN).
|
|
5713
|
+
import re as _re_nested
|
|
5714
|
+
statically_referenced: set[str] = set()
|
|
5715
|
+
nested_patterns: dict[str, "tuple"] = {}
|
|
5716
|
+
for n in nodes:
|
|
5717
|
+
if n["fqn"] in dispatched_fqns:
|
|
5718
|
+
continue
|
|
5719
|
+
parts = (n.get("fqn") or "").split(".")
|
|
5720
|
+
if len(parts) >= 2 and parts[-2][:1].isupper():
|
|
5721
|
+
outer, nested = parts[-2], parts[-1]
|
|
5722
|
+
nested_patterns[n["fqn"]] = (
|
|
5723
|
+
_re_nested.compile(r"\b" + _re_nested.escape(outer) + r"\." + _re_nested.escape(nested) + r"\b"),
|
|
5724
|
+
n.get("source_file") or "",
|
|
5725
|
+
)
|
|
5726
|
+
if nested_patterns:
|
|
5727
|
+
files_scanned = 0
|
|
5728
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
5729
|
+
dirnames[:] = [d for d in dirnames
|
|
5730
|
+
if d not in {".git", "build", "out", "target", "node_modules", ".gradle"}]
|
|
5731
|
+
for fname in filenames:
|
|
5732
|
+
if not fname.endswith(".java"):
|
|
5733
|
+
continue
|
|
5734
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5735
|
+
break
|
|
5736
|
+
fpath = os.path.join(dirpath, fname)
|
|
5737
|
+
rel = os.path.relpath(fpath, root)
|
|
5738
|
+
try:
|
|
5739
|
+
with open(fpath, "r", encoding="utf-8", errors="replace") as fh:
|
|
5740
|
+
text = fh.read(_CONFIG_SCAN_MAX_BYTES)
|
|
5741
|
+
except OSError:
|
|
5742
|
+
continue
|
|
5743
|
+
files_scanned += 1
|
|
5744
|
+
for fqn, (pat, own) in nested_patterns.items():
|
|
5745
|
+
if fqn in statically_referenced or rel == own:
|
|
5746
|
+
continue
|
|
5747
|
+
if pat.search(text):
|
|
5748
|
+
statically_referenced.add(fqn)
|
|
5749
|
+
if files_scanned >= _CONFIG_SCAN_MAX_FILES or len(statically_referenced) == len(nested_patterns):
|
|
5750
|
+
break
|
|
5751
|
+
|
|
5752
|
+
unreferenced = [n for n in nodes
|
|
5753
|
+
if n["fqn"] not in dispatched_fqns and n["fqn"] not in statically_referenced]
|
|
5667
5754
|
dispatched = [n for n in nodes if n["fqn"] in dispatched_fqns]
|
|
5668
5755
|
return unreferenced, dispatched
|
|
5669
5756
|
|
|
@@ -5870,8 +5957,13 @@ def modernize_cmd(
|
|
|
5870
5957
|
"edge_count": _cnt,
|
|
5871
5958
|
"reverse_edge_count": _reverse,
|
|
5872
5959
|
# A mutual (cyclic) dependency is the actual "tangle" — both modules
|
|
5873
|
-
# reference each other, so neither can be extracted independently.
|
|
5960
|
+
# reference each other, so neither can be extracted independently. A
|
|
5961
|
+
# one-way (directional) dependency is normal layering, NOT a tangle:
|
|
5962
|
+
# TANGLE-1 (petclinic field test) surfaced owner→model / vet→model
|
|
5963
|
+
# (reverse_edge_count:0) being labelled "tangles" when they are plain
|
|
5964
|
+
# base-model usage. coupling_type states which it is, honestly.
|
|
5874
5965
|
"mutual": _reverse > 0,
|
|
5966
|
+
"coupling_type": "cyclic" if _reverse > 0 else "directional",
|
|
5875
5967
|
"example": _pair_example.get((_a, _b), ""),
|
|
5876
5968
|
})
|
|
5877
5969
|
# Surface mutual tangles first (highest decomposition risk), then by strength.
|
|
@@ -5947,17 +6039,10 @@ def modernize_cmd(
|
|
|
5947
6039
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
5948
6040
|
for n in coupling_nodes
|
|
5949
6041
|
],
|
|
5950
|
-
#
|
|
5951
|
-
# so it is not confused with `explain`'s caller
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
"(imports, injects, extends/implements, references, annotations), "
|
|
5955
|
-
"counted at symbol level. This is deliberately larger than and NOT "
|
|
5956
|
-
"equal to `ask explain`'s caller count, which reports DISTINCT "
|
|
5957
|
-
"dependent classes (DI dependents + reverse-call-graph callers, "
|
|
5958
|
-
"deduplicated to class level). Use in_degree for blast-radius ranking, "
|
|
5959
|
-
"explain's caller list for the concrete dependents to inspect."
|
|
5960
|
-
),
|
|
6042
|
+
# SC-6 (Broadleaf field test): make the in_degree metric self-describing
|
|
6043
|
+
# so it is not confused with `explain`'s / `impact`'s caller counts. All
|
|
6044
|
+
# three share one reconciliation note so they can never contradict.
|
|
6045
|
+
"high_coupling_nodes_note": CALLER_METRIC_RECONCILIATION,
|
|
5961
6046
|
"statically_unreferenced": [
|
|
5962
6047
|
{"fqn": n["fqn"], "type": n.get("type", ""), "role": n.get("role", "other")}
|
|
5963
6048
|
for n in dead_zones
|
|
@@ -5978,10 +6063,12 @@ def modernize_cmd(
|
|
|
5978
6063
|
"cross_module_tangles_note": (
|
|
5979
6064
|
"Directed inter-subsystem coupling measured from structural graph "
|
|
5980
6065
|
"edges (imports/injects/extends/implements/references/calls). "
|
|
5981
|
-
"edge_count = edges from_package→to_package
|
|
5982
|
-
"
|
|
5983
|
-
"independent module extraction
|
|
5984
|
-
"
|
|
6066
|
+
"edge_count = edges from_package→to_package. coupling_type=cyclic "
|
|
6067
|
+
"(mutual=true) is a bidirectional dependency — the actual tangle "
|
|
6068
|
+
"that blocks independent module extraction; coupling_type="
|
|
6069
|
+
"directional (reverse_edge_count=0) is a normal one-way layered "
|
|
6070
|
+
"dependency, listed for coupling strength, NOT a tangle. Empty "
|
|
6071
|
+
"means no cross-subsystem structural coupling was detected."
|
|
5985
6072
|
),
|
|
5986
6073
|
# BUG-05 fix: don't recommend "Start with hotspot_candidates" when the list is empty.
|
|
5987
6074
|
"recommendation": (
|
|
@@ -5992,7 +6079,9 @@ def modernize_cmd(
|
|
|
5992
6079
|
)
|
|
5993
6080
|
+ "statically_unreferenced lists classes with no Java callers — review "
|
|
5994
6081
|
+ "for framework dispatch (XML/reflection/SPI) before removing. "
|
|
5995
|
-
+ "
|
|
6082
|
+
+ "In cross_module_tangles, coupling_type=cyclic entries are the "
|
|
6083
|
+
+ "real tangles to decompose first; directional entries are normal "
|
|
6084
|
+
+ "layering ranked by coupling strength."
|
|
5996
6085
|
),
|
|
5997
6086
|
}
|
|
5998
6087
|
|
sourcecode/detectors/java.py
CHANGED
|
@@ -176,6 +176,28 @@ class JavaDetector(AbstractDetector):
|
|
|
176
176
|
frameworks = [f for f in frameworks if f.name != "MyBatis"]
|
|
177
177
|
|
|
178
178
|
entry_points = self._collect_entry_points(context)
|
|
179
|
+
|
|
180
|
+
# C1 (Eureka field test): a repo whose REST surface is JAX-RS / Jakarta
|
|
181
|
+
# REST — declared on the source via @Path/@GET, not through a web framework
|
|
182
|
+
# coordinate in the build manifest — must still surface that in the headline
|
|
183
|
+
# stack. Eureka wires JAX-RS through Jersey 1.x (com.sun.jersey) + jsr311-api
|
|
184
|
+
# + Guice, none of which the manifest-coordinate scan models, and its child
|
|
185
|
+
# build.gradle files are not scanned at all — yet it ships 18 @Path resource
|
|
186
|
+
# classes. Without this the headline read "Java library / package" while the
|
|
187
|
+
# endpoints command listed a full JAX-RS API. When the annotation scan found
|
|
188
|
+
# JAX-RS entry points and no Jakarta-EE framework was inferred from the
|
|
189
|
+
# manifest, surface it so the stack (and project_type) reflect the REST API
|
|
190
|
+
# the endpoints already prove. Structural (VAI): reasons from the JAX-RS
|
|
191
|
+
# entry points found in source, not from a vendor dependency-name allowlist.
|
|
192
|
+
# detected_via carries no path form, so it is treated as repo-wide evidence
|
|
193
|
+
# (never localized to an adapter submodule).
|
|
194
|
+
if not any(f.name == "Jakarta EE" for f in frameworks) and any(
|
|
195
|
+
ep.kind in ("jax_rs_controller", "jax_rs_provider") for ep in entry_points
|
|
196
|
+
):
|
|
197
|
+
frameworks.append(FrameworkDetection(
|
|
198
|
+
name="Jakarta EE", source="annotation", detected_via=["jax_rs_endpoints"],
|
|
199
|
+
))
|
|
200
|
+
|
|
179
201
|
transactional_classes = self._collect_transactional_classes(context, all_paths)
|
|
180
202
|
stack = StackDetection(
|
|
181
203
|
stack="java",
|
|
@@ -459,7 +481,13 @@ class JavaDetector(AbstractDetector):
|
|
|
459
481
|
# 1. @SpringBootApplication entry: Application.java / Main.java by name
|
|
460
482
|
# Exclude test trees: test helpers like AdminApplication.java in
|
|
461
483
|
# integration/src/test/java/ must not be treated as production entrypoints.
|
|
462
|
-
|
|
484
|
+
# B2 (openmrs field test): exclude not just src/test/ trees but whole
|
|
485
|
+
# test-harness MODULES whose sources sit under src/main — e.g. openmrs'
|
|
486
|
+
# `test-suite/module/omod/src/main/java/.../TestModuleController.java`, a
|
|
487
|
+
# scaffolding @Controller (`GET /module/testmodule/hello`) that a narrow
|
|
488
|
+
# src/test/ check misses, so it surfaced as a production entry point. The
|
|
489
|
+
# broader is_test_or_fixture_path catches test-suite / *-test / *-it modules.
|
|
490
|
+
from sourcecode.path_filters import is_test_or_fixture_path as _is_test_path
|
|
463
491
|
# BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
|
|
464
492
|
# not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
|
|
465
493
|
# `Application.java` model classes (no main(), no bootstrap annotation) in a
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Single source of truth for the endpoint / route-surface reconciliation note.
|
|
2
|
+
|
|
3
|
+
Three commands report an endpoint count, each from a different point in the
|
|
4
|
+
pipeline. Read side by side they look contradictory (e.g. 245 vs 225 vs 220 for
|
|
5
|
+
one repo) — they are not. Expected ordering for a single repo:
|
|
6
|
+
|
|
7
|
+
repo-ir.route_surface >= endpoints.total >= spring-audit.endpoints_analyzed
|
|
8
|
+
|
|
9
|
+
- ``repo-ir.route_surface`` is the RAW structural surface (every @RequestMapping /
|
|
10
|
+
@Path handler after inheritance projection and JAX-RS sub-resource-locator
|
|
11
|
+
composition). It still contains framework dynamic-admin routes whose path is a
|
|
12
|
+
Java FQN (e.g. ``/org.broadleafcommerce.core.search.domain.FieldImpl``), which
|
|
13
|
+
the ``endpoints`` command filters out — so it is NOT the canonical API surface.
|
|
14
|
+
- ``endpoints.total`` is the canonical REST/API surface (FQN-shaped framework
|
|
15
|
+
routes removed). This is the number to cite for "how many endpoints".
|
|
16
|
+
- ``spring-audit.endpoints_analyzed`` is that canonical surface further deduplicated
|
|
17
|
+
by (method, path, controller, handler) for security analysis.
|
|
18
|
+
|
|
19
|
+
The exact arithmetic between the three is intentionally NOT asserted (the pipelines
|
|
20
|
+
differ in dedup/expansion, so route_surface minus FQN-paths does not equal
|
|
21
|
+
endpoints.total). Only the robust ordering and the population of each are stated.
|
|
22
|
+
|
|
23
|
+
``spring-audit`` already documents its own count inline; this note gives repo-ir's
|
|
24
|
+
raw ``route_surface`` the same self-description so no reader mistakes it for the
|
|
25
|
+
canonical endpoint total.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
ENDPOINT_SURFACE_RECONCILIATION: str = (
|
|
29
|
+
"route_surface is the RAW structural route surface: every @RequestMapping/@Path "
|
|
30
|
+
"handler after inheritance projection and JAX-RS sub-resource-locator composition. "
|
|
31
|
+
"It is NOT the canonical API surface — it still contains framework dynamic-admin "
|
|
32
|
+
"routes whose path is a Java FQN (e.g. /org.broadleafcommerce...), which the "
|
|
33
|
+
"`endpoints` command excludes. Endpoint counts differ across commands BY DESIGN "
|
|
34
|
+
"and are not contradictory; expected ordering for one repo is "
|
|
35
|
+
"repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
|
|
36
|
+
"endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
|
|
37
|
+
"spring-audit.metadata.endpoints_analyzed (canonical surface further deduplicated "
|
|
38
|
+
"by (method, path, controller, handler) for security analysis). Cite "
|
|
39
|
+
"endpoints.total for the real API surface; the pipelines differ in dedup and "
|
|
40
|
+
"prefix expansion, so the three counts are related by this ordering, not by exact "
|
|
41
|
+
"arithmetic."
|
|
42
|
+
)
|
sourcecode/explain.py
CHANGED
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
from typing import TYPE_CHECKING, Optional
|
|
19
19
|
|
|
20
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
20
21
|
from sourcecode.fqn_utils import normalize_owner_fqn
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
@@ -121,6 +122,7 @@ class ClassExplanation:
|
|
|
121
122
|
"purpose": self.purpose,
|
|
122
123
|
"public_methods": self.public_methods,
|
|
123
124
|
"incoming_callers": self.incoming_callers,
|
|
125
|
+
"incoming_callers_note": CALLER_METRIC_RECONCILIATION,
|
|
124
126
|
"outgoing_deps": self.outgoing_deps,
|
|
125
127
|
"events_published": self.events_published,
|
|
126
128
|
"events_consumed": self.events_consumed,
|
sourcecode/path_filters.py
CHANGED
|
@@ -16,7 +16,8 @@ _TEST_SEGMENTS = frozenset({
|
|
|
16
16
|
# code lives under src/main (e.g. a Maven module that ships a test framework).
|
|
17
17
|
# A finding under one of these modules is test infrastructure, not the product.
|
|
18
18
|
_TEST_MODULE_SEGMENTS = frozenset({
|
|
19
|
-
"testsuite", "test-
|
|
19
|
+
"testsuite", "test-suite", "test-suites",
|
|
20
|
+
"test-framework", "testframework",
|
|
20
21
|
"integration-arquillian", "arquillian",
|
|
21
22
|
"test-utils", "test-util", "testutils",
|
|
22
23
|
"test-support", "testsupport",
|
sourcecode/repository_ir.py
CHANGED
|
@@ -22,6 +22,8 @@ from dataclasses import dataclass, field
|
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
from typing import Any, Iterable, Optional
|
|
24
24
|
|
|
25
|
+
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
26
|
+
from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
|
|
25
27
|
from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
|
|
26
28
|
from sourcecode.path_filters import is_test_path as _is_test_path
|
|
27
29
|
from sourcecode.security_config import (
|
|
@@ -3965,6 +3967,11 @@ def _assemble(
|
|
|
3965
3967
|
return {
|
|
3966
3968
|
**_base,
|
|
3967
3969
|
"route_surface": _route_surface,
|
|
3970
|
+
# SC-9 (Broadleaf field test): route_surface is the RAW surface (includes
|
|
3971
|
+
# framework dynamic-admin FQN-shaped routes the `endpoints` command filters).
|
|
3972
|
+
# Self-describe it so its count is never read as contradicting endpoints /
|
|
3973
|
+
# spring-audit totals when cross-referenced.
|
|
3974
|
+
"route_surface_note": ENDPOINT_SURFACE_RECONCILIATION,
|
|
3968
3975
|
"spring_events": _spring_events,
|
|
3969
3976
|
"analysis_gaps": _analysis_gaps,
|
|
3970
3977
|
"security_model": _security_model_asm,
|
|
@@ -4236,6 +4243,10 @@ def _build_route_surface(
|
|
|
4236
4243
|
"return_type": (sym.return_type.strip() if sym.return_type else "void"),
|
|
4237
4244
|
"stable_id": sym.stable_id,
|
|
4238
4245
|
"inheritance_depth": 0,
|
|
4246
|
+
# Provenance: which annotation family this modeled route came from.
|
|
4247
|
+
# Used downstream so a MODELED framework (JAX-RS) is never also reported
|
|
4248
|
+
# as an un-modeled "non_spring_rest_surface" (A2 field-validation fix).
|
|
4249
|
+
"framework": "jax_rs" if ann_name in _JAXRS_HTTP_ANNOTATIONS else "spring_mvc",
|
|
4239
4250
|
}
|
|
4240
4251
|
_route_entry["security_annotations"] = _sec
|
|
4241
4252
|
routes.append(_route_entry)
|
|
@@ -4721,11 +4732,16 @@ def apply_ir_size_limits(
|
|
|
4721
4732
|
if isinstance(raw_rs, list):
|
|
4722
4733
|
_rs_total = len(raw_rs)
|
|
4723
4734
|
out["route_surface"] = raw_rs[:50]
|
|
4735
|
+
# SC-9: always carry the raw-surface reconciliation note; prepend the
|
|
4736
|
+
# truncation notice when the list was capped.
|
|
4724
4737
|
if _rs_total > 50:
|
|
4725
4738
|
out["route_surface_note"] = (
|
|
4726
|
-
f"Showing 50/{_rs_total}
|
|
4727
|
-
"Remove --summary-only for full route surface."
|
|
4739
|
+
f"Showing 50/{_rs_total} route-surface entries. "
|
|
4740
|
+
"Remove --summary-only for the full route surface. "
|
|
4741
|
+
+ ENDPOINT_SURFACE_RECONCILIATION
|
|
4728
4742
|
)
|
|
4743
|
+
else:
|
|
4744
|
+
out["route_surface_note"] = ENDPOINT_SURFACE_RECONCILIATION
|
|
4729
4745
|
elif isinstance(raw_rs, dict):
|
|
4730
4746
|
# Legacy dict format with "endpoints" sub-key
|
|
4731
4747
|
raw_eps: list = raw_rs.get("endpoints") or []
|
|
@@ -5116,16 +5132,19 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5116
5132
|
# total false negative that also disables the `validation` command downstream.
|
|
5117
5133
|
# Detection is by SYNTACTIC SHAPE (no framework knowledge): an HTTP-verb method
|
|
5118
5134
|
# name, a first argument that is a string literal looking like a path (starts
|
|
5119
|
-
# with "/", may contain :param / {param}), AND a second argument
|
|
5120
|
-
#
|
|
5121
|
-
#
|
|
5122
|
-
#
|
|
5123
|
-
#
|
|
5124
|
-
#
|
|
5125
|
-
#
|
|
5135
|
+
# with "/", may contain :param / {param}), AND a second argument that is a
|
|
5136
|
+
# HANDLER, not a literal. Matches both bare `get(...)` (static-import / Spark
|
|
5137
|
+
# style) and `app.get(...)` (Javalin style); the lookbehind only rejects an
|
|
5138
|
+
# identifier char so `forget(` is not mistaken for `get(`.
|
|
5139
|
+
# B1 fix (field validation, openmrs FormUtil): a route handler is always a
|
|
5140
|
+
# method-ref / lambda / call / identifier — never a bare literal. The negative
|
|
5141
|
+
# lookahead after the comma rejects a literal second argument, so a plain map
|
|
5142
|
+
# write like `swapChars.put("/", "slash")` is no longer mistaken for a `PUT /`
|
|
5143
|
+
# route. Reported at confidence "medium" — an occasional flagged false positive
|
|
5144
|
+
# beats a total silent false negative.
|
|
5126
5145
|
_DSL_ROUTE_RE = _re.compile(
|
|
5127
5146
|
r'(?<![A-Za-z0-9_])(get|post|put|delete|patch|head|options)\s*\(\s*'
|
|
5128
|
-
r'"(/[^"\s]*)"\s*,'
|
|
5147
|
+
r'"(/[^"\s]*)"\s*,(?!\s*(?:["0-9]|true\b|false\b|null\b))'
|
|
5129
5148
|
)
|
|
5130
5149
|
_dsl_routes: list[dict] = []
|
|
5131
5150
|
|
|
@@ -5441,6 +5460,15 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5441
5460
|
if _webscript_descriptors:
|
|
5442
5461
|
_nonspring["webscripts"] += _webscript_descriptors
|
|
5443
5462
|
|
|
5463
|
+
# A2 fix (eureka/keycloak field validation): JAX-RS is MODELED by _build_route_surface
|
|
5464
|
+
# (routes carry framework="jax_rs"). When we actually modeled JAX-RS endpoints, it is
|
|
5465
|
+
# NOT an un-modeled "non_spring_rest_surface" — reporting it as such contradicted the
|
|
5466
|
+
# endpoints just emitted (e.g. keycloak's 689 JAX-RS routes labelled "not modeled").
|
|
5467
|
+
# Only genuinely un-modeled surfaces (WebScripts, Servlets, or JAX-RS that produced no
|
|
5468
|
+
# modeled route — client proxies / extraction gaps) remain here.
|
|
5469
|
+
if any(r.get("framework") == "jax_rs" for r in routes):
|
|
5470
|
+
_nonspring.pop("jax_rs", None)
|
|
5471
|
+
|
|
5444
5472
|
_nonspring_total = sum(_nonspring.values())
|
|
5445
5473
|
if _nonspring_total:
|
|
5446
5474
|
_frameworks = [k for k, v in _nonspring.items() if v]
|
|
@@ -5471,9 +5499,9 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
5471
5499
|
)
|
|
5472
5500
|
else:
|
|
5473
5501
|
_msg = (
|
|
5474
|
-
f"Additional REST surface
|
|
5475
|
-
f"
|
|
5476
|
-
f"
|
|
5502
|
+
f"Additional REST surface detected but NOT modeled here: {_fw_label}. "
|
|
5503
|
+
f"The endpoints above cover the annotation-modeled surface (Spring MVC and "
|
|
5504
|
+
f"JAX-RS); the {_fw_label} surface is additional and not included in the count."
|
|
5477
5505
|
)
|
|
5478
5506
|
result.setdefault("warnings", []).append(_msg)
|
|
5479
5507
|
|
|
@@ -6153,10 +6181,18 @@ def compute_blast_radius(
|
|
|
6153
6181
|
"no indirect callers reachable from sampled seeds (terminal sink or sparse graph). "
|
|
6154
6182
|
"Use a lower-fan-in entry point for full transitive traversal."
|
|
6155
6183
|
)
|
|
6184
|
+
# SC-6 (Broadleaf field test): the direct_callers array is a truncated display
|
|
6185
|
+
# sample. Disclose the cap and point to stats.direct_caller_count as the true
|
|
6186
|
+
# total (== explain.incoming_callers), then append the cross-command
|
|
6187
|
+
# reconciliation so len(direct_callers) is never misread as a contradiction.
|
|
6156
6188
|
if len(direct_callers) > 30:
|
|
6157
6189
|
out["direct_callers_note"] = (
|
|
6158
|
-
f"Showing 30/{n_direct} direct callers
|
|
6190
|
+
f"Showing 30/{n_direct} distinct direct callers; "
|
|
6191
|
+
f"stats.direct_caller_count = {n_direct} is the true total "
|
|
6192
|
+
f"(use --output for the full list). " + CALLER_METRIC_RECONCILIATION
|
|
6159
6193
|
)
|
|
6194
|
+
else:
|
|
6195
|
+
out["direct_callers_note"] = CALLER_METRIC_RECONCILIATION
|
|
6160
6196
|
if len(indirect_callers) > 50:
|
|
6161
6197
|
out["indirect_callers_note"] = (
|
|
6162
6198
|
f"Showing 50/{n_indirect} indirect callers. Use --output to inspect full IR."
|
|
@@ -179,7 +179,9 @@ class SemanticIntegrationEngine:
|
|
|
179
179
|
|
|
180
180
|
def detect(self) -> list[Integration]:
|
|
181
181
|
"""All outbound integrations recoverable from typed facts, deterministically
|
|
182
|
-
ordered. One record per (
|
|
182
|
+
ordered. One record per semantic point (kind + evidence location): a type that
|
|
183
|
+
touches several client libraries of the same kind counts once. Targets are
|
|
184
|
+
attributed by
|
|
183
185
|
annotation args (declarative) or by literal scheme (imperative), never by a
|
|
184
186
|
receiver variable name."""
|
|
185
187
|
per_type: dict[str, list[Integration]] = {}
|
|
@@ -231,7 +233,21 @@ class SemanticIntegrationEngine:
|
|
|
231
233
|
per_type[t.fqn] = list(recs.values())
|
|
232
234
|
|
|
233
235
|
out = [r for recs in per_type.values() for r in recs]
|
|
234
|
-
|
|
236
|
+
out.sort(key=lambda i: (i.kind, i.client, i.evidence))
|
|
237
|
+
# REG-1 (Broadleaf field test): one integration per semantic point =
|
|
238
|
+
# (kind, evidence location). A single type touching several client-library
|
|
239
|
+
# types of the SAME kind — e.g. spring-ldap + spring-security-ldap on one
|
|
240
|
+
# LdapUserDetailsMapper, or javamail + spring-mail on one MessageCreator —
|
|
241
|
+
# previously emitted one record per client label, double-counting the same
|
|
242
|
+
# integration. Collapse to a single record per (kind, evidence), preferring
|
|
243
|
+
# one that carries a resolved target; ties broken by the deterministic
|
|
244
|
+
# (kind, client, evidence) order established above.
|
|
245
|
+
deduped: dict[tuple, Integration] = {}
|
|
246
|
+
for r in out:
|
|
247
|
+
k = (r.kind, r.evidence)
|
|
248
|
+
if k not in deduped or (r.target and deduped[k].target is None):
|
|
249
|
+
deduped[k] = r
|
|
250
|
+
return list(deduped.values())
|
|
235
251
|
|
|
236
252
|
def _coverage(self, n_records: int, n_files: int) -> "tuple[str, str]":
|
|
237
253
|
"""Honest coverage signal (mirrors integration_detector): a low count on a
|
sourcecode/serializer.py
CHANGED
|
@@ -462,11 +462,31 @@ def _mybatis_pairing(sm: "SourceMap", *, full: bool = False) -> "Optional[dict[s
|
|
|
462
462
|
|
|
463
463
|
|
|
464
464
|
def _spring_boot_version(sm: "SourceMap") -> "Optional[str]":
|
|
465
|
-
"""Extract Spring Boot version from detected frameworks.
|
|
465
|
+
"""Extract Spring Boot version from detected frameworks.
|
|
466
|
+
|
|
467
|
+
A3 fix (field validation, Broadleaf): when Spring Boot IS detected but the
|
|
468
|
+
stack detector could not attach a version (e.g. the version lives in a
|
|
469
|
+
`<spring.boot.version>` pom property rather than spring-boot-starter-parent),
|
|
470
|
+
fall back to migrate-check's property-resolving detector so `--compact`
|
|
471
|
+
reports the SAME version migrate-check already knows — the engine must not
|
|
472
|
+
surface a fact in one command and hide it in another. Gated on Boot already
|
|
473
|
+
being detected, so no phantom version is ever introduced.
|
|
474
|
+
"""
|
|
475
|
+
_boot_detected = False
|
|
466
476
|
for s in sm.stacks:
|
|
467
477
|
for fw in s.frameworks:
|
|
468
|
-
if fw.name == "Spring Boot"
|
|
469
|
-
|
|
478
|
+
if fw.name == "Spring Boot":
|
|
479
|
+
if fw.version:
|
|
480
|
+
return fw.version
|
|
481
|
+
_boot_detected = True
|
|
482
|
+
if _boot_detected:
|
|
483
|
+
_root = getattr(sm.metadata, "analyzed_path", None) if sm.metadata else None
|
|
484
|
+
if _root:
|
|
485
|
+
try:
|
|
486
|
+
from sourcecode.migrate_check import _detect_spring_boot
|
|
487
|
+
return _detect_spring_boot(Path(_root), 0)[1]
|
|
488
|
+
except Exception:
|
|
489
|
+
return None
|
|
470
490
|
return None
|
|
471
491
|
|
|
472
492
|
|
sourcecode/spring_semantic.py
CHANGED
|
@@ -122,9 +122,17 @@ class TransactionBoundary:
|
|
|
122
122
|
class TransactionBoundaryIndex:
|
|
123
123
|
"""Index of all @Transactional boundaries in a repository."""
|
|
124
124
|
|
|
125
|
-
# FQN → boundary (both class-level and method-level)
|
|
125
|
+
# FQN → boundary (both class-level and method-level). Keyed by FQN for
|
|
126
|
+
# effective_boundary() call-graph lookups; OVERLOADED methods share an FQN
|
|
127
|
+
# (e.g. findAll() and findAll(Pageable) → Repo#findAll) so this dict collapses
|
|
128
|
+
# them — do NOT count from it (see all_declared / A1 field-validation fix).
|
|
126
129
|
by_symbol: dict[str, TransactionBoundary] = field(default_factory=dict)
|
|
127
130
|
|
|
131
|
+
# Every declared @Transactional boundary, one entry per annotation site.
|
|
132
|
+
# Unlike by_symbol this does NOT collapse overloaded methods, so it is the
|
|
133
|
+
# authoritative source for counts/stats.
|
|
134
|
+
all_declared: list[TransactionBoundary] = field(default_factory=list)
|
|
135
|
+
|
|
128
136
|
# class FQN → list of method-level boundaries declared on that class
|
|
129
137
|
by_class: dict[str, list[TransactionBoundary]] = field(default_factory=dict)
|
|
130
138
|
|
|
@@ -152,17 +160,19 @@ class TransactionBoundaryIndex:
|
|
|
152
160
|
return list(self.by_symbol.values())
|
|
153
161
|
|
|
154
162
|
def stats(self) -> dict:
|
|
163
|
+
# Count from all_declared (one entry per annotation site) so overloaded
|
|
164
|
+
# @Transactional methods are not collapsed by the FQN-keyed by_symbol.
|
|
155
165
|
n_class = len(self.class_level)
|
|
156
|
-
n_method = sum(1 for b in self.
|
|
166
|
+
n_method = sum(1 for b in self.all_declared if b.scope == "method")
|
|
157
167
|
propagations: dict[str, int] = {}
|
|
158
|
-
for b in self.
|
|
168
|
+
for b in self.all_declared:
|
|
159
169
|
propagations[b.propagation] = propagations.get(b.propagation, 0) + 1
|
|
160
170
|
return {
|
|
161
|
-
"total": len(self.
|
|
171
|
+
"total": len(self.all_declared),
|
|
162
172
|
"class_level": n_class,
|
|
163
173
|
"method_level": n_method,
|
|
164
174
|
"propagations": propagations,
|
|
165
|
-
"read_only_count": sum(1 for b in self.
|
|
175
|
+
"read_only_count": sum(1 for b in self.all_declared if b.read_only),
|
|
166
176
|
}
|
|
167
177
|
|
|
168
178
|
|
|
@@ -353,6 +363,9 @@ def build_tx_index(cir: "CanonicalRepositoryIR") -> TransactionBoundaryIndex:
|
|
|
353
363
|
boundary = _build_boundary(fqn, scope, raw_args, source_file, modifiers)
|
|
354
364
|
|
|
355
365
|
index.by_symbol[fqn] = boundary
|
|
366
|
+
# Authoritative per-site list — overloaded methods share an FQN and would
|
|
367
|
+
# collapse in by_symbol; all_declared keeps one entry per annotation site.
|
|
368
|
+
index.all_declared.append(boundary)
|
|
356
369
|
|
|
357
370
|
if scope == "class":
|
|
358
371
|
index.class_level[fqn] = boundary
|
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -194,8 +194,12 @@ class _TX001ProxyBypass:
|
|
|
194
194
|
model: Optional[SpringSemanticModel] = None,
|
|
195
195
|
) -> list[SpringFinding]:
|
|
196
196
|
findings: list[SpringFinding] = []
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
# A1-RESIDUAL: iterate all_declared (one entry per annotation site), not the
|
|
198
|
+
# FQN-keyed all_boundaries which collapses overloads — a public overload
|
|
199
|
+
# overwriting a private one would hide the proxy-bypass risk. Dedupe by
|
|
200
|
+
# finding id (symbol-keyed) so two risky overloads of the same FQN yield one.
|
|
201
|
+
_seen_ids: set[str] = set()
|
|
202
|
+
for boundary in tx_index.all_declared:
|
|
199
203
|
if boundary.scope != "method":
|
|
200
204
|
continue
|
|
201
205
|
if not boundary.is_proxy_bypass_risk:
|
|
@@ -209,10 +213,14 @@ class _TX001ProxyBypass:
|
|
|
209
213
|
if problematic_modifier == "private"
|
|
210
214
|
else "Spring CGLIB proxy cannot override final methods"
|
|
211
215
|
)
|
|
216
|
+
_fid = SpringFinding.make_id(self.pattern_id, boundary.symbol)
|
|
217
|
+
if _fid in _seen_ids:
|
|
218
|
+
continue
|
|
219
|
+
_seen_ids.add(_fid)
|
|
212
220
|
simple_name = boundary.symbol.rsplit(".", 1)[-1].replace("#", ".")
|
|
213
221
|
|
|
214
222
|
findings.append(SpringFinding(
|
|
215
|
-
id=
|
|
223
|
+
id=_fid,
|
|
216
224
|
pattern_id=self.pattern_id,
|
|
217
225
|
category="tx",
|
|
218
226
|
severity=self.severity,
|
|
@@ -550,8 +558,11 @@ class _TX005ExceptionSwallowing:
|
|
|
550
558
|
return []
|
|
551
559
|
|
|
552
560
|
findings: list[SpringFinding] = []
|
|
553
|
-
|
|
554
|
-
|
|
561
|
+
# A1-RESIDUAL: iterate all_declared so a write overload that swallows is not
|
|
562
|
+
# hidden by a read_only overload of the same FQN winning the by_symbol slot
|
|
563
|
+
# (the read_only gate below would skip the whole FQN). Dedupe by finding id.
|
|
564
|
+
_seen_ids: set[str] = set()
|
|
565
|
+
for boundary in tx_index.all_declared:
|
|
555
566
|
if boundary.scope != "method":
|
|
556
567
|
continue
|
|
557
568
|
if not boundary.source_file:
|
|
@@ -573,9 +584,13 @@ class _TX005ExceptionSwallowing:
|
|
|
573
584
|
if not self._has_swallowed_exception(source, boundary.symbol):
|
|
574
585
|
continue
|
|
575
586
|
|
|
587
|
+
_fid = SpringFinding.make_id(self.pattern_id, boundary.symbol)
|
|
588
|
+
if _fid in _seen_ids:
|
|
589
|
+
continue
|
|
590
|
+
_seen_ids.add(_fid)
|
|
576
591
|
simple_name = boundary.symbol.rsplit(".", 1)[-1].replace("#", ".")
|
|
577
592
|
findings.append(SpringFinding(
|
|
578
|
-
id=
|
|
593
|
+
id=_fid,
|
|
579
594
|
pattern_id=self.pattern_id,
|
|
580
595
|
category="tx",
|
|
581
596
|
severity=self.severity,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -38,11 +38,11 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
# ASK Engine
|
|
40
40
|
|
|
41
|
-
> **Persistent structural intelligence for AI coding agents
|
|
41
|
+
> **ASK — Actionable Software Knowledge.** Persistent structural intelligence for AI coding agents.
|
|
42
42
|
|
|
43
43
|
**Context · Impact · Migration · Architecture · Review — everything from one structural model.**
|
|
44
44
|
|
|
45
|
-

|
|
46
46
|

|
|
47
47
|
|
|
48
48
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -84,7 +84,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
84
84
|
# pip / pipx
|
|
85
85
|
pipx install sourcecode # or: pip install sourcecode
|
|
86
86
|
|
|
87
|
-
ask version # ask 2.
|
|
87
|
+
ask version # ask 2.2.0
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
> **Package vs. command.** The install package is named `sourcecode` this release
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=QOkIfC7KA2tM2ix9_wUgS6fHiERh2v_TTNhjw8RBb1o,308
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=6_wC4TYuBYxaqckZS0I1vBSMRPeH2vzlDB48gXwOOd4,46627
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=UaxmUU77oTlWxttMU3c6PxHrU-Q6q5N31TeU_NWWTdU,24297
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
|
|
6
6
|
sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
|
|
7
7
|
sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
|
|
8
|
+
sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,2574
|
|
8
9
|
sourcecode/canonical_ir.py,sha256=U69m8Vkr0p407xF1q5y1KguHXWEAubYw8NFHR9hDNJk,29178
|
|
9
10
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
10
11
|
sourcecode/classifier.py,sha256=YTTCoRdcLEFRVcql9Ow1dE7eYQj0jq2rgx32bRDnb1k,13852
|
|
11
|
-
sourcecode/cli.py,sha256=
|
|
12
|
+
sourcecode/cli.py,sha256=CKy6zdBfecnT6reYb749pLY4NqH1_Qy7c8DHnBWamrU,293593
|
|
12
13
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
13
14
|
sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
|
|
14
15
|
sourcecode/context_graph.py,sha256=8MVDu06bPhvRDTgUqWEvRME2fOw34bFLf4OUILZd13I,43527
|
|
@@ -21,11 +22,12 @@ sourcecode/dependency_analyzer.py,sha256=fQCaWQ7_BNcVgZv8gweJCwJ1CGlXsz8nw_tqN3a
|
|
|
21
22
|
sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
|
|
22
23
|
sourcecode/dynamic_argument_surface.py,sha256=iRzrJhs57UXrGvLhNPiTA-e_sdO6ka-wNqc0wIiegiM,2554
|
|
23
24
|
sourcecode/endpoint_literals.py,sha256=Qf4gTZzvNSFDGDuOvF0YRL9NvaYKKj24klhR5LIOaXc,3263
|
|
25
|
+
sourcecode/endpoint_metrics.py,sha256=wkAonXe34ItHlvlGur3YRFUrcZfg6GdiiwiE4bMa21Y,2590
|
|
24
26
|
sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
|
|
25
27
|
sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
|
|
26
28
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
27
29
|
sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpXxo,4277
|
|
28
|
-
sourcecode/explain.py,sha256=
|
|
30
|
+
sourcecode/explain.py,sha256=bo4sy8qkEstIF3sIefECKRYJBK97C6cMjyzRa8USCdI,21189
|
|
29
31
|
sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
|
|
30
32
|
sourcecode/file_classifier.py,sha256=A0fEABqtfVu1MfoaxnPAvGpZgneGgVXlJDhT74NYXxE,15314
|
|
31
33
|
sourcecode/format_contract.py,sha256=1cTNqwP8geA2hbQoBHUPgX3_vSh3l8guJT_jmgEnFF8,3466
|
|
@@ -40,7 +42,7 @@ sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7
|
|
|
40
42
|
sourcecode/migrate_check.py,sha256=HJhvHuvXDGTJSRQU9_AMx0L94h2TImC1DCGs3m3RTu0,108199
|
|
41
43
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
42
44
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
43
|
-
sourcecode/path_filters.py,sha256=
|
|
45
|
+
sourcecode/path_filters.py,sha256=T1y0SJsmUJBju2Hh_-z40kURAowUEq42FpPgWLbOaY0,6931
|
|
44
46
|
sourcecode/pr_comment_renderer.py,sha256=KmcjMruhR44gjzMDJwjBSkWP9QEvh8xWBLyxzxoRbj0,14542
|
|
45
47
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
46
48
|
sourcecode/prepare_context.py,sha256=PYygAKL_jwz8gGElgM9mroUSih18UKSSjv08ndfl4aA,222877
|
|
@@ -50,7 +52,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
50
52
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
51
53
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
52
54
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
53
|
-
sourcecode/repository_ir.py,sha256=
|
|
55
|
+
sourcecode/repository_ir.py,sha256=3DONydNhy-UU1R_1l-caePlBiUlZxkk9X5h92VXsQ34,284195
|
|
54
56
|
sourcecode/ris.py,sha256=aG2D4159gtpg968yD3PylYbIXhGFOwIlFBI0DSr84yY,20412
|
|
55
57
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
56
58
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -59,16 +61,16 @@ sourcecode/security_config.py,sha256=_m8oQAy2gP7Ho2I-IIMTFFjFVWbJIGlLEOiAWK2TDjs
|
|
|
59
61
|
sourcecode/security_posture.py,sha256=CAJw4oJD0aAW2yRewvd_fonkzi9_HpUjBZlIDZle00s,26034
|
|
60
62
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
61
63
|
sourcecode/semantic_impact_engine.py,sha256=HpgjenY6DLPHPomP7Hw850zA-EBokf1S4O2sh8NegdI,20351
|
|
62
|
-
sourcecode/semantic_integration_engine.py,sha256=
|
|
64
|
+
sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
|
|
63
65
|
sourcecode/semantic_services.py,sha256=AO_p7PRfjc-AUh6ThvLMfm4iUGi0issL_frCf8sxGlI,10637
|
|
64
|
-
sourcecode/serializer.py,sha256=
|
|
66
|
+
sourcecode/serializer.py,sha256=FmQo7eojA_z3ptLW-Vgze5mrEJ6FabcZqiwbQWWTgFI,130056
|
|
65
67
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
66
68
|
sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
|
|
67
69
|
sourcecode/spring_impact.py,sha256=1Eu1vkdwTVsw92iBEJDe_i_FaSf4uGH9Rb0eSgIAAZc,57542
|
|
68
70
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
69
71
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
70
|
-
sourcecode/spring_semantic.py,sha256=
|
|
71
|
-
sourcecode/spring_tx_analyzer.py,sha256=
|
|
72
|
+
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
73
|
+
sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
|
|
72
74
|
sourcecode/summarizer.py,sha256=ByfFxIGVRrGrf3paQPuDOg1rVj78LyzXN_EXBkfbCLU,23582
|
|
73
75
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
74
76
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
@@ -85,7 +87,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
85
87
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
86
88
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
87
89
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
88
|
-
sourcecode/detectors/java.py,sha256=
|
|
90
|
+
sourcecode/detectors/java.py,sha256=rTeVIKIliElE1yPdIkll1pKqDUzXD1EFFJdXxp24gr4,41005
|
|
89
91
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
90
92
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
91
93
|
sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
|
|
@@ -113,8 +115,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
113
115
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
114
116
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
115
117
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
116
|
-
sourcecode-2.
|
|
117
|
-
sourcecode-2.
|
|
118
|
-
sourcecode-2.
|
|
119
|
-
sourcecode-2.
|
|
120
|
-
sourcecode-2.
|
|
118
|
+
sourcecode-2.2.0.dist-info/METADATA,sha256=GWUjIL4RNPYx8cMKSRHxzELL49K0anIx6qH8aHAsB3I,10837
|
|
119
|
+
sourcecode-2.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
120
|
+
sourcecode-2.2.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
121
|
+
sourcecode-2.2.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
122
|
+
sourcecode-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|