sourcecode 1.72.0__tar.gz → 1.73.0__tar.gz

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.
Files changed (124) hide show
  1. {sourcecode-1.72.0 → sourcecode-1.73.0}/CHANGELOG.md +52 -0
  2. {sourcecode-1.72.0 → sourcecode-1.73.0}/PKG-INFO +1 -1
  3. {sourcecode-1.72.0 → sourcecode-1.73.0}/pyproject.toml +1 -1
  4. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/cli.py +34 -6
  6. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/java.py +99 -8
  7. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/parsers.py +23 -0
  8. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/integration_detector.py +43 -0
  9. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/migrate_check.py +4 -12
  10. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/repository_ir.py +35 -0
  11. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/serializer.py +6 -1
  12. {sourcecode-1.72.0 → sourcecode-1.73.0}/.github/workflows/build-windows.yml +0 -0
  13. {sourcecode-1.72.0 → sourcecode-1.73.0}/.gitignore +0 -0
  14. {sourcecode-1.72.0 → sourcecode-1.73.0}/.ruff.toml +0 -0
  15. {sourcecode-1.72.0 → sourcecode-1.73.0}/CONTRIBUTING.md +0 -0
  16. {sourcecode-1.72.0 → sourcecode-1.73.0}/LICENSE +0 -0
  17. {sourcecode-1.72.0 → sourcecode-1.73.0}/README.md +0 -0
  18. {sourcecode-1.72.0 → sourcecode-1.73.0}/SECURITY.md +0 -0
  19. {sourcecode-1.72.0 → sourcecode-1.73.0}/raw +0 -0
  20. {sourcecode-1.72.0 → sourcecode-1.73.0}/scripts/generate_jdk_exports.py +0 -0
  21. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/adaptive_scanner.py +0 -0
  22. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/architecture_analyzer.py +0 -0
  23. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/architecture_summary.py +0 -0
  24. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/ast_extractor.py +0 -0
  25. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/cache.py +0 -0
  26. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/canonical_ir.py +0 -0
  27. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  30. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/confidence_analyzer.py +0 -0
  31. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/context_scorer.py +0 -0
  32. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/context_summarizer.py +0 -0
  33. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/contract_model.py +0 -0
  34. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/contract_pipeline.py +0 -0
  35. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/dependency_analyzer.py +0 -0
  37. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/__init__.py +0 -0
  38. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/base.py +0 -0
  39. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  40. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/dart.py +0 -0
  41. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/dotnet.py +0 -0
  42. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/elixir.py +0 -0
  43. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/go.py +0 -0
  44. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/heuristic.py +0 -0
  45. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/hybrid.py +0 -0
  46. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/php.py +0 -0
  49. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/project.py +0 -0
  50. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/python.py +0 -0
  51. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/ruby.py +0 -0
  52. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/rust.py +0 -0
  53. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/systems.py +0 -0
  54. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/terraform.py +0 -0
  55. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/detectors/tooling.py +0 -0
  56. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/doc_analyzer.py +0 -0
  57. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  58. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/env_analyzer.py +0 -0
  59. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/error_schema.py +0 -0
  60. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/explain.py +0 -0
  61. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/file_chunker.py +0 -0
  62. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/flow_analyzer.py +0 -0
  64. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/format_contract.py +0 -0
  65. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/fqn_utils.py +0 -0
  66. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/git_analyzer.py +0 -0
  67. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/graph_analyzer.py +0 -0
  68. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/hibernate_strat.py +0 -0
  69. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/jdk_exports.py +0 -0
  70. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/license.py +0 -0
  71. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/__init__.py +0 -0
  72. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  73. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  74. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  75. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  76. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  77. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  78. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/registry.py +0 -0
  79. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/runner.py +0 -0
  80. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp/server.py +0 -0
  81. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/mcp_nudge.py +0 -0
  82. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/metrics_analyzer.py +0 -0
  83. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/openapi_surface.py +0 -0
  84. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/output_budget.py +0 -0
  85. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/path_filters.py +0 -0
  86. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  87. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/pr_impact.py +0 -0
  88. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/prepare_context.py +0 -0
  89. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/progress.py +0 -0
  90. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/ranking_engine.py +0 -0
  91. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/redactor.py +0 -0
  92. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/relevance_scorer.py +0 -0
  93. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/rename_refactor.py +0 -0
  94. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/repo_classifier.py +0 -0
  95. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/ris.py +0 -0
  96. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/runtime_classifier.py +0 -0
  97. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/scanner.py +0 -0
  98. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/schema.py +0 -0
  99. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/security_config.py +0 -0
  100. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/semantic_analyzer.py +0 -0
  101. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_event_topology.py +0 -0
  102. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_findings.py +0 -0
  103. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_impact.py +0 -0
  104. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_model.py +0 -0
  105. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_security_audit.py +0 -0
  106. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_semantic.py +0 -0
  107. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  108. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/summarizer.py +0 -0
  109. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/__init__.py +0 -0
  110. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/config.py +0 -0
  111. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/consent.py +0 -0
  112. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/events.py +0 -0
  113. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/filters.py +0 -0
  114. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/telemetry/transport.py +0 -0
  115. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/tree_utils.py +0 -0
  116. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/validation_surface.py +0 -0
  117. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/version_check.py +0 -0
  118. {sourcecode-1.72.0 → sourcecode-1.73.0}/src/sourcecode/workspace.py +0 -0
  119. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/functions/README.md +0 -0
  120. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/functions/get-license/index.ts +0 -0
  121. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  122. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/functions/telemetry/index.ts +0 -0
  123. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/sql/license_event_ordering.sql +0 -0
  124. {sourcecode-1.72.0 → sourcecode-1.73.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.73.0] — 2026-07-01
