sourcecode 1.45.0__py3-none-any.whl → 1.50.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.45.0"
3
+ __version__ = "1.50.0"
@@ -6,29 +6,20 @@ Converts the MCP from a flat tool collection into a guided agent operating syste
6
6
  - run_pr_review_flow: auto-chains delta + review_pr + blast radius
7
7
  - run_bug_investigation_flow: auto-chains fix_bug + impact + IR context
8
8
  - run_feature_flow: auto-chains context + endpoints + delta + structural awareness
9
-
10
- TODO planned high-value Java/Spring flow presets (audit 2026-06-16, repo SAINT):
11
- These extend the existing orchestrator; they are NOT yet implemented.
12
-
13
- 1. TODO: implement later run_migrate_flow
14
- Preset wrapping `migrate-check`. Primary high-value entry point for
15
- Spring Boot 2→3 planning (audit: produces 1,356-file prioritized
16
- inventory in ~6s the strongest determinante win). Should surface
17
- readiness_score, blocking count, per-target breakdown (jakarta /
18
- spring_security_6 / java_11) and estimated_effort_days.
19
-
20
- 2. TODO: implement later run_security_audit_flow
21
- Preset wrapping `spring-audit` + `endpoints`. Auto-handle the
22
- config-less case: when no sourcecode.config.json is present and the
23
- repo carries custom security annotations, emit a fallback WARNING +
24
- hint to add sourcecode.config.json (customAnnotations) rather than
25
- returning a misleading 100% none_detected result.
26
-
27
- 3. TODO: implement later — extend R2 orchestration rule (apply_orchestration_rules)
28
- Inject preset (1)/(2) when detected intent maps to migration or
29
- security audit, mirroring the existing R2 java_no_endpoints rule.
30
- Requires new INTENT_MIGRATION / INTENT_SECURITY_AUDIT constants +
31
- _INTENT_PATTERNS entries + WORKFLOW_SEQUENCES / FLOW_RUNNERS wiring.
9
+ - run_migrate_flow: wraps migrate-check; Spring Boot 2→3 readiness in one call
10
+ - run_security_audit_flow: wraps spring-audit + endpoints; config-less blind-spot warning
11
+
12
+ High-value Java/Spring flow presets (audit 2026-06-16, repo SAINT) — implemented:
13
+ 1. run_migrate_flow preset over `migrate-check`, the primary entry point for
14
+ Spring Boot 2→3 planning. Lifts readiness_score, blocking_count,
15
+ estimated_effort_days and the per-target breakdown to a top-level headline.
16
+ 2. run_security_audit_flowpreset over `spring-audit` + `endpoints`. Detects the
17
+ config-less case (no sourcecode.config.json + every endpoint none_detected) and
18
+ emits a warning plus a ready-to-paste config hint instead of a misleading 100%
19
+ unsecured surface.
20
+ 3. apply_orchestration_rules R5/R6inject these presets when the detected intent
21
+ maps to migration (INTENT_MIGRATION) or security audit (INTENT_SECURITY_AUDIT),
22
+ mirroring the existing R2 java_no_endpoints rule.
32
23
  """
33
24
  from __future__ import annotations
34
25
 
@@ -55,6 +46,8 @@ SESSION_READY_FOR_REVIEW = "READY_FOR_REVIEW" # flow complete
55
46
  # ---------------------------------------------------------------------------
56
47
 
57
48
  INTENT_PR_REVIEW = "pr_review"
49
+ INTENT_MIGRATION = "migration"
50
+ INTENT_SECURITY_AUDIT = "security_audit"
58
51
  INTENT_BUG_INVESTIGATION = "bug_investigation"
59
52
  INTENT_FEATURE_IMPLEMENTATION = "feature_implementation"
60
53
  INTENT_REFACTOR = "refactor"
@@ -67,6 +60,8 @@ INTENT_ORIENTATION = "orientation"
67
60
 
68
61
  WORKFLOW_SEQUENCES: dict[str, list[str]] = {
69
62
  INTENT_PR_REVIEW: ["get_delta", "review_pr_context", "get_impact_context"],
63
+ INTENT_MIGRATION: ["get_migration_readiness"],
64
+ INTENT_SECURITY_AUDIT: ["get_spring_audit", "get_endpoints"],
70
65
  INTENT_BUG_INVESTIGATION: ["fix_bug_context", "get_impact_context"],
71
66
  INTENT_FEATURE_IMPLEMENTATION: ["get_compact_context", "get_endpoints", "get_delta"],
72
67
  INTENT_REFACTOR: ["get_agent_context", "modernize_context", "get_ir_summary"],
@@ -76,6 +71,8 @@ WORKFLOW_SEQUENCES: dict[str, list[str]] = {
76
71
 
77
72
  WORKFLOW_DESCRIPTIONS: dict[str, str] = {
78
73
  INTENT_PR_REVIEW: "PR review: delta → execution paths → blast radius of changed classes",
74
+ INTENT_MIGRATION: "Spring Boot 2→3 migration: readiness score → javax→jakarta blockers → effort estimate",
75
+ INTENT_SECURITY_AUDIT: "Security audit: Spring TX/security findings → endpoint authorization surface",
79
76
  INTENT_BUG_INVESTIGATION: "Bug investigation: risk-ranked files → impact of suspect class",
80
77
  INTENT_FEATURE_IMPLEMENTATION: "Feature implementation: context → API surface → recent changes",
81
78
  INTENT_REFACTOR: "Refactor: deep context → modernization opportunities → IR coupling",
@@ -85,6 +82,8 @@ WORKFLOW_DESCRIPTIONS: dict[str, str] = {
85
82
 
86
83
  FLOW_RUNNERS: dict[str, str] = {
87
84
  INTENT_PR_REVIEW: "run_pr_review_flow",
85
+ INTENT_MIGRATION: "run_migrate_flow",
86
+ INTENT_SECURITY_AUDIT: "run_security_audit_flow",
88
87
  INTENT_BUG_INVESTIGATION: "run_bug_investigation_flow",
89
88
  INTENT_FEATURE_IMPLEMENTATION: "run_feature_flow",
90
89
  INTENT_REFACTOR: "run_feature_flow",
@@ -101,6 +100,16 @@ _INTENT_PATTERNS: list[tuple[str, list[str]]] = [
101
100
  r"\bpr\b", r"pull request", r"review pr", r"\bdiff\b", r"merge request",
102
101
  r"code review", r"changes in branch", r"review.*branch", r"branch.*review",
103
102
  ]),
103
+ (INTENT_MIGRATION, [
104
+ r"\bmigrat", r"spring.?boot.?[23]", r"spring boot 2.*3", r"boot 2.*3",
105
+ r"\bjakarta\b", r"\bjavax\b", r"javax.*jakarta", r"namespace.*(migrat|change)",
106
+ r"2\s*(?:->|to|→|–|—)\s*3", r"\bupgrade\b.*(spring|boot|jakarta|java)",
107
+ ]),
108
+ (INTENT_SECURITY_AUDIT, [
109
+ r"security audit", r"audit.*security", r"security.*surface",
110
+ r"\bauthoriz", r"\bpreauthorize\b", r"\bsecured\b", r"access control",
111
+ r"who can (?:call|access)", r"endpoint.*(secur|auth)", r"(secur|auth).*endpoint",
112
+ ]),
104
113
  (INTENT_BUG_INVESTIGATION, [
105
114
  r"\bbug\b", r"\berror\b", r"\bexception\b", r"\bcrash\b", r"\bnpe\b",
106
115
  r"\bfix\b", r"\bbroken\b", r"\bfail(s|ing)?\b", r"stack.?trace",
@@ -168,6 +177,9 @@ def apply_orchestration_rules(
168
177
  R2 java_no_endpoints → prepend get_endpoints (api_surface must exist first)
169
178
  R3 large_repo (>1000) → note RIS path preferred (informational, no seq change)
170
179
  R4 no_symptom_bug_flow → quality warning issued by caller (not a seq change)
180
+ R5 migration_intent (Java) → ensure get_migration_readiness leads the sequence
181
+ R6 security_audit_intent (Java) → ensure get_endpoints precedes the audit so the
182
+ authorization surface is available when findings are interpreted
171
183
  """
172
184
  seq = list(sequence)
173
185
  rules: list[str] = []
@@ -186,6 +198,17 @@ def apply_orchestration_rules(
186
198
  if repo_class_count > 1000:
187
199
  rules.append("R3:large_repo→RIS_path_preferred")
188
200
 
201
+ # R5: migration intent on a Java repo → migration readiness is the entry point.
202
+ if intent == INTENT_MIGRATION and is_java and "get_migration_readiness" not in seq:
203
+ seq.insert(0, "get_migration_readiness")
204
+ rules.append("R5:migration_intent→prepend_migration_readiness")
205
+
206
+ # R6: security-audit intent on a Java repo → endpoints surface must precede the
207
+ # audit (mirrors R2: the authorization surface has to exist first).
208
+ if intent == INTENT_SECURITY_AUDIT and is_java and "get_endpoints" not in seq:
209
+ seq.insert(0, "get_endpoints")
210
+ rules.append("R6:security_audit_intent→prepend_get_endpoints")
211
+
189
212
  return seq, rules
190
213
 
191
214
 
@@ -729,3 +752,166 @@ def run_feature_flow_impl(repo_path: str, feature_description: str = "") -> dict
729
752
  "tools_suggested_to_agent": 0,
730
753
  },
731
754
  }
755
+
756
+
757
+ # ---------------------------------------------------------------------------
758
+ # Flow: Spring Boot 2→3 Migration
759
+ # ---------------------------------------------------------------------------
760
+
761
+ def run_migrate_flow_impl(repo_path: str, min_severity: str = "low") -> dict[str, Any]:
762
+ """Migration Flow: Spring Boot 2→3 readiness in one call.
763
+
764
+ Primary high-value entry point for migration planning. Wraps ``migrate-check``
765
+ and lifts the headline numbers (readiness_score, blocking_count,
766
+ estimated_effort_days, per-target breakdown) to the top level so the agent
767
+ can plan a 2→3 upgrade without parsing the full report.
768
+ """
769
+ t0 = time.monotonic()
770
+ steps: list[str] = []
771
+ quality_warnings: list[str] = []
772
+ output: dict[str, Any] = {}
773
+
774
+ if not _is_java_repo(repo_path):
775
+ quality_warnings.append(
776
+ "not_a_java_repo: migration analysis targets Spring Boot / Java repos. "
777
+ "Result will be empty (readiness_score=100, no findings)."
778
+ )
779
+
780
+ report = _exec(["migrate-check", repo_path, "--min-severity", min_severity])
781
+ steps.append(f"get_migration_readiness(min_severity={min_severity})")
782
+ headline: dict[str, Any] = {}
783
+ if "_exec_error" not in report:
784
+ output["migration_readiness"] = report
785
+ # Lift the planning-relevant headline numbers to the top level.
786
+ for key in (
787
+ "readiness_score", "blocking_count", "estimated_effort_days",
788
+ "spring_boot_2_detected",
789
+ ):
790
+ if key in report:
791
+ headline[key] = report[key]
792
+ _summary = report.get("summary", {})
793
+ if isinstance(_summary, dict):
794
+ headline["total_findings"] = _summary.get("total_findings")
795
+ headline["affected_files"] = _summary.get("affected_files")
796
+ headline["by_severity"] = _summary.get("by_severity")
797
+ headline["by_target"] = _summary.get("by_target") or _summary.get("by_rule")
798
+ else:
799
+ quality_warnings.append(f"migrate_check_failed: {report['_exec_error']}")
800
+
801
+ ttfca_ms = int((time.monotonic() - t0) * 1000)
802
+
803
+ return {
804
+ "session_state": SESSION_READY_FOR_REVIEW,
805
+ "flow": "migration",
806
+ "min_severity": min_severity,
807
+ "headline": headline,
808
+ "steps_executed": steps,
809
+ "quality_warnings": quality_warnings,
810
+ "consolidated_output": output,
811
+ "session_meta": {
812
+ "ttfca_ms": ttfca_ms,
813
+ "steps_auto_executed": len(steps),
814
+ "tools_suggested_to_agent": 0,
815
+ },
816
+ }
817
+
818
+
819
+ # ---------------------------------------------------------------------------
820
+ # Flow: Security Audit
821
+ # ---------------------------------------------------------------------------
822
+
823
+ def _endpoint_security_coverage(endpoints: dict[str, Any]) -> tuple[int, int]:
824
+ """Return (total_endpoints, none_detected) from an endpoints result."""
825
+ if "_exec_error" in endpoints:
826
+ return 0, 0
827
+ data = endpoints.get("data", endpoints) if isinstance(endpoints, dict) else {}
828
+ total = int(data.get("total", 0) or 0)
829
+ none_detected = int(data.get("no_security_signal", 0) or 0)
830
+ return total, none_detected
831
+
832
+
833
+ def run_security_audit_flow_impl(repo_path: str, scope: str = "all") -> dict[str, Any]:
834
+ """Security Audit Flow: Spring findings + endpoint authorization surface.
835
+
836
+ Wraps ``spring-audit`` (TX + security findings) and ``endpoints`` (authorization
837
+ surface) in one call. Auto-handles the config-less case: when no
838
+ ``sourcecode.config.json`` is present and every endpoint reads as
839
+ ``none_detected``, the all-zero security surface is almost certainly a
840
+ custom-annotation blind spot rather than a truly unsecured API — so the flow
841
+ emits a warning plus a ready-to-paste config hint instead of letting the
842
+ misleading result stand.
843
+ """
844
+ from sourcecode.security_config import CONFIG_FILENAME
845
+
846
+ t0 = time.monotonic()
847
+ steps: list[str] = []
848
+ quality_warnings: list[str] = []
849
+ output: dict[str, Any] = {}
850
+
851
+ if not _is_java_repo(repo_path):
852
+ quality_warnings.append(
853
+ "not_a_java_repo: spring-audit targets Java/Spring repos and will "
854
+ "report spring_detected=false."
855
+ )
856
+
857
+ # Step 1: Spring semantic audit (TX anomalies + security findings).
858
+ audit = _exec(["spring-audit", repo_path, "--scope", scope])
859
+ steps.append(f"get_spring_audit(scope={scope})")
860
+ if "_exec_error" not in audit:
861
+ output["spring_audit"] = audit
862
+ else:
863
+ quality_warnings.append(f"spring_audit_failed: {audit['_exec_error']}")
864
+
865
+ # Step 2: endpoint authorization surface.
866
+ endpoints = _exec(["endpoints", repo_path])
867
+ steps.append("get_endpoints")
868
+ if "_exec_error" not in endpoints:
869
+ output["endpoint_security_surface"] = endpoints
870
+ else:
871
+ quality_warnings.append(f"endpoints_failed: {endpoints['_exec_error']}")
872
+
873
+ # Config-less blind-spot detection: no sourcecode.config.json + every endpoint
874
+ # none_detected → warn rather than report a misleading 100% unsecured surface.
875
+ has_config = (Path(repo_path) / CONFIG_FILENAME).exists()
876
+ total, none_detected = _endpoint_security_coverage(endpoints)
877
+ if not has_config and total > 0 and none_detected == total:
878
+ quality_warnings.append(
879
+ "config_less_security_blind_spot: no sourcecode.config.json found and "
880
+ f"all {total} endpoints report none_detected. If this repo uses a custom "
881
+ "authorization annotation, the surface is a false negative — add the "
882
+ "config below and re-run."
883
+ )
884
+ output["security_config_hint"] = {
885
+ "reason": "no_custom_security_annotations_configured",
886
+ "file": CONFIG_FILENAME,
887
+ "example": {
888
+ "customSecurityAnnotations": [
889
+ {
890
+ "shortName": "YourSecurityAnnotation",
891
+ "resourceParam": "resourceName",
892
+ "levelParam": "requiredLevel",
893
+ }
894
+ ]
895
+ },
896
+ }
897
+
898
+ ttfca_ms = int((time.monotonic() - t0) * 1000)
899
+
900
+ return {
901
+ "session_state": SESSION_READY_FOR_REVIEW,
902
+ "flow": "security_audit",
903
+ "scope": scope,
904
+ "endpoint_security_coverage": {
905
+ "total_endpoints": total,
906
+ "none_detected": none_detected,
907
+ "config_present": has_config,
908
+ },
909
+ "steps_executed": steps,
910
+ "quality_warnings": quality_warnings,
911
+ "consolidated_output": output,
912
+ "session_meta": {
913
+ "ttfca_ms": ttfca_ms,
914
+ "steps_auto_executed": len(steps),
915
+ "tools_suggested_to_agent": 0,
916
+ },
917
+ }
sourcecode/mcp/server.py CHANGED
@@ -510,6 +510,75 @@ def run_feature_flow(repo_path: str = ".", feature_description: str = "") -> dic
510
510
  )
511
511
 
512
512
 
513
+ @mcp.tool()
514
+ def run_migrate_flow(repo_path: str = ".", min_severity: str = "low") -> dict:
515
+ """Migration Flow — Spring Boot 2→3 readiness in one call. JAVA/SPRING ONLY.
516
+
517
+ Primary high-value entry point for migration planning. Wraps migrate-check and
518
+ lifts the headline numbers to the top level so the agent can plan a 2→3 upgrade
519
+ without parsing the full report:
520
+ - readiness_score (0–100; 100 = ready), blocking_count, estimated_effort_days
521
+ - by_severity and by_target breakdown (jakarta / spring_security_6 / java_11)
522
+
523
+ Use this instead of calling get_migration_readiness and interpreting it by hand.
524
+ Returns headline + consolidated_output.migration_readiness (full report).
525
+
526
+ repo_path: absolute path to the Java repository (default: current working directory).
527
+ min_severity: "critical" | "high" | "medium" | "low" (default — include everything).
528
+ """
529
+ _raw = repo_path
530
+ try:
531
+ if not isinstance(repo_path, str):
532
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
533
+ repo_path = _normalize_repo_path(repo_path)
534
+ _path_err = _check_repo_path(repo_path)
535
+ if _path_err is not None:
536
+ return _path_err
537
+ from sourcecode.mcp.orchestrator import run_migrate_flow_impl
538
+ return _ok(run_migrate_flow_impl(repo_path, min_severity or "low"))
539
+ except Exception as exc:
540
+ return _err(
541
+ f"Internal error: {type(exc).__name__}: {exc} — repo_path: {_raw}",
542
+ "INTERNAL_ERROR",
543
+ )
544
+
545
+
546
+ @mcp.tool()
547
+ def run_security_audit_flow(repo_path: str = ".", scope: str = "all") -> dict:
548
+ """Security Audit Flow — Spring findings + endpoint authorization surface. JAVA/SPRING ONLY.
549
+
550
+ Auto-chains in one call:
551
+ 1. get_spring_audit(scope) — TX anomalies + security-surface findings
552
+ 2. get_endpoints — endpoint authorization surface
553
+
554
+ Config-less safeguard: when no sourcecode.config.json is present and every
555
+ endpoint reads none_detected, the flow flags a likely custom-annotation blind
556
+ spot (quality_warnings) and returns a ready-to-paste security_config_hint —
557
+ rather than letting a misleading 100% none_detected surface stand.
558
+
559
+ Returns endpoint_security_coverage + consolidated_output (spring_audit,
560
+ endpoint_security_surface, optional security_config_hint).
561
+
562
+ repo_path: absolute path to the Java repository (default: current working directory).
563
+ scope: "all" (default) | "tx" | "security".
564
+ """
565
+ _raw = repo_path
566
+ try:
567
+ if not isinstance(repo_path, str):
568
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
569
+ repo_path = _normalize_repo_path(repo_path)
570
+ _path_err = _check_repo_path(repo_path)
571
+ if _path_err is not None:
572
+ return _path_err
573
+ from sourcecode.mcp.orchestrator import run_security_audit_flow_impl
574
+ return _ok(run_security_audit_flow_impl(repo_path, scope or "all"))
575
+ except Exception as exc:
576
+ return _err(
577
+ f"Internal error: {type(exc).__name__}: {exc} — repo_path: {_raw}",
578
+ "INTERNAL_ERROR",
579
+ )
580
+
581
+
513
582
  @mcp.tool()
514
583
  def get_cold_start_context(repo_path: str = ".") -> dict:
515
584
  """Instant session bootstrap from persisted Repository Intelligence Snapshot (RIS).
@@ -1333,16 +1402,19 @@ _NATIVE_MCP_TOOLS: frozenset[str] = frozenset({
1333
1402
  "run_pr_review_flow",
1334
1403
  "run_bug_investigation_flow",
1335
1404
  "run_feature_flow",
1405
+ "run_migrate_flow",
1406
+ "run_security_audit_flow",
1336
1407
  })
1337
1408
 
1338
1409
 
1339
1410
  def _finalize_mcp_registry() -> None:
1340
1411
  """Sync the MCP server with the runtime-generated registry.
1341
1412
 
1342
- Removes every tool except the 5 native orchestration tools (start_session,
1343
- analyze_task, flow runners) which are registered via @mcp.tool() in this
1344
- module and have no backing CLI command. All other tools are rebuilt from the
1345
- runtime-derived registry (build_mcp_tool_specs).
1413
+ Removes every tool except the native orchestration tools (start_session,
1414
+ analyze_task, and the flow runners see _NATIVE_MCP_TOOLS) which are
1415
+ registered via @mcp.tool() in this module and have no backing CLI command.
1416
+ All other tools are rebuilt from the runtime-derived registry
1417
+ (build_mcp_tool_specs).
1346
1418
  """
1347
1419
  from sourcecode.mcp.registry import build_mcp_tool_specs, make_tool_callable, validate_registry
1348
1420
 
@@ -3829,6 +3829,18 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3829
3829
  all_symbols: list[SymbolRecord] = []
3830
3830
  extends_map: dict[str, str] = {}
3831
3831
 
3832
+ # F-2: detect WebFlux / functional (RouterFunction) routing. Such routes register
3833
+ # via a fluent DSL (route().GET("/path", handler)) instead of @RequestMapping
3834
+ # annotations, so the annotation-based surface built below does not see them. We
3835
+ # deliberately do NOT synthesize endpoint entries: the literal paths here are
3836
+ # relative (the real path includes nest()/group-version prefixes that cannot be
3837
+ # resolved statically), and emitting partial paths would mislead more than an empty
3838
+ # surface. Instead we count them and surface an honest limitation so a zero/partial
3839
+ # annotation surface is never read as "this app exposes no endpoints".
3840
+ _FN_ROUTE_RE = _re.compile(r'\.(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(\s*"')
3841
+ _fn_route_files: set[str] = set()
3842
+ _fn_route_count = 0
3843
+
3832
3844
  for jf in java_files:
3833
3845
  try:
3834
3846
  source = jf.read_text(encoding="utf-8", errors="replace")
@@ -3838,6 +3850,11 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3838
3850
  rel = str(jf.relative_to(root)).replace("\\", "/")
3839
3851
  except ValueError:
3840
3852
  rel = str(jf).replace("\\", "/")
3853
+ if "RouterFunction" in source or "RequestPredicates" in source:
3854
+ _fn_hits = len(_FN_ROUTE_RE.findall(source))
3855
+ if _fn_hits:
3856
+ _fn_route_files.add(rel)
3857
+ _fn_route_count += _fn_hits
3841
3858
  _, symbols, _ = _extract_symbols(source, rel, extra_capture=_extra_capture)
3842
3859
  for sym in symbols:
3843
3860
  all_symbols.append(sym)
@@ -4045,6 +4062,26 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
4045
4062
  result["spec_sourced_endpoints"] = len(_spec_endpoints)
4046
4063
  if _openapi_spec_path:
4047
4064
  result["openapi_spec"] = _openapi_spec_path
4065
+ # F-2: surface functional/RouterFunction routing as an honest limitation. Not modeled
4066
+ # in the surface above; counted so an empty/partial annotation surface is not misread.
4067
+ if _fn_route_count:
4068
+ result["functional_routing"] = {
4069
+ "files": len(_fn_route_files),
4070
+ "route_registrations": _fn_route_count,
4071
+ "modeled": False,
4072
+ }
4073
+ _fr_msg = (
4074
+ f"{_fn_route_count} functional route registration(s) across "
4075
+ f"{len(_fn_route_files)} file(s) use WebFlux/RouterFunction routing "
4076
+ f'(route().GET("/path", handler)), which is NOT modeled here — this surface '
4077
+ f"covers annotation-based (@RequestMapping/@GetMapping) endpoints only."
4078
+ )
4079
+ if not endpoints:
4080
+ _fr_msg += (
4081
+ " This surface is EMPTY despite functional routes being present — "
4082
+ "do NOT read it as 'no endpoints'; the app's HTTP surface is unmodeled."
4083
+ )
4084
+ result.setdefault("warnings", []).append(_fr_msg)
4048
4085
  return result
4049
4086
 
4050
4087
 
@@ -44,7 +44,18 @@ _SCHEMA_VERSION = "1.0"
44
44
  # Only appears on class nodes, never method nodes. Including it
45
45
  # chains through DTOs and entities that merely reference the service
46
46
  # type, inflating caller counts without semantic value.
47
- _SKIP_EDGE_TYPES: frozenset[str] = frozenset({"contained_in", "imports"})
47
+ # implements / — CH-006: structural type declarations, NOT calls. The reverse edge
48
+ # extends on an interface/base lists its implementors/subclasses; an
49
+ # implementor does not *call* the interface by virtue of implementing
50
+ # it. Traversing these attributes every SIBLING implementor of a shared
51
+ # interface as a "caller". On a high-fanout in-repo hub interface (e.g.
52
+ # halo's CustomEndpoint, 43 implementors) this turned a leaf endpoint
53
+ # into 42 false direct callers / risk:high. Interface→impl expansion that
54
+ # IS wanted (CH-001a/b) flows through ImplementationGraph indices, not
55
+ # through these reverse-graph edges, so excluding them here is loss-free.
56
+ _SKIP_EDGE_TYPES: frozenset[str] = frozenset(
57
+ {"contained_in", "imports", "implements", "extends"}
58
+ )
48
59
 
49
60
  # Max BFS depth guard — caller growth is bounded per _bfs_callers
50
61
  _BFS_DEFAULT_DEPTH = 4
@@ -148,6 +159,68 @@ _STEREOTYPE_ANNOTATIONS = frozenset({
148
159
  _VALUE_TYPE_KINDS = frozenset({"class", "enum", "record"})
149
160
 
150
161
 
162
+ # ---------------------------------------------------------------------------
163
+ # CH-005 — framework/external-interface DI blind-spot detection
164
+ # ---------------------------------------------------------------------------
165
+ # When a class implements/extends a type that is NOT an in-repo symbol (a
166
+ # framework or library supertype — e.g. Spring Security's RedirectStrategy, a
167
+ # servlet Filter, a JPA base), the class is typically invoked polymorphically
168
+ # *through that external type* and wired by framework DI/config. No in-repo call
169
+ # edge ever names the impl's own method, and ImplementationGraph.build()
170
+ # deliberately drops external supertypes (cir_graphs: `to_fqn not in
171
+ # known_symbols` → skipped), so CH-001b cannot expand to the interface. Result:
172
+ # impact-chain reports 0 callers / risk:low at confidence=high — a dangerous
173
+ # false negative, since the real blast radius flows through framework wiring the
174
+ # static call-graph never traverses. Detect the external supertype positively,
175
+ # warn, and downgrade confidence (parallel to the CH-003 value-type guard).
176
+ #
177
+ # Inert marker interfaces carry no methods → no polymorphic dispatch → no hidden
178
+ # blast radius, so they are excluded to avoid firing on plain Serializable DTOs.
179
+ _INERT_MARKER_SUPERTYPES = frozenset({
180
+ "Serializable", "java.io.Serializable",
181
+ "Cloneable", "java.lang.Cloneable",
182
+ "Externalizable", "java.io.Externalizable",
183
+ })
184
+
185
+
186
+ def _external_supertypes(cir, class_fqn: str) -> list[str]:
187
+ """Return supertypes of class_fqn that are NOT in-repo symbols.
188
+
189
+ Reads raw implements/extends edges from cir.dependencies and keeps only those
190
+ whose target cannot be resolved to a single in-repo class (i.e. framework /
191
+ library types). Mirrors ImplementationGraph's resolution rules (exact FQN
192
+ match, then unambiguous simple-name match) so the internal/external split is
193
+ identical. Inert marker interfaces are dropped. Order-preserving, deduped.
194
+ """
195
+ deps = getattr(cir, "dependencies", None) or []
196
+ known: set[str] = set(getattr(cir, "symbols", None) or [])
197
+ simple_to_fqn: dict[str, list[str]] = {}
198
+ for sym in known:
199
+ if "#" not in sym and "." in sym:
200
+ simple_to_fqn.setdefault(sym.rsplit(".", 1)[1], []).append(sym)
201
+
202
+ external: list[str] = []
203
+ for edge in deps:
204
+ if edge.get("type") not in ("implements", "extends"):
205
+ continue
206
+ frm = normalize_owner_fqn((edge.get("from") or "").strip())
207
+ if frm != class_fqn:
208
+ continue
209
+ to = (edge.get("to") or "").strip()
210
+ if not to or ">" in to or "<" in to:
211
+ continue
212
+ simple = to.rsplit(".", 1)[1] if "." in to else to
213
+ if simple in _INERT_MARKER_SUPERTYPES or to in _INERT_MARKER_SUPERTYPES:
214
+ continue
215
+ # Internal if it resolves to exactly one in-repo class (exact or simple-name).
216
+ if to in known:
217
+ continue
218
+ if len(simple_to_fqn.get(simple, [])) == 1:
219
+ continue
220
+ external.append(to)
221
+ return list(dict.fromkeys(external))
222
+
223
+
151
224
  def _is_unmodeled_value_type(cir, class_fqn: str, model) -> bool:
152
225
  """True iff class_fqn is positively a plain value/DTO type whose blast radius
153
226
  flows only through type-usage edges the impact graph does not model.
@@ -605,6 +678,12 @@ class ImpactOrchestrator:
605
678
  t0 = time.monotonic()
606
679
  depth = max(1, min(depth, _BFS_HARD_LIMIT))
607
680
  warnings: list[str] = []
681
+ # F-1: not every warning degrades confidence. The CH-001a/b interface↔impl
682
+ # expansion notices are INFORMATIONAL (they describe normal, correct operation)
683
+ # and previously forced every Spring interface/impl query — the common case — down
684
+ # to confidence=medium permanently. Only genuinely degrading conditions (capped
685
+ # traversal) set this flag; resolution=="partial" is handled separately below.
686
+ confidence_reducing = False
608
687
 
609
688
  # ── 1. Resolve symbol ─────────────────────────────────────────────
610
689
  resolution, seed_fqns, sym_warnings = _resolve_symbol(symbol, cir.symbols)
@@ -702,6 +781,7 @@ class ImpactOrchestrator:
702
781
  "Hub-class guard active: symbol has > 500 direct callers — "
703
782
  "indirect caller traversal capped at depth=1."
704
783
  )
784
+ confidence_reducing = True # capped traversal → result is incomplete
705
785
 
706
786
  # ── 3. Endpoints affected ─────────────────────────────────────────
707
787
  all_callers = direct_callers + indirect_callers
@@ -750,16 +830,38 @@ class ImpactOrchestrator:
750
830
  impact_findings_raw,
751
831
  )
752
832
 
753
- # CH-003: empty blast radius on a positively-identified value/DTO type is a
754
- # type-usage blind spot, not proof of dead code warn + drop confidence.
833
+ # Empty blast radius is ambiguous: genuinely-unused code OR an unmodeled-edge
834
+ # blind spot. Two positively-detected blind spots reclassify it from a
835
+ # high-confidence "safe to change" into a low-confidence "look further".
755
836
  empty_blast = (
756
837
  not direct_callers and not indirect_callers
757
838
  and not endpoints_affected and not subtype_classes_added
758
839
  )
840
+ class_level_seed = "#" not in resolved_symbol and resolution != "not_found"
841
+
842
+ # CH-005: framework/external-interface DI blind spot. Checked first because
843
+ # its diagnosis (polymorphic invocation via an external supertype + framework
844
+ # wiring) is more specific than the value-type fallback for the same symbol.
845
+ external_supertypes: list[str] = []
846
+ if empty_blast and class_level_seed:
847
+ external_supertypes = _external_supertypes(cir, resolved_symbol)
848
+ framework_di_blind_spot = bool(external_supertypes)
849
+ if framework_di_blind_spot:
850
+ warnings.append(
851
+ "Framework/external-interface DI blind spot (CH-005): this class "
852
+ "implements/extends external type(s) [" + ", ".join(external_supertypes)
853
+ + "] and is likely invoked polymorphically through them and wired by "
854
+ "framework DI/config. The static call-graph has no in-repo edge naming "
855
+ "this class's methods, so 0 callers is NOT proof it is unused — search "
856
+ "DI/security/config wiring for the supertype to find the real callers."
857
+ )
858
+
859
+ # CH-003: empty blast radius on a positively-identified value/DTO type is a
860
+ # type-usage blind spot, not proof of dead code — warn + drop confidence.
759
861
  value_type_blind_spot = (
760
862
  empty_blast
761
- and "#" not in resolved_symbol
762
- and resolution != "not_found"
863
+ and class_level_seed
864
+ and not framework_di_blind_spot
763
865
  and _is_unmodeled_value_type(cir, resolved_symbol, model)
764
866
  )
765
867
  if value_type_blind_spot:
@@ -773,9 +875,9 @@ class ImpactOrchestrator:
773
875
  confidence: str
774
876
  if resolution == "not_found":
775
877
  confidence = "low"
776
- elif value_type_blind_spot:
878
+ elif framework_di_blind_spot or value_type_blind_spot:
777
879
  confidence = "low"
778
- elif resolution == "partial" or warnings:
880
+ elif resolution == "partial" or confidence_reducing:
779
881
  confidence = "medium"
780
882
  else:
781
883
  confidence = "high"
@@ -803,6 +905,11 @@ class ImpactOrchestrator:
803
905
  "risk_score": risk_score,
804
906
  "model_build_time_ms": model.build_time_ms,
805
907
  "query_time_ms": elapsed_ms,
908
+ "blind_spots": (
909
+ (["framework_di"] if framework_di_blind_spot else [])
910
+ + (["value_type"] if value_type_blind_spot else [])
911
+ ),
912
+ "external_supertypes": external_supertypes,
806
913
  },
807
914
  )
808
915
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.45.0
3
+ Version: 1.50.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
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.45.0-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.50.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -368,6 +368,8 @@ sourcecode endpoints /path/to/repo --output endpoints.json
368
368
 
369
369
  Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
370
370
 
371
+ **Functional / WebFlux routing (honest limitation).** Routes registered via the functional DSL — `route().GET("/path", handler)` / `RouterFunction` / `CustomEndpoint`, common in reactive Spring apps — are **not** modeled (their real paths depend on `nest()`/group-version prefixes that can't be resolved statically). Rather than emit partial paths that would mislead, the output reports a `functional_routing` block (`files`, `route_registrations`, `modeled: false`) plus a warning. When the annotation surface is empty but functional routes exist, the warning explicitly tells you not to read it as "no endpoints". Annotation-based (MVC/JAX-RS) repos are unaffected.
372
+
371
373
  **Custom security annotations.** Enterprise repos often guard endpoints with a bespoke annotation instead of `@PreAuthorize`/`@Secured`. Drop a `sourcecode.config.json` at the repo root to teach the scanner about it — otherwise those endpoints report `policy: "none_detected"`:
372
374
 
373
375
  ```json
@@ -435,6 +437,12 @@ Unlike `impact` (which traces the caller graph), `impact-chain` builds on the Sp
435
437
  | `security_surfaces` | Per-endpoint security policy + SEC finding IDs |
436
438
  | `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
437
439
  | `risk_level` | `critical` \| `high` \| `medium` \| `low` |
440
+ | `confidence` | `high` \| `medium` \| `low` — `low` on a detected blind spot, `medium` on partial resolution or capped traversal. Informational interface↔impl expansion notices do **not** lower it, so a clean resolved query stays `high`. |
441
+ | `metadata.blind_spots` | `framework_di` and/or `value_type` when an empty result is unmodeled-edge driven, not real dead code |
442
+
443
+ **Framework/DI blind spot (CH-005).** An empty blast radius is ambiguous: genuinely unused, or invoked through an edge the static graph does not model. When the target class implements/extends an **external** framework type (e.g. Spring Security's `RedirectStrategy`, a servlet `Filter`) it is typically wired by framework DI/config and invoked polymorphically — no in-repo edge names its methods, so `direct_callers` is `0`. Rather than report that as `risk:low` at high confidence (a dangerous false negative that reads as "safe to change"), `impact-chain` detects the external supertype, drops `confidence` to `low`, lists it in `metadata.external_supertypes`, and emits a `CH-005` warning telling you to search the DI/security/config wiring for the supertype. Inert markers (`Serializable`, `Cloneable`) are excluded.
444
+
445
+ **Caller precision (CH-006).** `implements`/`extends` are structural type declarations, not calls — so they are excluded from the caller graph. Querying a class that implements a high-fanout interface (e.g. a 40-implementor `CustomEndpoint` or a shared `Mapper<E,D>` base) does **not** report its sibling implementors as callers; only real `injects`/`calls` edges count. This prevents a leaf class from being inflated to a large false blast radius.
438
446
 
439
447
  **Event topology** — query the publisher/consumer graph for a Spring event class:
440
448
 
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=r64WSv9Pulq3yQh1i31dLAG4Q5FJDkTYFw7e54HHTsc,103
1
+ sourcecode/__init__.py,sha256=pCkOXPgXyXxjAbBGF1i4NrzrWef2vm102RXTjjOE8go,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=liCwQmLgb5vplohy8arjYxs_HOIv5C9MjLh_gY6bc5Q,44115
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -44,7 +44,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
44
44
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
45
45
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
46
46
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
47
- sourcecode/repository_ir.py,sha256=WjDYwbBm-eWp-k6aSdBrgO_XcRGuP-Llp0TZHBhq8bY,208237
47
+ sourcecode/repository_ir.py,sha256=eBC8Jh1rBH8xE46atHmdGNQxcjuROyUCr0iCuhqviUc,210340
48
48
  sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
49
49
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
50
50
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -54,7 +54,7 @@ sourcecode/semantic_analyzer.py,sha256=4OdG6tTSnTvq3_dSWMbQu8Ad1ndSCKeG-b9qM4hIx
54
54
  sourcecode/serializer.py,sha256=TGzftrSKitZrtl6Hh-R05s4KdTOxwTmph_lGDbo2Wzg,125015
55
55
  sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
56
56
  sourcecode/spring_findings.py,sha256=G7Or2lKBUQbcTDqudLvSs9XvNg_YoAa-_lBOG_ULs8E,5457
57
- sourcecode/spring_impact.py,sha256=rUKSiCfXh9NpC9a97KvjCu6Kn8bYezTnMDY3F7sgtCI,38737
57
+ sourcecode/spring_impact.py,sha256=WUbBw6Ne9esN_KczIs9BCJdRAmjDKtU6E2_auo-771s,44865
58
58
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
59
59
  sourcecode/spring_security_audit.py,sha256=XtPJ1SXlZJ8k6VYmaWuAp7Bbir4UmreAL7doIGQ5I7o,20595
60
60
  sourcecode/spring_semantic.py,sha256=O1nKSGVzlukuxLHQVuCPxc-XrcrMFxwlHA20_dmEGgM,13307
@@ -86,10 +86,10 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
86
86
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
87
87
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
88
88
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
89
- sourcecode/mcp/orchestrator.py,sha256=luOglUaKPaMnsq9j3XI3D-P68ABURYpiP1fojK5LDi4,28177
89
+ sourcecode/mcp/orchestrator.py,sha256=kT7IssYNyQXVtDf2Q69qrMSTuyJlJ1Rhkp6_EHqc_38,36520
90
90
  sourcecode/mcp/registry.py,sha256=8LxxalpJy1L_BrEfwiVfywFVOcJJMXLGusJaUGGH7Y4,65663
91
91
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
92
- sourcecode/mcp/server.py,sha256=f4-k-nx2amhSghlM7EBeZWyqCMEAGodOgrYPdbIUK08,59891
92
+ sourcecode/mcp/server.py,sha256=6Q98B1JkgBR5pZx3JyyXZosazpPX7FSBDqfG9nlZaII,63114
93
93
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
94
94
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
95
95
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -101,8 +101,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
101
101
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
102
102
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
103
103
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
104
- sourcecode-1.45.0.dist-info/METADATA,sha256=24sPQbi_FdplZlmfRZ1zc3_gQPZEYwbs0HGCY4A59j8,32359
105
- sourcecode-1.45.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
106
- sourcecode-1.45.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
107
- sourcecode-1.45.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
108
- sourcecode-1.45.0.dist-info/RECORD,,
104
+ sourcecode-1.50.0.dist-info/METADATA,sha256=AMKgfVLsoGpmtnK3Vk75eHizMToSqxiiPsoMzTA-res,34684
105
+ sourcecode-1.50.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
106
+ sourcecode-1.50.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
107
+ sourcecode-1.50.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
108
+ sourcecode-1.50.0.dist-info/RECORD,,