sourcecode 3.1.0__py3-none-any.whl → 3.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.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/cli.py +128 -9
- sourcecode/detectors/java.py +10 -5
- sourcecode/integration_coordinates.py +175 -0
- sourcecode/mcp/registry.py +3 -1
- sourcecode/mcp/server.py +5 -2
- sourcecode/migrate_check.py +22 -1
- sourcecode/migration_blast.py +232 -0
- sourcecode/repository_ir.py +25 -17
- sourcecode/retrieval/steps_graph.py +10 -8
- sourcecode/retrieval/steps_intf.py +23 -1
- sourcecode/security_posture.py +165 -19
- sourcecode/semantic_integration_engine.py +54 -2
- sourcecode/spring_impact.py +124 -7
- sourcecode/spring_semantic.py +56 -4
- {sourcecode-3.1.0.dist-info → sourcecode-3.2.0.dist-info}/METADATA +3 -3
- {sourcecode-3.1.0.dist-info → sourcecode-3.2.0.dist-info}/RECORD +20 -18
- {sourcecode-3.1.0.dist-info → sourcecode-3.2.0.dist-info}/WHEEL +0 -0
- {sourcecode-3.1.0.dist-info → sourcecode-3.2.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-3.1.0.dist-info → sourcecode-3.2.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -4084,7 +4084,11 @@ def impact_cmd(
|
|
|
4084
4084
|
- direct_callers — classes that directly call or depend on the target
|
|
4085
4085
|
- indirect_callers — transitive callers (BFS, bounded by --depth)
|
|
4086
4086
|
- endpoints_affected — HTTP endpoints that transitively depend on the target
|
|
4087
|
-
- transactional_boundaries_touched — @Transactional
|
|
4087
|
+
- transactional_boundaries_touched — @Transactional declarations on the target
|
|
4088
|
+
and on everything that calls it
|
|
4089
|
+
- transactional_boundaries_reaching — the callers-only subset: what would run
|
|
4090
|
+
this change inside a transaction (the figure
|
|
4091
|
+
`ask retrieve transactions-reaching` reports)
|
|
4088
4092
|
- risk_score / risk_level — quantified change risk
|
|
4089
4093
|
|
|
4090
4094
|
\b
|
|
@@ -4714,7 +4718,10 @@ def export_cmd(
|
|
|
4714
4718
|
),
|
|
4715
4719
|
integrations: bool = typer.Option(
|
|
4716
4720
|
False, "--integrations",
|
|
4717
|
-
help=
|
|
4721
|
+
help=(
|
|
4722
|
+
"Detect outbound integrations (HTTP/LDAP/JMS clients) with file:line "
|
|
4723
|
+
"evidence, plus the client libraries the build declares."
|
|
4724
|
+
),
|
|
4718
4725
|
),
|
|
4719
4726
|
c4: bool = typer.Option(
|
|
4720
4727
|
False, "--c4",
|
|
@@ -4742,6 +4749,11 @@ def export_cmd(
|
|
|
4742
4749
|
rolled up from class-level relation edges.
|
|
4743
4750
|
--integrations Outbound integrations (RestTemplate/WebClient/Feign/LDAP/JMS)
|
|
4744
4751
|
with file:line evidence — external-system dependency arrows.
|
|
4752
|
+
Also lists the client libraries the BUILD declares
|
|
4753
|
+
(declared_clients) and which of those no source construct
|
|
4754
|
+
uses — a config-wired client has no construct to find, so a
|
|
4755
|
+
count of 0 is never left standing alone against a client
|
|
4756
|
+
library named in the stack block.
|
|
4745
4757
|
--c4 Unified architecture document mapped onto the open C4 model
|
|
4746
4758
|
(context/containers/components/code) + an API surface and a
|
|
4747
4759
|
per-directory content-hash manifest for incremental consumers.
|
|
@@ -4792,9 +4804,12 @@ def export_cmd(
|
|
|
4792
4804
|
graph = ir.get("graph", {})
|
|
4793
4805
|
_ep_data = extract_java_endpoints(root)
|
|
4794
4806
|
endpoints = _ep_data.get("endpoints", [])
|
|
4807
|
+
from sourcecode.integration_coordinates import declared_integration_coordinates
|
|
4795
4808
|
_integrations = SemanticIntegrationEngine(
|
|
4796
4809
|
ContextGraph.build(file_list, root)
|
|
4797
|
-
).as_report(
|
|
4810
|
+
).as_report(
|
|
4811
|
+
len(file_list), declared=declared_integration_coordinates(root),
|
|
4812
|
+
)
|
|
4798
4813
|
data = _build_c4_export(
|
|
4799
4814
|
root,
|
|
4800
4815
|
file_list,
|
|
@@ -4831,9 +4846,12 @@ def export_cmd(
|
|
|
4831
4846
|
if integrations:
|
|
4832
4847
|
from sourcecode.context_graph import ContextGraph
|
|
4833
4848
|
from sourcecode.semantic_integration_engine import SemanticIntegrationEngine
|
|
4849
|
+
from sourcecode.integration_coordinates import declared_integration_coordinates
|
|
4834
4850
|
data["integrations"] = SemanticIntegrationEngine(
|
|
4835
4851
|
ContextGraph.build(file_list, root)
|
|
4836
|
-
).as_report(
|
|
4852
|
+
).as_report(
|
|
4853
|
+
len(file_list), declared=declared_integration_coordinates(root),
|
|
4854
|
+
)
|
|
4837
4855
|
|
|
4838
4856
|
_prog.finish()
|
|
4839
4857
|
output = _serialize_dict(data, format)
|
|
@@ -5684,19 +5702,35 @@ def _render_gate_coverage_section(result: "SpringAuditResult") -> list[str]: #
|
|
|
5684
5702
|
gc = (result.security_posture or {}).get("gate_coverage")
|
|
5685
5703
|
if not gc:
|
|
5686
5704
|
return []
|
|
5687
|
-
not_covered = gc.get("not_carrying_gate", 0)
|
|
5705
|
+
not_covered = gc.get("endpoints_not_carrying_gate", gc.get("not_carrying_gate", 0))
|
|
5688
5706
|
gates = ", ".join(f"`{g}`" for g in gc.get("gate_annotations", [])) or "the detected gate"
|
|
5689
|
-
|
|
5707
|
+
# One unit, named: these are endpoints, not handler methods. The two counts differ
|
|
5708
|
+
# (one method can serve several mappings) and must never be summed together.
|
|
5709
|
+
total = gc.get("endpoints_total", gc.get("total_controller_handlers", 0))
|
|
5690
5710
|
lines: list[str] = ["", "---", ""]
|
|
5691
5711
|
if not_covered == 0:
|
|
5692
|
-
|
|
5712
|
+
_gated = gc.get("endpoints_carrying_gate", total)
|
|
5713
|
+
_std = gc.get("endpoints_standard_guarded", 0)
|
|
5714
|
+
if _std:
|
|
5715
|
+
lines.append(
|
|
5716
|
+
f"✅ **Gate coverage** — all {total} endpoints are guarded: {_gated} carry "
|
|
5717
|
+
f"{gates}, {_std} rely on a standard guard."
|
|
5718
|
+
)
|
|
5719
|
+
else:
|
|
5720
|
+
lines.append(f"✅ **Gate coverage** — all {total} endpoints carry {gates}.")
|
|
5693
5721
|
return lines
|
|
5694
5722
|
|
|
5695
5723
|
covered = gc.get("possibly_filter_covered", 0)
|
|
5724
|
+
_standard = gc.get("endpoints_standard_guarded", 0)
|
|
5696
5725
|
lines.append(
|
|
5697
|
-
f"🔓 **Gate coverage** — {not_covered} of {total}
|
|
5726
|
+
f"🔓 **Gate coverage** — {not_covered} of {total} endpoints do not carry "
|
|
5698
5727
|
f"{gates}."
|
|
5699
5728
|
)
|
|
5729
|
+
if _standard:
|
|
5730
|
+
lines.append(
|
|
5731
|
+
f"_{gc.get('endpoints_carrying_gate', 0)} carry the gate, {_standard} rely on a "
|
|
5732
|
+
f"standard guard, {not_covered} on neither — the three sum to {total}._"
|
|
5733
|
+
)
|
|
5700
5734
|
if gc.get("reconstructed_filter_patterns"):
|
|
5701
5735
|
lines.append(
|
|
5702
5736
|
f"_{covered} of those match a reconstructed servlet filter pattern "
|
|
@@ -6201,6 +6235,65 @@ def posture_cmd(
|
|
|
6201
6235
|
# ── Spring Boot Migration Check ───────────────────────────────────────────────
|
|
6202
6236
|
|
|
6203
6237
|
|
|
6238
|
+
def _migration_blast_radius(
|
|
6239
|
+
target: Path,
|
|
6240
|
+
file_list: list[str],
|
|
6241
|
+
report: "object",
|
|
6242
|
+
depth: int,
|
|
6243
|
+
) -> dict:
|
|
6244
|
+
"""Resolve the regression scope behind the migration findings (--blast-radius).
|
|
6245
|
+
|
|
6246
|
+
Reuses the shared context-cache CIR — the same one `impact-chain`, `explain` and
|
|
6247
|
+
`cache warm` use — so a warmed repository skips the Java parse. Every failure mode
|
|
6248
|
+
degrades to a block that says what could not be computed; it never fabricates reach
|
|
6249
|
+
and never breaks the scan that already succeeded.
|
|
6250
|
+
"""
|
|
6251
|
+
from sourcecode.migration_blast import compute_migration_blast, SCHEMA_VERSION
|
|
6252
|
+
|
|
6253
|
+
_depth = max(1, min(int(depth), 8))
|
|
6254
|
+
findings = list(getattr(report, "findings", []) or [])
|
|
6255
|
+
|
|
6256
|
+
def _degraded(reason: str) -> dict:
|
|
6257
|
+
return {
|
|
6258
|
+
"schema_version": SCHEMA_VERSION,
|
|
6259
|
+
"depth": _depth,
|
|
6260
|
+
"status": "unavailable",
|
|
6261
|
+
"endpoints_in_repo": 0,
|
|
6262
|
+
"endpoints_reached": 0,
|
|
6263
|
+
"request_surface_share": None,
|
|
6264
|
+
"files_analyzed": 0,
|
|
6265
|
+
"files_on_request_path": 0,
|
|
6266
|
+
"files_off_request_path": 0,
|
|
6267
|
+
"files_unresolved": [],
|
|
6268
|
+
"files": [],
|
|
6269
|
+
"files_omitted": 0,
|
|
6270
|
+
"notes": [reason],
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
if not file_list:
|
|
6274
|
+
return _degraded(
|
|
6275
|
+
"No Java sources in this repository — there is no call graph to traverse, "
|
|
6276
|
+
"so no regression scope can be attributed."
|
|
6277
|
+
)
|
|
6278
|
+
|
|
6279
|
+
try:
|
|
6280
|
+
from sourcecode import context_cache as _ctxcache
|
|
6281
|
+
from sourcecode.context_graph import ContextGraph
|
|
6282
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
6283
|
+
|
|
6284
|
+
try:
|
|
6285
|
+
cir, _ = _ctxcache.get_or_build_cir(_resolve_repo_root(target), target, file_list)
|
|
6286
|
+
except Exception:
|
|
6287
|
+
cir = ContextGraph.build(file_list, target).cir
|
|
6288
|
+
model = SpringSemanticModel.build(cir)
|
|
6289
|
+
return compute_migration_blast(cir, model, findings, depth=_depth)
|
|
6290
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
6291
|
+
return _degraded(
|
|
6292
|
+
f"Blast radius unavailable: {type(exc).__name__} while building the "
|
|
6293
|
+
"repository IR. The migration findings above are unaffected."
|
|
6294
|
+
)
|
|
6295
|
+
|
|
6296
|
+
|
|
6204
6297
|
@app.command("migrate-check")
|
|
6205
6298
|
def migrate_check_cmd(
|
|
6206
6299
|
path: Path = typer.Argument(
|
|
@@ -6265,6 +6358,18 @@ def migrate_check_cmd(
|
|
|
6265
6358
|
False, "--force",
|
|
6266
6359
|
help="Emit to stdout even when the report exceeds the output-size guard.",
|
|
6267
6360
|
),
|
|
6361
|
+
blast_radius: bool = typer.Option(
|
|
6362
|
+
False, "--blast-radius",
|
|
6363
|
+
help=(
|
|
6364
|
+
"Rank affected product files by the HTTP endpoints whose call path runs "
|
|
6365
|
+
"through them (regression scope), using the same caller traversal as "
|
|
6366
|
+
"impact-chain. Builds the repository IR — slower than a plain scan."
|
|
6367
|
+
),
|
|
6368
|
+
),
|
|
6369
|
+
depth: int = typer.Option(
|
|
6370
|
+
4, "--depth",
|
|
6371
|
+
help="Caller BFS depth for --blast-radius (1-8, default: 4).",
|
|
6372
|
+
),
|
|
6268
6373
|
) -> None:
|
|
6269
6374
|
"""Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
|
|
6270
6375
|
|
|
@@ -6298,6 +6403,16 @@ def migrate_check_cmd(
|
|
|
6298
6403
|
ask migrate-check . --output migration.json
|
|
6299
6404
|
ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
|
|
6300
6405
|
ask migrate-check . --trend days-remaining over time
|
|
6406
|
+
ask migrate-check . --blast-radius endpoints behind each blocker
|
|
6407
|
+
|
|
6408
|
+
\b
|
|
6409
|
+
Blast radius (--blast-radius):
|
|
6410
|
+
Ranks the affected product files by the HTTP endpoints whose call path runs
|
|
6411
|
+
through them — the re-test plan, ordered by regression scope. Reach is never
|
|
6412
|
+
build breakage: a javax→jakarta finding fails compilation whether or not an
|
|
6413
|
+
endpoint reaches it. A file whose types the IR could not resolve is reported
|
|
6414
|
+
unresolved (reach unknown, never zero), and a repository with no endpoints in
|
|
6415
|
+
the IR gets a null share with the reason stated instead of a 0.0.
|
|
6301
6416
|
|
|
6302
6417
|
\b
|
|
6303
6418
|
Readiness time series:
|
|
@@ -6366,6 +6481,9 @@ def migrate_check_cmd(
|
|
|
6366
6481
|
if _file_limitations:
|
|
6367
6482
|
report.limitations.extend(_file_limitations)
|
|
6368
6483
|
|
|
6484
|
+
if blast_radius:
|
|
6485
|
+
report.blast_radius = _migration_blast_radius(target, file_list, report, depth)
|
|
6486
|
+
|
|
6369
6487
|
if format == "text":
|
|
6370
6488
|
output = report.to_text(min_severity=min_severity)
|
|
6371
6489
|
else:
|
|
@@ -6513,7 +6631,8 @@ def impact_chain_cmd(
|
|
|
6513
6631
|
- indirect_callers — transitive callers (BFS up to --depth hops)
|
|
6514
6632
|
- endpoints_affected — HTTP endpoints reachable through the call chain
|
|
6515
6633
|
- transaction_boundary — @Transactional semantics on the target (if any)
|
|
6516
|
-
- security_surfaces — per-endpoint security
|
|
6634
|
+
- security_surfaces — per-endpoint security verdict + confidence
|
|
6635
|
+
(security_posture authority) + declared policy + SEC findings
|
|
6517
6636
|
- impact_findings — TX/SEC audit findings touching the call chain
|
|
6518
6637
|
- risk_level — critical | high | medium | low
|
|
6519
6638
|
|
sourcecode/detectors/java.py
CHANGED
|
@@ -15,6 +15,7 @@ from sourcecode.detectors.parsers import (
|
|
|
15
15
|
substitute_maven_properties as _resolve_maven_property,
|
|
16
16
|
unique_strings,
|
|
17
17
|
)
|
|
18
|
+
from sourcecode.integration_coordinates import coordinates_for_kind
|
|
18
19
|
from sourcecode.schema import FrameworkDetection
|
|
19
20
|
from sourcecode.tree_utils import flatten_file_tree
|
|
20
21
|
|
|
@@ -488,11 +489,15 @@ class JavaDetector(AbstractDetector):
|
|
|
488
489
|
if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
|
|
489
490
|
frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))
|
|
490
491
|
# The Boot starter names neither of the module coordinates, so a Boot app
|
|
491
|
-
# wiring LDAP the standard way declared no LDAP framework at all.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
# wiring LDAP the standard way declared no LDAP framework at all. The coordinate
|
|
493
|
+
# list lives in `integration_coordinates` — the same set `export --integrations`
|
|
494
|
+
# reads, so this stack block and that report can never recognize different
|
|
495
|
+
# libraries for the same external system (ADR-0008 R12).
|
|
496
|
+
# Only the Spring-family coordinates may carry the "Spring LDAP" label — the
|
|
497
|
+
# authority also knows non-Spring directory clients, and naming the wrong library
|
|
498
|
+
# would trade one incoherence for another.
|
|
499
|
+
if any(
|
|
500
|
+
c in text for c in coordinates_for_kind("ldap") if c.startswith("spring-")
|
|
496
501
|
):
|
|
497
502
|
frameworks.append(FrameworkDetection(name="Spring LDAP", source=source))
|
|
498
503
|
if "spring-aspects" in text or "spring-aop" in text:
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""integration_coordinates.py — the declared-client signal for outbound integrations.
|
|
2
|
+
|
|
3
|
+
THE authority for one question: *which outbound-integration client libraries does this
|
|
4
|
+
repository's build declare?* (ADR-0008 R1). It is a different question from "which client
|
|
5
|
+
constructs does the source contain" — that one belongs to
|
|
6
|
+
:mod:`sourcecode.semantic_integration_engine` — and the two answers only conflict when a
|
|
7
|
+
document reports one and stays silent about the other.
|
|
8
|
+
|
|
9
|
+
That is exactly what the field reported, twice, two majors apart: `export --integrations`
|
|
10
|
+
answered `count: 0` on a repository whose stack block listed **Spring LDAP**. Reproduced on
|
|
11
|
+
a Boot app that declares `spring-boot-starter-data-ldap` and configures
|
|
12
|
+
`spring.ldap.urls` in YAML: the directory is wired entirely by properties, so no LDAP
|
|
13
|
+
client type is ever imported and the construct scan is *right* to find nothing — but the
|
|
14
|
+
document as a whole then said the system has no outbound integrations while its own stack
|
|
15
|
+
block said it talks to a directory.
|
|
16
|
+
|
|
17
|
+
A declared coordinate is **evidence of a declared client, never an observed integration**
|
|
18
|
+
(the same discipline that stopped a `org.mybatis` coordinate from being reported as MyBatis
|
|
19
|
+
usage in `detectors/java.py`). This module answers what the build declares; the engine
|
|
20
|
+
answers what the code does; the payload states both and says which is which.
|
|
21
|
+
|
|
22
|
+
VAI (see `security_posture` §0.5): the table below lists **published open-source artifact
|
|
23
|
+
coordinates** — the identity of a library, matched literally, the same basis as the engine's
|
|
24
|
+
published client-type table. No proprietary, client or convention name appears here, and no
|
|
25
|
+
predicate branches on a repository's own naming.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import re
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Iterable, Optional
|
|
33
|
+
|
|
34
|
+
# Build-manifest artifact coordinate → the outbound-integration kind it declares.
|
|
35
|
+
# Kinds are exactly the engine's kinds, so declared and observed answers are comparable.
|
|
36
|
+
# Only coordinates whose sole purpose is to talk to an external system are listed: a
|
|
37
|
+
# driver-or-anything coordinate that a repository might carry for unrelated reasons has no
|
|
38
|
+
# place here, because a declaration nobody can act on is noise, not evidence.
|
|
39
|
+
_COORDINATE_KIND: "dict[str, str]" = {
|
|
40
|
+
# Directory (LDAP)
|
|
41
|
+
"spring-ldap-core": "ldap",
|
|
42
|
+
"spring-security-ldap": "ldap",
|
|
43
|
+
"spring-boot-starter-data-ldap": "ldap",
|
|
44
|
+
"spring-boot-starter-ldap": "ldap",
|
|
45
|
+
"unboundid-ldapsdk": "ldap",
|
|
46
|
+
# Kafka
|
|
47
|
+
"spring-kafka": "kafka",
|
|
48
|
+
"kafka-clients": "kafka",
|
|
49
|
+
"spring-cloud-stream-binder-kafka": "kafka",
|
|
50
|
+
# RabbitMQ / AMQP
|
|
51
|
+
"spring-boot-starter-amqp": "rabbitmq",
|
|
52
|
+
"spring-rabbit": "rabbitmq",
|
|
53
|
+
"amqp-client": "rabbitmq",
|
|
54
|
+
# JMS brokers
|
|
55
|
+
"spring-boot-starter-activemq": "jms",
|
|
56
|
+
"spring-boot-starter-artemis": "jms",
|
|
57
|
+
"activemq-client": "jms",
|
|
58
|
+
"activemq-broker": "jms",
|
|
59
|
+
"artemis-jms-client": "jms",
|
|
60
|
+
"spring-jms": "jms",
|
|
61
|
+
# Redis
|
|
62
|
+
"spring-boot-starter-data-redis": "redis",
|
|
63
|
+
"spring-data-redis": "redis",
|
|
64
|
+
"lettuce-core": "redis",
|
|
65
|
+
"jedis": "redis",
|
|
66
|
+
# Elasticsearch
|
|
67
|
+
"spring-boot-starter-data-elasticsearch": "elasticsearch",
|
|
68
|
+
"elasticsearch-rest-high-level-client": "elasticsearch",
|
|
69
|
+
"elasticsearch-java": "elasticsearch",
|
|
70
|
+
# Mail (SMTP)
|
|
71
|
+
"spring-boot-starter-mail": "smtp",
|
|
72
|
+
# Declarative HTTP clients
|
|
73
|
+
"spring-cloud-starter-openfeign": "http",
|
|
74
|
+
"feign-core": "http",
|
|
75
|
+
"okhttp": "http",
|
|
76
|
+
# SOAP
|
|
77
|
+
"spring-ws-core": "soap",
|
|
78
|
+
"spring-boot-starter-web-services": "soap",
|
|
79
|
+
"cxf-rt-frontend-jaxws": "soap",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Build manifests scanned. Text-level, like the framework-coordinate scan in
|
|
83
|
+
# `detectors/java.py` — a build file is configuration, not source.
|
|
84
|
+
_MANIFEST_NAMES: frozenset[str] = frozenset({
|
|
85
|
+
"pom.xml", "build.gradle", "build.gradle.kts",
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Directories that never hold a production build manifest.
|
|
89
|
+
_SKIP_DIRS: frozenset[str] = frozenset({
|
|
90
|
+
".git", ".idea", "target", "build", "node_modules", ".sourcecode-cache",
|
|
91
|
+
".gradle", "out", "dist",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# A repository with thousands of modules is real (OFBiz, Broadleaf); an unbounded walk is
|
|
95
|
+
# not. The cap is stated in the payload when it bites, never applied silently.
|
|
96
|
+
_MAX_MANIFESTS = 400
|
|
97
|
+
|
|
98
|
+
_TEST_SCOPE_WINDOW = 4 # lines after a Maven coordinate that may carry its <scope>
|
|
99
|
+
_TEST_SCOPE = re.compile(r"<scope>\s*(test|provided)\s*</scope>", re.IGNORECASE)
|
|
100
|
+
_GRADLE_TEST_CONF = re.compile(r"^\s*(test|androidTest)[A-Za-z]*\s*[(\s'\"]", re.IGNORECASE)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class DeclaredClient:
|
|
105
|
+
"""A client library the build declares, with the line that declares it."""
|
|
106
|
+
|
|
107
|
+
kind: str
|
|
108
|
+
coordinate: str
|
|
109
|
+
evidence: str # "relpath:line" — same shape as an Integration's evidence
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict:
|
|
112
|
+
return {"kind": self.kind, "coordinate": self.coordinate, "evidence": self.evidence}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def coordinates_for_kind(kind: str) -> tuple[str, ...]:
|
|
116
|
+
"""Published coordinates that declare ``kind``. One home for the list, so a consumer
|
|
117
|
+
that recognizes a library recognizes the same set this authority does."""
|
|
118
|
+
return tuple(sorted(c for c, k in _COORDINATE_KIND.items() if k == kind))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _manifest_paths(root: Path) -> list[Path]:
|
|
122
|
+
found: list[Path] = []
|
|
123
|
+
for name in sorted(_MANIFEST_NAMES):
|
|
124
|
+
for path in sorted(root.rglob(name)):
|
|
125
|
+
if len(found) >= _MAX_MANIFESTS:
|
|
126
|
+
return found
|
|
127
|
+
if any(part in _SKIP_DIRS for part in path.parts):
|
|
128
|
+
continue
|
|
129
|
+
if path.is_file():
|
|
130
|
+
found.append(path)
|
|
131
|
+
return found
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _is_test_scoped(lines: list[str], idx: int, manifest: str) -> bool:
|
|
135
|
+
"""True when the declaration is a test/provided dependency — a build-time coordinate
|
|
136
|
+
is not a statement that the running system talks to that external party."""
|
|
137
|
+
if manifest == "pom.xml":
|
|
138
|
+
for line in lines[idx: idx + _TEST_SCOPE_WINDOW + 1]:
|
|
139
|
+
if _TEST_SCOPE.search(line):
|
|
140
|
+
return True
|
|
141
|
+
return False
|
|
142
|
+
return bool(_GRADLE_TEST_CONF.match(lines[idx]))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def declared_integration_coordinates(
|
|
146
|
+
root: Optional[Path], *, manifests: Optional[Iterable[Path]] = None,
|
|
147
|
+
) -> list[DeclaredClient]:
|
|
148
|
+
"""Client libraries the build declares, deterministically ordered, deduplicated by
|
|
149
|
+
(kind, coordinate). Never raises: an unreadable manifest contributes nothing.
|
|
150
|
+
"""
|
|
151
|
+
if root is None:
|
|
152
|
+
return []
|
|
153
|
+
root = Path(root)
|
|
154
|
+
paths = list(manifests) if manifests is not None else _manifest_paths(root)
|
|
155
|
+
|
|
156
|
+
seen: dict[tuple[str, str], DeclaredClient] = {}
|
|
157
|
+
for path in paths:
|
|
158
|
+
try:
|
|
159
|
+
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
160
|
+
except OSError:
|
|
161
|
+
continue
|
|
162
|
+
try:
|
|
163
|
+
rel = str(path.relative_to(root)).replace("\\", "/")
|
|
164
|
+
except ValueError:
|
|
165
|
+
rel = path.name
|
|
166
|
+
for idx, line in enumerate(lines):
|
|
167
|
+
for coordinate, kind in _COORDINATE_KIND.items():
|
|
168
|
+
if coordinate not in line:
|
|
169
|
+
continue
|
|
170
|
+
if _is_test_scoped(lines, idx, path.name):
|
|
171
|
+
continue
|
|
172
|
+
key = (kind, coordinate)
|
|
173
|
+
if key not in seen:
|
|
174
|
+
seen[key] = DeclaredClient(kind, coordinate, f"{rel}:{idx + 1}")
|
|
175
|
+
return [seen[k] for k in sorted(seen)]
|
sourcecode/mcp/registry.py
CHANGED
|
@@ -781,7 +781,9 @@ Blast-radius analysis: who calls a class and what breaks if it changes? Java onl
|
|
|
781
781
|
|
|
782
782
|
Maps to: ask impact <target> <repo_path> [--depth <depth>]
|
|
783
783
|
Returns: direct_callers, indirect_callers, endpoints_affected,
|
|
784
|
-
transactional_boundaries_touched
|
|
784
|
+
transactional_boundaries_touched (target + callers),
|
|
785
|
+
transactional_boundaries_reaching (callers only), risk_score,
|
|
786
|
+
risk_level, stats.
|
|
785
787
|
|
|
786
788
|
Use this when:
|
|
787
789
|
- Planning a refactor: understand the full call chain before changing a class
|
sourcecode/mcp/server.py
CHANGED
|
@@ -890,7 +890,8 @@ def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) ->
|
|
|
890
890
|
Returns: ImpactChainResult with schema_version, symbol, resolution,
|
|
891
891
|
direct_callers, indirect_callers, endpoints_affected,
|
|
892
892
|
transaction_boundary (propagation/isolation/read_only),
|
|
893
|
-
security_surfaces (per-endpoint
|
|
893
|
+
security_surfaces (per-endpoint verdict/confidence from the
|
|
894
|
+
security_posture authority + declared policy + finding IDs),
|
|
894
895
|
impact_findings (TX-001..005 + SEC-001..003 findings in call chain),
|
|
895
896
|
analysis_warnings, risk_level, confidence, metadata.
|
|
896
897
|
|
|
@@ -1320,7 +1321,9 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
|
|
|
1320
1321
|
|
|
1321
1322
|
Maps to: ask impact <target> <repo_path> [--depth <depth>]
|
|
1322
1323
|
Returns: direct_callers, indirect_callers, endpoints_affected,
|
|
1323
|
-
transactional_boundaries_touched
|
|
1324
|
+
transactional_boundaries_touched (target + callers),
|
|
1325
|
+
transactional_boundaries_reaching (callers only), risk_score,
|
|
1326
|
+
risk_level, stats.
|
|
1324
1327
|
|
|
1325
1328
|
Use this when:
|
|
1326
1329
|
- Planning a refactor: understand the full call chain before changing a class
|
sourcecode/migrate_check.py
CHANGED
|
@@ -1402,6 +1402,11 @@ class MigrationReport:
|
|
|
1402
1402
|
# exposure map, call-chain detection, upgrade-vs-rewrite verdict). Attached
|
|
1403
1403
|
# by run_migrate_check; None when not computed.
|
|
1404
1404
|
hibernate: Optional["HibernateStratification"] = None
|
|
1405
|
+
# Regression scope behind the findings: the endpoints whose call path runs
|
|
1406
|
+
# through each affected file (migration_blast.compute_migration_blast).
|
|
1407
|
+
# Opt-in — attached by the CLI under --blast-radius; None when not computed,
|
|
1408
|
+
# and then absent from the payload so the default report stays byte-identical.
|
|
1409
|
+
blast_radius: Optional[dict] = None
|
|
1405
1410
|
|
|
1406
1411
|
def finalize(self) -> "MigrationReport":
|
|
1407
1412
|
if not self.generated_at:
|
|
@@ -1682,7 +1687,7 @@ class MigrationReport:
|
|
|
1682
1687
|
return self
|
|
1683
1688
|
|
|
1684
1689
|
def to_dict(self) -> dict:
|
|
1685
|
-
|
|
1690
|
+
d: dict = {
|
|
1686
1691
|
"schema_version": self.schema_version,
|
|
1687
1692
|
"generated_at": self.generated_at,
|
|
1688
1693
|
"repo_id": self.repo_id,
|
|
@@ -1718,6 +1723,11 @@ class MigrationReport:
|
|
|
1718
1723
|
"limitations": self.limitations,
|
|
1719
1724
|
"metadata": self.metadata,
|
|
1720
1725
|
}
|
|
1726
|
+
# Additive and opt-in: the key appears only when --blast-radius computed it,
|
|
1727
|
+
# so a default run emits exactly the payload it emitted before.
|
|
1728
|
+
if self.blast_radius is not None:
|
|
1729
|
+
d["blast_radius"] = self.blast_radius
|
|
1730
|
+
return d
|
|
1721
1731
|
|
|
1722
1732
|
def to_compact_dict(self, top_n: int = _COMPACT_TOP_N) -> dict:
|
|
1723
1733
|
"""Bounded, decision-grade projection of `to_dict()`.
|
|
@@ -1754,6 +1764,11 @@ class MigrationReport:
|
|
|
1754
1764
|
if isinstance(h.get(_col), list):
|
|
1755
1765
|
h[_col] = _cap_collection(h[_col], top_n)
|
|
1756
1766
|
compact["hibernate"] = h
|
|
1767
|
+
blast = full.get("blast_radius")
|
|
1768
|
+
if isinstance(blast, dict) and isinstance(blast.get("files"), list):
|
|
1769
|
+
b = dict(blast)
|
|
1770
|
+
b["files"] = _cap_collection(b["files"], top_n)
|
|
1771
|
+
compact["blast_radius"] = b
|
|
1757
1772
|
return compact
|
|
1758
1773
|
|
|
1759
1774
|
def to_text(self, min_severity: str = "low") -> str:
|
|
@@ -1802,6 +1817,12 @@ class MigrationReport:
|
|
|
1802
1817
|
lines.append(self.hibernate.to_text())
|
|
1803
1818
|
lines.append("")
|
|
1804
1819
|
|
|
1820
|
+
if self.blast_radius is not None:
|
|
1821
|
+
from sourcecode.migration_blast import render_blast_text
|
|
1822
|
+
|
|
1823
|
+
lines.append(render_blast_text(self.blast_radius))
|
|
1824
|
+
lines.append("")
|
|
1825
|
+
|
|
1805
1826
|
if not visible:
|
|
1806
1827
|
lines.append("No findings at or above selected severity.")
|
|
1807
1828
|
return "\n".join(lines)
|