4
+
5
+ ### Fixed — audit reliability on a large non-Boot Spring monolith (Alfresco Community Repo field test)
6
+
7
+ A real architecture audit of **alfresco-community-repo** (`fe6dd26c3f`; 6,549
8
+ classes, 24 Maven modules, Spring 7 **without Spring Boot**, iBatis persistence,
9
+ Camel/ActiveMQ messaging, Acegi-derived security, WebScript REST surface)
10
+ surfaced seven defects — each capable of leading an automated consultant to a
11
+ false conclusion. All fixed at root cause with regression coverage and
12
+ re-verified on the live repo.
13
+
14
+ - **`--compact` did not resolve `language_version` from a Maven property (BUG #1).**
15
+ Alfresco declares `<maven.compiler.source>${java.version}</...>` chained to
16
+ `<java.version>21</java.version>`; the Java-stack detector picked the first key
17
+ and emitted the literal `${java.version}` while `migrate-check` resolved it. A
18
+ shared `substitute_maven_properties` resolver (in `detectors/parsers.py`) now
19
+ backs BOTH paths — `--compact` reports `21`.
20
+ - **`explain --output` wrote Markdown to a `.json` file with no warning (BUG #2).**
21
+ Broke any consumer assuming JSON parity across subcommands. `explain` now infers
22
+ JSON from a `.json` output extension (an explicit `--format` always wins), and
23
+ `--help` documents that its default output is human-readable text/Markdown.
24
+ - **`entry_points.bootstrap` false positives on `*Application.java` by name (BUG #3).**
25
+ Alfresco's XSD/JAXB-generated `Application.java` model classes (no `main()`, no
26
+ bootstrap annotation, in a WAR) were listed as application entry points. A file
27
+ now qualifies only with a real `main(String[])` or a bootstrap annotation
28
+ (`@SpringBootApplication` / WAR `SpringBootServletInitializer` / `@QuarkusMain`
29
+ / …); the serializer's name-based bootstrap fallback was removed.
30
+ - **"MyBatis" framework declared against the tool's own zero evidence (BUG #4).**
31
+ A `org.mybatis:mybatis` pom coordinate alone produced a "MyBatis" framework label
32
+ while the `mybatis` block reported `mapper_interfaces: 0, xml_files: 0` (Alfresco's
33
+ SQL layer is iBatis-style `*-SqlMap.xml`, surfaced separately). MyBatis is now
34
+ emitted only with real usage evidence — an `@Mapper` interface or a `*Mapper.xml`.
35
+ - **`export --integrations` missed ActiveMQ/Camel messaging (BUG #5).**
36
+ JMS detection recognized little beyond a direct `JmsTemplate`, reporting 1
37
+ integration on a repo with an ActiveMQ connection-factory config and Camel routes.
38
+ Added `ActiveMQSslConnectionFactory` / pooled / caching connection-factory tokens
39
+ and an Apache Camel route pass (literal `activemq:`/`jms:` endpoint URIs, plus
40
+ variable-URI `from()/to()` routes inside a `RouteBuilder` as honest low-confidence
41
+ `camel-route` messaging records). Alfresco JMS/messaging surface: 1 → 5.
42
+ - **`readiness_score` Hibernate false-positive — regression test (BUG #6).**
43
+ v1.63.0 once scored this Hibernate-free repo 42/100 with a phantom
44
+ `hibernate_rewrite` headline (~46 days). Already fixed by v1.72.0; added an
45
+ explicit regression test (`no Hibernate/JPA imports → hibernate.detected False,
46
+ headline_blocker never hibernate_rewrite`) so a future detector refactor cannot
47
+ reintroduce it.
48
+ - **`endpoints` / C4 `api_surface` returned 0 with no explanation (BUG #7).**
49
+ On an obvious API server (WebScripts: XML descriptor + handler class, not
50
+ `@RestController`), a bare `total: 0` reads as "no public API". `endpoints` now
51
+ emits a first-class `zero_result_reason` naming the routing patterns searched and
52
+ any non-Spring surface detected (Alfresco: 482 WebScript descriptors / 901 handler
53
+ classes), and the C4 export's `api_surface` carries the same explanation.
54
+
3
55
  ## [1.72.0] — 2026-07-01
4
56
 
5
57
  ### Fixed — correctness on a large real-world Spring Boot 3 monolith (Broadleaf Commerce CE field test)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.72.0
3
+ Version: 1.73.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.72.0"
7
+ version = "1.73.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.72.0"
3
+ __version__ = "1.73.0"
@@ -4221,6 +4221,7 @@ def _build_c4_export(
4221
4221
  edges: "list[dict]",
4222
4222
  endpoints: "list[dict]",
4223
4223
  integrations: "dict",
4224
+ endpoint_meta: "Optional[dict]" = None,
4224
4225
  ) -> "dict":
4225
4226
  """Assemble a unified, tool-agnostic C4 architecture export + incremental manifest.
4226
4227
 
@@ -4236,6 +4237,15 @@ def _build_c4_export(
4236
4237
  # consumer counts/classifies modules instead of inferring them from leaf dirs.
4237
4238
  module_graph["module_roots"] = _detect_module_roots(by_directory)
4238
4239
  api_surface = _group_endpoints_by_controller(endpoints)
4240
+ # BUG #7 (Alfresco field test): carry the endpoint extractor's structured
4241
+ # zero-result explanation and non-Spring REST surface into api_surface, so a
4242
+ # C4 consumer seeing 0 controllers understands WHY (unrecognized routing, e.g.
4243
+ # WebScripts) instead of concluding the system has no public API.
4244
+ if endpoint_meta:
4245
+ if endpoint_meta.get("zero_result_reason"):
4246
+ api_surface["zero_result_reason"] = endpoint_meta["zero_result_reason"]
4247
+ if endpoint_meta.get("non_spring_rest_surface"):
4248
+ api_surface["non_spring_rest_surface"] = endpoint_meta["non_spring_rest_surface"]
4239
4249
  containers = _detect_containers(root)
4240
4250
 
4241
4251
  limitations: "list[str]" = []
@@ -4364,7 +4374,8 @@ def export_cmd(
4364
4374
  from sourcecode.repository_ir import extract_java_endpoints
4365
4375
  ir = build_repo_ir(file_list, root)
4366
4376
  graph = ir.get("graph", {})
4367
- endpoints = extract_java_endpoints(root).get("endpoints", [])
4377
+ _ep_data = extract_java_endpoints(root)
4378
+ endpoints = _ep_data.get("endpoints", [])
4368
4379
  data = _build_c4_export(
4369
4380
  root,
4370
4381
  file_list,
@@ -4372,6 +4383,7 @@ def export_cmd(
4372
4383
  graph.get("edges", []),
4373
4384
  endpoints,
4374
4385
  detect_integrations(file_list, root),
4386
+ endpoint_meta=_ep_data,
4375
4387
  )
4376
4388
  output = _serialize_dict(data, format)
4377
4389
  _emit_command_output(output, output_path, copy,
@@ -5215,14 +5227,16 @@ def explain_cmd(
5215
5227
  Path("."),
5216
5228
  help="Repository root (default: current directory)",
5217
5229
  ),
5218
- format: str = typer.Option(
5219
- "text", "--format", "-f",
5220
- help="Output format: text (default) or json.",
5221
- show_default=True,
5230
+ format: Optional[str] = typer.Option(
5231
+ None, "--format", "-f",
5232
+ help="Output format: text (default) or json. If unset and --output ends "
5233
+ "in .json, JSON is written automatically.",
5222
5234
  ),
5223
5235
  output_path: Optional[Path] = typer.Option(
5224
5236
  None, "--output", "-o",
5225
- help="Write output to a file instead of stdout.",
5237
+ help="Write output to a file instead of stdout. NOTE: the default format is "
5238
+ "human-readable text/Markdown, NOT JSON — pass --format json (or use a "
5239
+ ".json output path) for machine-readable output.",
5226
5240
  ),
5227
5241
  copy: bool = typer.Option(
5228
5242
  False, "--copy", "-c",
@@ -5231,6 +5245,11 @@ def explain_cmd(
5231
5245
  ) -> None:
5232
5246
  """Human-readable architectural summary for a class.
5233
5247
 
5248
+ \b
5249
+ OUTPUT FORMAT: defaults to text/Markdown (human-oriented). Unlike most
5250
+ subcommands this is NOT JSON by default — pass --format json, or give an
5251
+ --output path ending in .json, for a machine-readable envelope.
5252
+
5234
5253
  \b
5235
5254
  Generates a structured explanation derived entirely from static analysis:
5236
5255
  - Purpose and Spring stereotype
@@ -5278,6 +5297,15 @@ def explain_cmd(
5278
5297
  )
5279
5298
  raise typer.Exit(code=1)
5280
5299
 
5300
+ # BUG #2 (Alfresco field test): `explain -o out.json` previously wrote Markdown
5301
+ # to a .json file, breaking any programmatic consumer that assumed JSON parity
5302
+ # with the other subcommands. Infer JSON from a .json output extension when
5303
+ # --format is unset; an explicit --format always wins.
5304
+ if format is None:
5305
+ if output_path and str(output_path).lower().endswith(".json"):
5306
+ format = "json"
5307
+ else:
5308
+ format = "text"
5281
5309
  _enforce_format("explain", format)
5282
5310
 
5283
5311
  file_list = find_java_files(target)
@@ -10,7 +10,11 @@ from sourcecode.detectors.base import (
10
10
  EntryPoint,
11
11
  StackDetection,
12
12
  )
13
- from sourcecode.detectors.parsers import read_text_lines, unique_strings
13
+ from sourcecode.detectors.parsers import (
14
+ read_text_lines,
15
+ substitute_maven_properties as _resolve_maven_property,
16
+ unique_strings,
17
+ )
14
18
  from sourcecode.schema import FrameworkDetection
15
19
  from sourcecode.tree_utils import flatten_file_tree
16
20
 
@@ -55,6 +59,14 @@ _JAX_RS_PROVIDER_RE = re.compile(r'@Provider\b')
55
59
 
56
60
  # --- CDI / Jakarta EE scopes ---
57
61
  _CDI_SCOPED_RE = re.compile(r'@(?:ApplicationScoped|RequestScoped|SessionScoped|Singleton|Dependent)\b')
62
+ # BUG #3: a genuine bootstrap entry declares a real main() or a bootstrap annotation
63
+ # / WAR initializer supertype — never merely a `*Application.java` filename.
64
+ _MAIN_METHOD_RE = re.compile(r'\bstatic\b[^;{}()]*\bmain\s*\(\s*(?:final\s+)?String')
65
+ _BOOTSTRAP_ANNOTATION_RE = re.compile(
66
+ r'@(?:SpringBootApplication|QuarkusMain|MicronautApplication|'
67
+ r'EnableAutoConfiguration|SpringBootConfiguration)\b'
68
+ r'|\bextends\s+SpringBootServletInitializer\b'
69
+ )
58
70
 
59
71
  # --- Keycloak / Quarkus SPI ---
60
72
  # Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
@@ -151,6 +163,18 @@ class JavaDetector(AbstractDetector):
151
163
  # Spring profiles — check src/main/options/, src/main/resources/
152
164
  spring_profiles = self._detect_spring_profiles(context.root, all_paths)
153
165
 
166
+ # BUG #4 (Alfresco field test): a `org.mybatis` coordinate in the pom is not
167
+ # proof the app USES MyBatis mappers. Alfresco declares org.mybatis:mybatis
168
+ # yet has zero @Mapper interfaces and zero *Mapper.xml — its SQL layer is
169
+ # iBatis-style *-SqlMap.xml (surfaced separately). Emitting "MyBatis" as an
170
+ # architecture framework then contradicted the tool's own evidence block
171
+ # (mapper_interfaces:0, xml_files:0) and mislabeled bean DTO mappers. Keep
172
+ # the label only with real usage evidence: a *Mapper.xml or an @Mapper interface.
173
+ if any(f.name == "MyBatis" for f in frameworks) and not self._has_mybatis_usage(
174
+ context.root, all_paths
175
+ ):
176
+ frameworks = [f for f in frameworks if f.name != "MyBatis"]
177
+
154
178
  entry_points = self._collect_entry_points(context)
155
179
  transactional_classes = self._collect_transactional_classes(context, all_paths)
156
180
  stack = StackDetection(
@@ -211,6 +235,17 @@ class JavaDetector(AbstractDetector):
211
235
  break
212
236
  break
213
237
 
238
+ # BUG #1 (Alfresco field test): a picked value can be a Maven property
239
+ # reference — Alfresco's <maven.compiler.source>${java.version}</...> chains
240
+ # to <java.version>21</java.version> in the SAME <properties> block. Resolve
241
+ # ${...} references against the pom's own properties so --compact reports the
242
+ # concrete version ("21") instead of the literal "${java.version}", matching
243
+ # what migrate-check already surfaces. Bounded iteration guards ref cycles.
244
+ if "language_version" in result:
245
+ result["language_version"] = _resolve_maven_property(
246
+ result["language_version"], props
247
+ )
248
+
214
249
  return result
215
250
 
216
251
  def _detect_spring_profiles(self, root: Path, all_paths: list[str]) -> list[str]:
@@ -425,15 +460,25 @@ class JavaDetector(AbstractDetector):
425
460
  # Exclude test trees: test helpers like AdminApplication.java in
426
461
  # integration/src/test/java/ must not be treated as production entrypoints.
427
462
  from sourcecode.path_filters import is_test_path as _is_test_path
463
+ # BUG #3 (Alfresco field test): a `*Application.java` / `*Main.java` NAME does
464
+ # not make a file a bootstrap entry point. Alfresco has XSD-generated JAXB
465
+ # `Application.java` model classes (no main(), no bootstrap annotation) in a
466
+ # WAR with no own entry point — flagging them as `application` polluted the
467
+ # executive summary. Emit `kind="application"` ONLY when the file actually
468
+ # declares a `main(String[])` method or a bootstrap annotation.
428
469
  app_candidates = [
429
- p for p in all_java
430
- if p.endswith(("Application.java", "Main.java"))
431
- and not _is_test_path(p)
432
- ]
433
- entry_points: list[EntryPoint] = [
434
- EntryPoint(path=p, stack="java", kind="application", source="manifest")
435
- for p in unique_strings(app_candidates)
470
+ p for p in unique_strings([
471
+ q for q in all_java
472
+ if q.endswith(("Application.java", "Main.java")) and not _is_test_path(q)
473
+ ])
436
474
  ]
475
+ entry_points: list[EntryPoint] = []
476
+ for p in app_candidates:
477
+ verified = self._verify_bootstrap_entry(context.root / p)
478
+ if verified:
479
+ entry_points.append(EntryPoint(
480
+ path=p, stack="java", kind="application", source=verified,
481
+ ))
437
482
 
438
483
  # 2. Annotation-based scan: @RestController, @WebFilter, FilterRegistrationBean
439
484
  # Prioritize Controller-named files so all REST controllers are detected
@@ -481,6 +526,52 @@ class JavaDetector(AbstractDetector):
481
526
  unique_eps.append(ep)
482
527
  return unique_eps
483
528
 
529
+ def _has_mybatis_usage(self, root: Path, all_paths: list[str]) -> bool:
530
+ """True when the repo shows real MyBatis usage, not just a pom coordinate.
531
+
532
+ Evidence = a *Mapper.xml file OR a *Mapper.java carrying the MyBatis
533
+ @Mapper annotation. iBatis-style *-SqlMap.xml is deliberately NOT counted
534
+ here — it is a distinct legacy pattern surfaced elsewhere (BUG #4).
535
+ """
536
+ if any(p.endswith("Mapper.xml") for p in all_paths):
537
+ return True
538
+ from sourcecode.path_filters import is_test_path as _is_test_path
539
+ mapper_java = [
540
+ p for p in all_paths
541
+ if p.endswith("Mapper.java") and not _is_test_path(p)
542
+ ]
543
+ for rel in mapper_java[:_MAX_JAVA_ENTRY_SCAN]:
544
+ try:
545
+ abs_path = root / rel
546
+ if abs_path.stat().st_size > _MAX_FILE_SIZE:
547
+ continue
548
+ content = abs_path.read_text(encoding="utf-8", errors="replace")
549
+ except OSError:
550
+ continue
551
+ if "org.apache.ibatis.annotations.Mapper" in content or "@Mapper" in content:
552
+ return True
553
+ return False
554
+
555
+ def _verify_bootstrap_entry(self, abs_path: Path) -> "str | None":
556
+ """Return the evidence source when *abs_path* is a real bootstrap entry.
557
+
558
+ A file is a genuine application entry point only if it declares a
559
+ `main(String[])` method OR carries a recognized bootstrap annotation /
560
+ WAR initializer supertype. Returns "main_method" / "bootstrap_annotation"
561
+ accordingly, else None (name alone is never sufficient — see BUG #3).
562
+ """
563
+ try:
564
+ if abs_path.stat().st_size > _MAX_FILE_SIZE:
565
+ return None
566
+ content = abs_path.read_text(encoding="utf-8", errors="replace")
567
+ except OSError:
568
+ return None
569
+ if _BOOTSTRAP_ANNOTATION_RE.search(content):
570
+ return "bootstrap_annotation"
571
+ if _MAIN_METHOD_RE.search(content):
572
+ return "main_method"
573
+ return None
574
+
484
575
  def _augment_deep_java_controllers(self, context: DetectionContext, all_java: list[str]) -> None:
485
576
  """Scan standard Java source roots for *Controller*.java files not in all_java.
486
577
 
@@ -1,10 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import re
4
5
  from collections.abc import Iterable
5
6
  from pathlib import Path
6
7
  from typing import Any, Optional
7
8
 
9
+ _MAVEN_PROP_REF_RE = re.compile(r'\$\{([\w.\-]+)\}')
10
+
11
+
12
+ def substitute_maven_properties(text: str, props: "dict[str, str]") -> str:
13
+ """Resolve Maven ${prop} references in *text* against *props*.
14
+
15
+ Shared single source of truth for Maven property resolution across
16
+ subcommands (BUG #1, Alfresco field test): both the --compact Java-stack
17
+ detector and migrate-check must resolve `${java.version}` → `21` identically.
18
+ Multi-level references (${a} where a=${b}) resolve in up to 3 passes; a
19
+ reference with no matching property is left verbatim (honest, not blanked).
20
+ """
21
+ if not props or "${" not in text:
22
+ return text
23
+ resolved = text
24
+ for _ in range(3):
25
+ new = _MAVEN_PROP_REF_RE.sub(lambda m: props.get(m.group(1), m.group(0)), resolved)
26
+ if new == resolved:
27
+ break
28
+ resolved = new
29
+ return resolved
30
+
8
31
 
9
32
  def load_json_file(path: Path) -> Optional[dict[str, Any]]:
10
33
  """Carga un fichero JSON sin lanzar si el contenido es invalido."""
@@ -101,12 +101,33 @@ _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
101
101
  # JMS / messaging
102
102
  ("JmsTemplate", "jms", "jmstemplate"),
103
103
  ("ActiveMQConnectionFactory", "jms", "activemq"),
104
+ # BUG #5 (Alfresco field test): the connection-factory family beyond the plain
105
+ # ActiveMQConnectionFactory — Alfresco wires ActiveMQSslConnectionFactory and
106
+ # pooled/caching factories as Spring @Beans, none of which were recognized.
107
+ ("ActiveMQSslConnectionFactory", "jms", "activemq"),
108
+ ("PooledConnectionFactory", "jms", "activemq-pooled"),
109
+ ("CachingConnectionFactory", "jms", "spring-jms"),
104
110
  )
105
111
  _TOKEN_RES = tuple(
106
112
  (re.compile(r"\b" + re.escape(tok) + r"\b"), kind, client)
107
113
  for tok, kind, client in _TOKEN_CLIENTS
108
114
  )
109
115
 
116
+ # BUG #5: Apache Camel route detection. Only applied inside a confirmed Camel file.
117
+ _CAMEL_CONTEXT_RE = re.compile(r"org\.apache\.camel\b|\bextends\s+RouteBuilder\b")
118
+ # A from()/to() endpoint with a literal scheme URI, e.g. from("activemq:queue:foo").
119
+ _CAMEL_URI_RE = re.compile(r'\b(from|to)\s*\(\s*"([a-z][a-z0-9+.\-]*):([^"]*)"')
120
+ # A route anchored by from(<non-string>) — endpoint URI comes from a variable/property.
121
+ _CAMEL_VAR_ROUTE_RE = re.compile(r'\bfrom\s*\(\s*[A-Za-z_]')
122
+ # Camel URI scheme → integration kind (jms-family messaging schemes collapse to "jms").
123
+ _CAMEL_SCHEME_KIND: "dict[str, str]" = {
124
+ "activemq": "jms", "amq": "jms", "jms": "jms", "sjms": "jms", "sjms2": "jms",
125
+ "amqp": "jms", "stomp": "jms",
126
+ "kafka": "kafka", "mqtt": "mqtt", "paho": "mqtt",
127
+ "http": "http", "https": "http", "http4": "http", "rest": "http",
128
+ "smtp": "smtp", "smtps": "smtp",
129
+ }
130
+
110
131
 
111
132
  def _line_of(text: str, idx: int) -> int:
112
133
  """1-based line number of character offset ``idx`` in ``text``."""
@@ -323,6 +344,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
323
344
  _add(kind, client, url.group(1), rel, lineno)
324
345
  break
325
346
 
347
+ # BUG #5 (Alfresco field test): Apache Camel routes are messaging/integration
348
+ # endpoints declared as a fluent DSL (from(...)/to(...)) inside a RouteBuilder,
349
+ # NOT as JmsTemplate/connection-factory constructs — so they were invisible.
350
+ # Only trust from()/to() inside a confirmed Camel file (import org.apache.camel
351
+ # or `extends RouteBuilder`) to avoid matching unrelated fluent `.to(...)`.
352
+ if _CAMEL_CONTEXT_RE.search(text):
353
+ for m in _CAMEL_URI_RE.finditer(text):
354
+ scheme = m.group(2).lower()
355
+ uri = f"{scheme}:{m.group(3)}"
356
+ kind = _CAMEL_SCHEME_KIND.get(scheme, "messaging")
357
+ _add(kind, f"camel-{scheme}", uri, rel, _line_of(text, m.start()))
358
+ # Routes whose endpoint URI is a variable/property (Alfresco's
359
+ # from(sourceQueue).to(targetTopic)) cannot yield a literal target, but the
360
+ # route IS a real messaging integration point — record it honestly with a
361
+ # null target and low confidence rather than dropping it.
362
+ if not any(r["client"].startswith("camel-") and r["evidence"].startswith(f"{rel}:")
363
+ for r in records):
364
+ vm = _CAMEL_VAR_ROUTE_RE.search(text)
365
+ if vm:
366
+ _add("messaging", "camel-route", None, rel,
367
+ _line_of(text, vm.start()), confidence="low")
368
+
326
369
  records.sort(key=lambda r: (r["kind"], r["client"], r["evidence"]))
327
370
 
328
371
  by_kind: "dict[str, int]" = {}
@@ -1168,18 +1168,10 @@ def _resolve_maven_properties(text: str) -> str:
1168
1168
  props: dict[str, str] = {}
1169
1169
  for m in re.finditer(r'<([A-Za-z][\w.\-]*)>\s*([^<${}]+?)\s*</\1>', text):
1170
1170
  props[m.group(1)] = m.group(2).strip()
1171
- if not props:
1172
- return text
1173
-
1174
- resolved = text
1175
- for _ in range(3):
1176
- def _sub(m: re.Match) -> str: # noqa: E306
1177
- return props.get(m.group(1), m.group(0))
1178
- resolved_new = re.sub(r'\$\{([\w.\-]+)\}', _sub, resolved)
1179
- if resolved_new == resolved:
1180
- break
1181
- resolved = resolved_new
1182
- return resolved
1171
+ # Shared ${...} substitution core (BUG #1): identical resolution semantics as
1172
+ # the --compact Java-stack detector.
1173
+ from sourcecode.detectors.parsers import substitute_maven_properties
1174
+ return substitute_maven_properties(text, props)
1183
1175
 
1184
1176
 
1185
1177
  def _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
@@ -4735,6 +4735,41 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
4735
4735
  f"endpoints only."
4736
4736
  )
4737
4737
  result.setdefault("warnings", []).append(_msg)
4738
+
4739
+ # BUG #7 (Alfresco field test): a bare "total: 0" must never be read by an
4740
+ # automated consumer as "this service exposes no public API". Emit a first-class
4741
+ # structured `zero_result_reason` (mirroring integrations.coverage_note /
4742
+ # security_model_detail.reason) naming the routing patterns that WERE searched and
4743
+ # any non-Spring surface detected, so a 0 is understood as "no RECOGNIZED pattern",
4744
+ # not "no surface". Only when the Spring/annotation surface is genuinely empty.
4745
+ if not endpoints:
4746
+ _searched = (
4747
+ "@RestController/@Controller + @RequestMapping/@GetMapping/@PostMapping/"
4748
+ "@PutMapping/@DeleteMapping/@PatchMapping, JAX-RS @Path, functional "
4749
+ "RouterFunction routing, and OpenAPI-spec-recovered routes"
4750
+ )
4751
+ if _nonspring_total:
4752
+ _fw_names = {
4753
+ "webscripts": "Alfresco WebScripts (XML descriptor + handler class)",
4754
+ "jax_rs": "JAX-RS",
4755
+ "servlets": "mapped Servlets",
4756
+ }
4757
+ _detected = ", ".join(
4758
+ _fw_names.get(k, k) for k, v in _nonspring.items() if v
4759
+ )
4760
+ result["zero_result_reason"] = (
4761
+ f"0 endpoints recognized. Searched: {_searched}. A NON-Spring REST "
4762
+ f"surface WAS detected ({_detected}) but is not statically modeled — "
4763
+ f"do NOT read this as 'no public API'. Inspect that framework's routing "
4764
+ f"(e.g. *.desc.xml descriptors for WebScripts) to enumerate the surface."
4765
+ )
4766
+ else:
4767
+ result["zero_result_reason"] = (
4768
+ f"0 endpoints recognized. Searched: {_searched}. No recognized routing "
4769
+ f"construct was found; a framework this analyzer does not model may "
4770
+ f"still expose an HTTP surface — verify manually before concluding the "
4771
+ f"service has no public API."
4772
+ )
4738
4773
  return result
4739
4774
 
4740
4775
 
@@ -702,7 +702,12 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
702
702
  kind = getattr(ep, "kind", "")
703
703
  stem = _Path(path).stem
704
704
 
705
- if kind == "application" or any(k in stem for k in ("Application", "Main", "Initializer", "Bootstrap")):
705
+ # BUG #3 (Alfresco field test): bootstrap membership is driven by the
706
+ # VERIFIED entry kind ("application" is emitted only when the detector
707
+ # confirmed a main()/bootstrap annotation), never by a `*Application*`
708
+ # filename alone — XSD-generated Application.java model classes must not
709
+ # appear here.
710
+ if kind == "application":
706
711
  if path not in seen_b:
707
712
  seen_b.add(path)
708
713
  bootstrap.append(path)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes