sourcecode 1.69.0__tar.gz → 1.71.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.69.0 → sourcecode-1.71.0}/CHANGELOG.md +134 -2
  2. {sourcecode-1.69.0 → sourcecode-1.71.0}/PKG-INFO +16 -5
  3. {sourcecode-1.69.0 → sourcecode-1.71.0}/README.md +15 -4
  4. {sourcecode-1.69.0 → sourcecode-1.71.0}/pyproject.toml +1 -1
  5. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/architecture_analyzer.py +39 -6
  7. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/architecture_summary.py +66 -0
  8. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/java.py +40 -2
  9. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/hibernate_strat.py +96 -21
  10. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/integration_detector.py +88 -0
  11. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/migrate_check.py +144 -14
  12. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/repository_ir.py +32 -0
  13. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_impact.py +30 -3
  14. {sourcecode-1.69.0 → sourcecode-1.71.0}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-1.69.0 → sourcecode-1.71.0}/.gitignore +0 -0
  16. {sourcecode-1.69.0 → sourcecode-1.71.0}/.ruff.toml +0 -0
  17. {sourcecode-1.69.0 → sourcecode-1.71.0}/CONTRIBUTING.md +0 -0
  18. {sourcecode-1.69.0 → sourcecode-1.71.0}/LICENSE +0 -0
  19. {sourcecode-1.69.0 → sourcecode-1.71.0}/SECURITY.md +0 -0
  20. {sourcecode-1.69.0 → sourcecode-1.71.0}/raw +0 -0
  21. {sourcecode-1.69.0 → sourcecode-1.71.0}/scripts/generate_jdk_exports.py +0 -0
  22. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/adaptive_scanner.py +0 -0
  23. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/ast_extractor.py +0 -0
  24. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/cache.py +0 -0
  25. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/canonical_ir.py +0 -0
  26. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/cir_graphs.py +0 -0
  27. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/classifier.py +0 -0
  28. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/cli.py +0 -0
  29. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  30. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/confidence_analyzer.py +0 -0
  31. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/context_scorer.py +0 -0
  32. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/context_summarizer.py +0 -0
  33. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/contract_model.py +0 -0
  34. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/contract_pipeline.py +0 -0
  35. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/dependency_analyzer.py +0 -0
  37. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/__init__.py +0 -0
  38. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/base.py +0 -0
  39. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  40. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/dart.py +0 -0
  41. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/dotnet.py +0 -0
  42. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/elixir.py +0 -0
  43. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/go.py +0 -0
  44. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/heuristic.py +0 -0
  45. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/hybrid.py +0 -0
  46. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/parsers.py +0 -0
  49. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/php.py +0 -0
  50. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/project.py +0 -0
  51. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  59. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/env_analyzer.py +0 -0
  60. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/error_schema.py +0 -0
  61. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/explain.py +0 -0
  62. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/file_chunker.py +0 -0
  63. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/file_classifier.py +0 -0
  64. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/flow_analyzer.py +0 -0
  65. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/format_contract.py +0 -0
  66. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/fqn_utils.py +0 -0
  67. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/git_analyzer.py +0 -0
  68. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/graph_analyzer.py +0 -0
  69. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/jdk_exports.py +0 -0
  70. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/license.py +0 -0
  71. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/__init__.py +0 -0
  72. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  73. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  74. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  75. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  76. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  77. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  78. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/registry.py +0 -0
  79. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/runner.py +0 -0
  80. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp/server.py +0 -0
  81. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/mcp_nudge.py +0 -0
  82. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/metrics_analyzer.py +0 -0
  83. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/openapi_surface.py +0 -0
  84. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/output_budget.py +0 -0
  85. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/path_filters.py +0 -0
  86. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  87. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/pr_impact.py +0 -0
  88. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/prepare_context.py +0 -0
  89. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/progress.py +0 -0
  90. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/ranking_engine.py +0 -0
  91. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/redactor.py +0 -0
  92. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/relevance_scorer.py +0 -0
  93. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/rename_refactor.py +0 -0
  94. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/repo_classifier.py +0 -0
  95. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/ris.py +0 -0
  96. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/runtime_classifier.py +0 -0
  97. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/scanner.py +0 -0
  98. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/schema.py +0 -0
  99. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/security_config.py +0 -0
  100. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/semantic_analyzer.py +0 -0
  101. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/serializer.py +0 -0
  102. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_event_topology.py +0 -0
  103. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_findings.py +0 -0
  104. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_model.py +0 -0
  105. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_security_audit.py +0 -0
  106. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_semantic.py +0 -0
  107. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  108. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/summarizer.py +0 -0
  109. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/__init__.py +0 -0
  110. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/config.py +0 -0
  111. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/consent.py +0 -0
  112. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/events.py +0 -0
  113. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/filters.py +0 -0
  114. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/telemetry/transport.py +0 -0
  115. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/tree_utils.py +0 -0
  116. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/validation_surface.py +0 -0
  117. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/version_check.py +0 -0
  118. {sourcecode-1.69.0 → sourcecode-1.71.0}/src/sourcecode/workspace.py +0 -0
  119. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/functions/README.md +0 -0
  120. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/functions/get-license/index.ts +0 -0
  121. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  122. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/functions/telemetry/index.ts +0 -0
  123. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/sql/license_event_ordering.sql +0 -0
  124. {sourcecode-1.69.0 → sourcecode-1.71.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,129 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.71.0] — 2026-07-01
4
+
5
+ ### Fixed — framework-framing & migration reliability on a large non-Boot Spring repo (Jenkins core field test)
6
+
7
+ A migration/architecture audit of **jenkins-ci/jenkins** (`e7eef8d2cf`, branch
8
+ `master`; ~1,900 production Java classes, Maven multi-module, uses
9
+ `spring-security-web` but **no Spring MVC, no Spring Boot, no DI container** —
10
+ Jenkins runs on its own Stapler web framework and ACL/SecurityRealm security SPI)
11
+ surfaced five reproducible defects. In every case the engine already computed the
12
+ *correct* internal signal (downgraded confidence, `spring_present`, `not_detected`,
13
+ a prose `coverage_note`) but that signal did not propagate to the high-level field
14
+ an automated consumer reads first. Each fix targets the root cause and adds
15
+ regression coverage. All findings were verified against direct `grep` over the same
16
+ repo.
17
+
18
+ - **Framework detection — phantom "Spring MVC / Spring AOP" from pom exclusion blocks (BUG #1).**
19
+ `_frameworks_from_pom` serialized the *entire* pom to text and substring-matched
20
+ framework tokens, so `<exclude>org.springframework:spring-web</exclude>` and
21
+ `<exclude>…:spring-aop</exclude>` inside an enforcer bytecode rule in
22
+ `war/pom.xml` (artifact **exclusions**, not dependencies) read as "uses Spring
23
+ MVC / Spring AOP". Detection is now restricted to real dependency / plugin /
24
+ parent **coordinates** (`<exclusion>`/`<exclude>` and comments carry none) — the
25
+ same discipline the Gradle path already applied (JobRunr fix). The Spring Security
26
+ token was also broadened to the true-positive `spring-security-web` /
27
+ `-config` coordinates (Jenkins' real dep), so the honest label survives while the
28
+ phantoms disappear.
29
+ - **`architecture_summary` — categorical "MVC pattern with … view layers" on a repo with zero controllers (BUG #1).**
30
+ The `mvc` pattern is a directory-name heuristic. On a Java/Kotlin repo it is now
31
+ cross-checked against the canonical endpoint extractor in the same run; with no
32
+ HTTP controllers detected it degrades to a directory-derived "Layered code
33
+ organization (… — no HTTP controllers detected)" note, dropping the unverified
34
+ "view" claim rather than asserting MVC.
35
+ - **`migrate-check` — `boot3` dimension applicable without any Spring Boot (BUG #2).**
36
+ The Boot 2→3 axis was gated on `spring_present` (any `org.springframework.*`
37
+ coordinate), so a repo carrying only `spring-security-web` received a
38
+ contaminated `readiness_score` from a migration that does not exist. Applicability
39
+ now requires **positive evidence**: a `spring-boot*` coordinate / Gradle plugin
40
+ (`@SpringBootApplication` always ships one), **or** a concrete `spring_boot_3` /
41
+ `spring_security_6` finding in main code. jakarta findings alone no longer enable
42
+ boot3. A new `spring_boot_present` flag is surfaced; when N/A the dimension is
43
+ excluded from the aggregate with a clear reason.
44
+ - **`migrate-check` — `MIG-002` (javax.servlet) counted `@Deprecated` frozen-legacy shims as high blockers (BUG #3).**
45
+ A namespace-migration finding on a **class-level `@Deprecated`** type (kept for
46
+ binary compat, with a live replacement — Jenkins `ChainedServletFilter` →
47
+ `ChainedServletFilter2`) is now classified `code_context="deprecated_shim"` and
48
+ excluded from `blocking_count`, readiness, and effort, alongside the existing
49
+ test/generated buckets. A `@Deprecated` **method** on a live class stays blocking.
50
+ On Jenkins this dropped `blocking_count` from 6 to 2.
51
+ - **`repo-ir` — `security_model` was a bare `"unknown"` string with no reason (BUG #4).**
52
+ A structured `security_model_detail` companion now mirrors the `not_detected`
53
+ pattern used elsewhere: `status`, `spring_security_filter_chain_detected`,
54
+ `annotation_policies_detected`, `custom_spi_detected`, and a `reason` that
55
+ explicitly notes absence of a *recognized* pattern is not evidence the repo is
56
+ unsecured (Jenkins uses a custom ACL/SecurityRealm SPI).
57
+ - **`export --integrations` — low count carried no structured coverage flag (BUG #5).**
58
+ The honest coverage caveat lived only in the prose `coverage_note`. A structured
59
+ `coverage_confidence` (`low` / `partial` / `high`) plus `coverage_confidence_reason`
60
+ is now emitted: a large repo (≥300 source files) with <10 recognized constructs is
61
+ flagged `low` so a consumer cannot read a low count as low external coupling
62
+ (Jenkins' custom remoting/SCM/Update-Center SPIs are invisible to static matching).
63
+
64
+ New regression coverage across five suites; full suite green (2,900+ tests).
65
+
66
+ ## [1.70.0] — 2026-07-01
67
+
68
+ ### Fixed — reliability & internal-consistency on a large Spring 5 / Hibernate 5 repo (openmrs-core field test)
69
+
70
+ A full migration audit of **openmrs-core** (`7c0144ad2`, branch `2.9.x`; ~866
71
+ production files, Maven multi-module, Spring Framework 5 + Hibernate 5, classic
72
+ WAR, **no Spring Boot**) surfaced four reproducible defects where a headline value
73
+ was wrong or internally contradictory. Each fix targets the **root cause** and,
74
+ where certainty is not achievable, degrades to an explicit low-confidence / `null`
75
+ signal rather than asserting a concrete-but-false value. New regression coverage
76
+ across four suites.
77
+
78
+ - **`migrate-check` — Hibernate version confused a sibling artifact's version (BUG #1, CRITICAL).**
79
+ Hibernate ORM was resolved by scanning for *any* `<hibernate*version>` property
80
+ and keeping the **newest**, so on openmrs `${hibernateSearchVersion}=6.2.4.Final`
81
+ (Hibernate **Search**, a different artifact) beat `${hibernateVersion}=5.6.15.Final`
82
+ (Hibernate **ORM**). The tool declared the repo "already on Hibernate 6", set
83
+ `migration_applicable:false`, and silently **dropped the 28.9–95.6 person-day**
84
+ rewrite range from `estimated_effort_days` (under-reporting 1.5–2.6×). Resolution
85
+ is now **anchored to the `hibernate-core`/`hibernate-orm` dependency** — the
86
+ `<version>` on that coordinate, resolving a `${property}` to the *specific*
87
+ referenced property, and the Gradle coordinate. The property fallback (BOM-managed
88
+ repos) **excludes sibling artifacts** (Search/Validator/Envers/OGM/…). openmrs now
89
+ resolves `5.6.15.Final` / major 5 / `migration_applicable:true`, and
90
+ `estimated_effort_days` includes the Hibernate rewrite range via a new
91
+ `effort_breakdown` block. The same-name contradiction (`hibernate_readiness:100`
92
+ at root vs `0` nested) is removed: the nested field is renamed
93
+ **`rewrite_zone_readiness`** (raw rewrite-zone score, independent of applicability),
94
+ leaving the document-root `hibernate_readiness` as the single authoritative,
95
+ applicability-gated score.
96
+
97
+ - **`impact-chain` — a class's own methods counted as "direct callers" (BUG #2, HIGH).**
98
+ For a class-level seed, the BFS folds in every method key of the class, so internal
99
+ method→method calls leaked into `direct_callers`. On `ConceptServiceImpl` this
100
+ reported **24 direct callers (22 of them own members)**, inflating `risk_score` to
101
+ `20.0`/`high` — ~12× the real blast radius. Callers whose owning class is a seed
102
+ class are now excluded (they are *members*, not external callers); the count is
103
+ surfaced as `self_referential_excluded` plus an `analysis_warnings` entry.
104
+ `ConceptServiceImpl` now reports **2 external callers**, `risk_score` recomputed to
105
+ `8.5`/`medium`.
106
+
107
+ - **`export --integrations` — a user class classified as a JDK API by bare name (BUG #3, MEDIUM).**
108
+ `org.openmrs.util.HttpClient` (a thin wrapper over `java.net.HttpURLConnection`)
109
+ was labeled `jdk-httpclient` purely because its simple name matches
110
+ `java.net.http.HttpClient`. The client is now resolved by the **fully-qualified
111
+ import** — `jdk-httpclient` only when `java.net.http.HttpClient` is actually
112
+ imported and the file does not declare its own — otherwise it degrades to a
113
+ low-confidence **`custom-http-wrapper`**. Pure type-declaration sites (field,
114
+ parameter, return type such as `setImplementationIdHttpClient(HttpClient c)`) are
115
+ no longer emitted as network-invocation evidence; only constructions / static calls
116
+ count.
117
+
118
+ - **`--compact` — "rest api" headline unsupported by the `endpoints` command (BUG #4, LOW).**
119
+ The `architecture_summary` label is driven by HTTP-framework **presence** (Spring
120
+ MVC on the classpath), so openmrs was headlined "Java rest api" while the
121
+ authoritative `endpoints` command found **only 3 endpoints** (one a test module,
122
+ one medium-confidence `router_dsl`). The summary now consults the same endpoint
123
+ extractor and, below a high-confidence-endpoint threshold, degrades to a qualified,
124
+ consistent phrasing ("HTTP framework present; only N endpoints detected — see
125
+ `endpoints`") instead of overclaiming (Java/Kotlin repos only).
126
+
3
127
  ## [1.69.0] — 2026-06-30
4
128
 
5
129
  ### Fixed — framing & symbol-graph fidelity on a non-framework Java library (JobRunr field test)
@@ -57,8 +181,16 @@ suites.
57
181
  emits the misleading "No stereotype detected — may be a plain class or utility" for
58
182
  a structurally central non-annotated class: it infers a low-confidence stereotype
59
183
  from in-degree, lifecycle methods, and naming (e.g. `BackgroundJobServer` →
60
- "Likely server/orchestrator … lifecycle methods detected (start/stop)"). JobRunr
61
- `project_type` "api" "library", `frameworks` `["Quarkus"]` `[]`.
184
+ "Likely server/orchestrator … lifecycle methods detected (start/stop)"). Finally
185
+ the `architecture_summary` no longer prints a phantom "MVC pattern with
186
+ controller, model, view layers" for a non-web library: MVC now requires a real
187
+ View layer (it was inferred from a `handlers` dir + a `model` dir with no view),
188
+ a bundled SPA under `src/main/resources/.../frontend` is no longer counted as a
189
+ backend view layer, `testFixtures` source roots are excluded, and View detection
190
+ recognizes template files (`.html`/etc.) that the code-extension filter dropped.
191
+ JobRunr `project_type` "api" → "library", `frameworks` `["Quarkus"]` → `[]`,
192
+ architecture "MVC … controller, model, view" → "Layered … controller, repository,
193
+ infrastructure".
62
194
 
63
195
  ## [1.68.0] — 2026-06-30
64
196
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.69.0
3
+ Version: 1.71.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
@@ -411,7 +411,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
411
411
  |------|--------|
412
412
  | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
413
413
  | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
414
- | `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`) + `coverage_note`: a count of `0` means "no client construct found", **not** "no integrations" (runtime/DI-wired clients are not statically visible). |
414
+ | `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`), a structured `coverage_confidence` (`low` / `partial` / `high`) with `coverage_confidence_reason`, and a prose `coverage_note`: a count of `0` or a low count on a large repo (≥300 files, <10 constructs) is flagged `low`, so a low count is never read as low external coupling (custom protocol/SPI clients are not statically visible). |
415
415
  | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
416
416
 
417
417
  The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
@@ -734,9 +734,20 @@ evidence. `migrate-check` enforces:
734
734
  `hibernate_rewrite` headline. An **unresolved** version degrades to a
735
735
  low-confidence hypothesis with no headline blocker. See
736
736
  `hibernate.effective_version` / `version_major` / `version_confidence`.
737
- - **Boot3 auto-disables without Spring.** `boot3.applicable` tracks real Spring
738
- usage; Quarkus / Micronaut / Helidon / Jakarta-pure repos report `boot3` as N/A
739
- (`spring_present: false`), not a contradictory `applicable: true`.
737
+ - **Boot3 needs Spring _Boot_, not just Spring.** `boot3.applicable` requires
738
+ positive Spring Boot evidence a `spring-boot*` coordinate / Gradle plugin (or
739
+ `@SpringBootApplication`), **or** a concrete `spring_boot_3` / `spring_security_6`
740
+ finding. A repo that merely carries a Spring library (e.g. `spring-security-web`
741
+ with no Boot — Jenkins core) reports `boot3` as N/A with a reason and is excluded
742
+ from the aggregate; `spring_present: true` but `spring_boot_present: false`. Quarkus
743
+ / Micronaut / Helidon / Jakarta-pure repos likewise report `boot3` N/A. jakarta
744
+ findings alone never enable boot3.
745
+ - **Frozen-legacy `@Deprecated` shims aren't blockers.** A `javax→jakarta` finding
746
+ on a **class-level `@Deprecated`** type (kept for binary compat, replaced
747
+ elsewhere) is classified `code_context: "deprecated_shim"` and excluded from
748
+ `blocking_count` / readiness / effort — surfaced under `non_blocking` with a
749
+ "verify before removing" note. A `@Deprecated` **method** on a live class still
750
+ blocks.
740
751
  - **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
741
752
  (`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
742
753
  `javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
@@ -373,7 +373,7 @@ Emits **structured, tool-agnostic** codebase views as plain JSON/YAML — the ki
373
373
  |------|--------|
374
374
  | `--by-directory` | One group per source directory, each symbol with a `source_file:line` reference. |
375
375
  | `--module-graph` | `{nodes, edges, summary}` — directories as modules, inter-module dependencies rolled up from class-level relation edges with hit counts + edge types. |
376
- | `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`) + `coverage_note`: a count of `0` means "no client construct found", **not** "no integrations" (runtime/DI-wired clients are not statically visible). |
376
+ | `--integrations` | Outbound integrations across Spring **and** plain-Java/Jakarta stacks: HTTP (`RestTemplate`, `WebClient`, `@FeignClient`, JDK `HttpClient`, Apache, OkHttp), LDAP (`LdapTemplate`, JNDI `InitialDirContext`/`LdapContext`), SMTP (`JavaMailSender`, JavaMail `Transport`/`MimeMessage`), and JMS — with `file:line` evidence and a literal `target` when present. Emits an explicit `confidence` (`observed` / `not_analyzed`), a structured `coverage_confidence` (`low` / `partial` / `high`) with `coverage_confidence_reason`, and a prose `coverage_note`: a count of `0` or a low count on a large repo (≥300 files, <10 constructs) is flagged `low`, so a low count is never read as low external coupling (custom protocol/SPI clients are not statically visible). |
377
377
  | `--c4` | Unified document: `c4.{context, containers, components, code}` + `api_surface` + a `manifest` with per-directory content hashes for **incremental** consumers (skip directories whose hash is unchanged). `components.module_roots` rolls leaf source dirs up to architectural module roots and classifies each `layered` (DDD: ≥2 of `domain`/`application`/`infrastructure`) vs `flat` (legacy/flat package), with a verifiable `module_count` — so a consumer enumerates real modules instead of inferring boundaries from leaf directories. |
378
378
 
379
379
  The section flags compose (pass several for one multi-section document); `--c4` assembles the full export on its own. URLs assembled at runtime yield `target: null` (honest absence, never a guess); containers are derived from build files (Maven/Gradle) and reported as a limitation when none are found.
@@ -696,9 +696,20 @@ evidence. `migrate-check` enforces:
696
696
  `hibernate_rewrite` headline. An **unresolved** version degrades to a
697
697
  low-confidence hypothesis with no headline blocker. See
698
698
  `hibernate.effective_version` / `version_major` / `version_confidence`.
699
- - **Boot3 auto-disables without Spring.** `boot3.applicable` tracks real Spring
700
- usage; Quarkus / Micronaut / Helidon / Jakarta-pure repos report `boot3` as N/A
701
- (`spring_present: false`), not a contradictory `applicable: true`.
699
+ - **Boot3 needs Spring _Boot_, not just Spring.** `boot3.applicable` requires
700
+ positive Spring Boot evidence a `spring-boot*` coordinate / Gradle plugin (or
701
+ `@SpringBootApplication`), **or** a concrete `spring_boot_3` / `spring_security_6`
702
+ finding. A repo that merely carries a Spring library (e.g. `spring-security-web`
703
+ with no Boot — Jenkins core) reports `boot3` as N/A with a reason and is excluded
704
+ from the aggregate; `spring_present: true` but `spring_boot_present: false`. Quarkus
705
+ / Micronaut / Helidon / Jakarta-pure repos likewise report `boot3` N/A. jakarta
706
+ findings alone never enable boot3.
707
+ - **Frozen-legacy `@Deprecated` shims aren't blockers.** A `javax→jakarta` finding
708
+ on a **class-level `@Deprecated`** type (kept for binary compat, replaced
709
+ elsewhere) is classified `code_context: "deprecated_shim"` and excluded from
710
+ `blocking_count` / readiness / effort — surfaced under `non_blocking` with a
711
+ "verify before removing" note. A `@Deprecated` **method** on a live class still
712
+ blocks.
702
713
  - **Permanent `javax.*` are never jakarta debt.** JDK/JSR namespaces
703
714
  (`javax.cache`, `javax.sql`, `javax.xml` JAXP, `javax.crypto`, `javax.naming`,
704
715
  `javax.management`, `javax.security.auth`, `javax.annotation.processing`, …) are
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.69.0"
7
+ version = "1.71.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.69.0"
3
+ __version__ = "1.71.0"
@@ -38,7 +38,15 @@ _CODE_EXTENSIONS = {
38
38
  }
39
39
  _GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
40
40
 
41
- _TEST_DIRS: frozenset[str] = frozenset({"tests", "test", "spec", "specs", "__tests__", "e2e"})
41
+ _TEST_DIRS: frozenset[str] = frozenset({
42
+ "tests", "test", "spec", "specs", "__tests__", "e2e",
43
+ # Gradle/Maven test-fixture source roots are test code, not runtime architecture.
44
+ "testfixtures", "testfixture",
45
+ })
46
+ # Always-vendored asset trees — never a backend code layer at any depth.
47
+ _ASSET_DIRS: frozenset[str] = frozenset({
48
+ "node_modules", "bower_components",
49
+ })
42
50
  _BENCHMARK_DIRS: frozenset[str] = frozenset({
43
51
  "benchmark", "benchmarks", "bench",
44
52
  "example", "examples",
@@ -50,7 +58,7 @@ _BENCHMARK_DIRS: frozenset[str] = frozenset({
50
58
  _DOCS_DIRS: frozenset[str] = frozenset({"docs", "doc", "documentation", "wiki"})
51
59
  _TOOLING_DIRS: frozenset[str] = frozenset({"scripts", "script", "tools", "tool", "ci"})
52
60
  # All dirs that are not part of the runtime source architecture
53
- _NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS
61
+ _NON_SOURCE_DIRS: frozenset[str] = _TEST_DIRS | _BENCHMARK_DIRS | _DOCS_DIRS | _TOOLING_DIRS | _ASSET_DIRS
54
62
 
55
63
  # Exact file stems that signal a specific architectural layer
56
64
  _LAYER_STEM_EXACT: dict[str, str] = {
@@ -150,6 +158,15 @@ LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
150
158
  },
151
159
  }
152
160
 
161
+ # Layer keys a pattern MUST match to qualify, regardless of score. BUG (JobRunr
162
+ # field test): "mvc" was inferred from a controller-ish dir (`handlers`) plus a
163
+ # `model` dir, with NO view layer — that is not MVC (a library with no templates/
164
+ # pages/components is at most layered). MVC's defining trait is the View, so require
165
+ # it; without it the pattern falls through to layered / a weaker match.
166
+ _PATTERN_REQUIRED_KEYS: dict[str, frozenset[str]] = {
167
+ "mvc": frozenset({"view"}),
168
+ }
169
+
153
170
  # Higher value = wins when score ties
154
171
  _PATTERN_PRIORITY: dict[str, int] = {
155
172
  "cqrs": 8,
@@ -245,8 +262,12 @@ class ArchitectureAnalyzer:
245
262
  # Step 2: domain clustering
246
263
  domains = self._cluster_domains(filtered)
247
264
 
248
- # Step 3: layer detection
249
- pattern, layers = self._detect_layers(filtered)
265
+ # Step 3: layer detection. Feed the FULL path list (not the code-extension
266
+ # `filtered` set): a View layer is template files (.html/.jinja/.ejs…) that
267
+ # `filtered` would drop, hiding the very layer that distinguishes MVC.
268
+ # _detect_layers applies its own non-source/asset/test dir filtering, so
269
+ # bundled frontend trees and test fixtures are still excluded.
270
+ pattern, layers = self._detect_layers(sm.file_paths)
250
271
  if pattern in (None, "flat", "unknown"):
251
272
  if pattern == "flat":
252
273
  limitations.append("Layer pattern not detected: project has a flat directory structure")
@@ -518,10 +539,18 @@ class ArchitectureAnalyzer:
518
539
  return domains
519
540
 
520
541
  def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
521
- # Exclude non-source paths (tests, benchmarks, docs, tooling) from layer scoring
542
+ # Exclude non-source paths (tests, benchmarks, docs, tooling, vendored assets)
543
+ # from layer scoring. Also exclude anything under a `resources/` segment: in
544
+ # Maven/Gradle layouts `src/main/resources/**` is bundled config/assets — e.g.
545
+ # JobRunr ships a React dashboard SPA at
546
+ # `core/src/main/resources/org/jobrunr/dashboard/frontend/src/components`, whose
547
+ # `components` dir would otherwise be miscounted as a backend MVC "view" layer.
522
548
  source_paths = [
523
549
  p for p in paths
524
- if not any(part.lower() in _NON_SOURCE_DIRS for part in p.replace("\\", "/").split("/"))
550
+ if not any(
551
+ part.lower() in _NON_SOURCE_DIRS or part.lower() == "resources"
552
+ for part in p.replace("\\", "/").split("/")
553
+ )
525
554
  ]
526
555
  if not source_paths:
527
556
  return "unknown", []
@@ -544,6 +573,10 @@ class ArchitectureAnalyzer:
544
573
  matched_dirs = [d for d in dir_names if d in keywords]
545
574
  if matched_dirs:
546
575
  matched[layer_key] = matched_dirs
576
+ # A pattern with unmet required keys cannot qualify (e.g. mvc needs a view).
577
+ required = _PATTERN_REQUIRED_KEYS.get(pattern_name)
578
+ if required and not required.issubset(matched.keys()):
579
+ continue
547
580
  score = len(matched)
548
581
  priority = _PATTERN_PRIORITY.get(pattern_name, 0)
549
582
  if (score, priority) > (best_score, best_priority):
@@ -23,6 +23,14 @@ _JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
23
23
 
24
24
  _CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
25
25
 
26
+ # BUG #4 (v1.70.0): the "REST API" project-type label is driven by HTTP-framework
27
+ # PRESENCE (Spring MVC / JAX-RS on the classpath), not by an actual endpoint count.
28
+ # The `--compact` summary is the first (often only) thing an agent reads, so it must
29
+ # not assert "rest api" when the authoritative `endpoints` command finds almost no
30
+ # high-confidence surface. Below this many high-confidence endpoints we degrade the
31
+ # headline to a qualified, consistent phrasing instead of overclaiming.
32
+ _MIN_REST_ENDPOINTS_FOR_LABEL = 5
33
+
26
34
  _OPTIONAL_LABEL_MAP: dict[str, str] = {
27
35
  "DependencyAnalyzer": "dependencias",
28
36
  "GraphAnalyzer": "grafo de módulos",
@@ -41,6 +49,7 @@ class ArchitectureSummarizer:
41
49
 
42
50
  def __init__(self, root: Path) -> None:
43
51
  self.root = root
52
+ self._endpoint_support_cache: tuple[int, int] | None = None
44
53
 
45
54
  def generate(self, sm: SourceMap) -> str | None:
46
55
  try:
@@ -133,6 +142,7 @@ class ArchitectureSummarizer:
133
142
  # Line 2: architecture pattern + layers
134
143
  if arch and arch.pattern not in (None, "unknown", "flat"):
135
144
  arch_line = self._describe_arch_pattern(arch)
145
+ arch_line = self._qualify_web_pattern(arch, arch_line, sm)
136
146
  if arch_line:
137
147
  lines.append(arch_line)
138
148
 
@@ -185,9 +195,65 @@ class ArchitectureSummarizer:
185
195
 
186
196
  fw_str = f" using {', '.join(fw_names)}" if fw_names else ""
187
197
  if runtime:
198
+ # BUG #4: never assert "rest api" in the headline unless the endpoints
199
+ # command actually backs it (Java/Kotlin only — that is where we have an
200
+ # authoritative extractor). Degrade to a qualified, consistent phrasing.
201
+ if sm.project_type == "api" and primary.stack in {"java", "kotlin"}:
202
+ total, high = self._endpoint_support()
203
+ if high < _MIN_REST_ENDPOINTS_FOR_LABEL:
204
+ plural = "s" if total != 1 else ""
205
+ return (
206
+ f"{stack_label} application{fw_str} "
207
+ f"(HTTP framework present; only {total} endpoint{plural} "
208
+ f"detected — see `endpoints`)."
209
+ )
188
210
  return f"{stack_label} {runtime.lower()}{fw_str}."
189
211
  return f"{stack_label} project{fw_str}."
190
212
 
213
+ def _endpoint_support(self) -> tuple[int, int]:
214
+ """Return (total, high_confidence) endpoint counts from the canonical
215
+ Java endpoint extractor — the same source the `endpoints` command uses,
216
+ so the summary cannot diverge from it. Cached; failure degrades to (0, 0)."""
217
+ if self._endpoint_support_cache is not None:
218
+ return self._endpoint_support_cache
219
+ total, high = 0, 0
220
+ try:
221
+ from sourcecode.repository_ir import extract_java_endpoints
222
+ data = extract_java_endpoints(self.root)
223
+ eps = data.get("endpoints", [])
224
+ total = data.get("total", len(eps))
225
+ high = sum(1 for e in eps if (e.get("confidence") or "high") == "high")
226
+ except Exception:
227
+ total, high = 0, 0
228
+ self._endpoint_support_cache = (total, high)
229
+ return self._endpoint_support_cache
230
+
231
+ def _qualify_web_pattern(self, arch: Any, arch_line: str, sm: SourceMap) -> str:
232
+ """BUG #1 (Jenkins field test): the "mvc" pattern is a directory-name
233
+ heuristic (dirs matching controller/model/view keywords). On a Java/Kotlin
234
+ repo, do not assert "MVC pattern with ... view layers" in prose when the
235
+ canonical endpoint extractor finds no HTTP controllers in the same run —
236
+ Jenkins (Stapler web framework) matches model/view-ish dirs but declares
237
+ zero Spring MVC controllers. Cross-check against `endpoints` and degrade to
238
+ a consistent, non-committal phrasing instead of a categorical MVC claim."""
239
+ if not arch_line or arch.pattern not in ("mvc", "spring_mvc_layered"):
240
+ return arch_line
241
+ primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
242
+ if primary is None or primary.stack not in {"java", "kotlin"}:
243
+ return arch_line
244
+ _total, high = self._endpoint_support()
245
+ if high >= _MIN_REST_ENDPOINTS_FOR_LABEL:
246
+ return arch_line
247
+ # No controller backing → the MVC/view claim is unverified. Report the
248
+ # directory-derived layers without the framework label or the "view" claim.
249
+ layer_names = [l.name for l in arch.layers[:4] if l.name != "view"] if arch.layers else []
250
+ if layer_names:
251
+ return (
252
+ f"Layered code organization ({', '.join(layer_names)}; "
253
+ f"directory-based — no HTTP controllers detected)."
254
+ )
255
+ return ""
256
+
191
257
  def _describe_arch_pattern(self, arch: Any) -> str:
192
258
  pattern_labels = {
193
259
  "clean": "Clean Architecture",
@@ -284,10 +284,43 @@ class JavaDetector(AbstractDetector):
284
284
  if parent_artifact == "spring-boot-starter-parent":
285
285
  sb_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
286
286
 
287
- text = ElementTree.tostring(root_elem, encoding="unicode").lower()
287
+ # BUG #1 (Jenkins field test): framework tokens must be matched only inside
288
+ # genuine dependency / plugin / parent coordinates, never anywhere in the
289
+ # serialized pom. Jenkins' war/pom.xml lists `<exclude>...:spring-web</exclude>`
290
+ # and `<exclude>...:spring-aop</exclude>` in an enforcer-plugin bytecode rule
291
+ # — those are exclusions of transitive artifacts, not declared dependencies.
292
+ # Serializing the whole tree let them read as "uses Spring MVC / Spring AOP".
293
+ # Restrict the scan to real dependency/plugin/parent artifact coordinates;
294
+ # `<exclusion>` and `<exclude>` elements carry neither.
295
+ text = self._pom_coordinate_text(root_elem, ns)
288
296
  frameworks = self._detect_jvm_frameworks(text, "pom.xml", sb_version=sb_version)
289
297
  return frameworks
290
298
 
299
+ def _pom_coordinate_text(self, root_elem: ElementTree.Element, ns: str) -> str:
300
+ """Collect groupId:artifactId coordinates from real dependency, plugin, and
301
+ parent declarations only. Skips `<exclusion>`/`<exclude>` (transitive-artifact
302
+ removals) and comments, which must never be read as usage signal."""
303
+ coords: list[str] = []
304
+
305
+ def _coord(elem: ElementTree.Element) -> None:
306
+ gid = (elem.findtext(f"{ns}groupId") or "").strip()
307
+ aid = (elem.findtext(f"{ns}artifactId") or "").strip()
308
+ if aid:
309
+ coords.append(f"{gid}:{aid}" if gid else aid)
310
+
311
+ # <dependency> nodes anywhere (dependencies, dependencyManagement, profiles).
312
+ # ElementTree never yields <dependency> under <exclusions> (those are
313
+ # <exclusion>), so exclusion coordinates are structurally excluded.
314
+ for dep in root_elem.iter(f"{ns}dependency"):
315
+ _coord(dep)
316
+ for plugin in root_elem.iter(f"{ns}plugin"):
317
+ _coord(plugin)
318
+ parent_elem = root_elem.find(f"{ns}parent")
319
+ if parent_elem is not None:
320
+ _coord(parent_elem)
321
+
322
+ return "\n".join(coords).lower()
323
+
291
324
  def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
292
325
  original = "\n".join(read_text_lines(path))
293
326
  content = original.lower()
@@ -361,7 +394,12 @@ class JavaDetector(AbstractDetector):
361
394
  frameworks.append(FrameworkDetection(name="Thymeleaf", source=source))
362
395
  if "freemarker" in text:
363
396
  frameworks.append(FrameworkDetection(name="FreeMarker", source=source))
364
- if "spring-boot-starter-security" in text or "spring-security-core" in text:
397
+ if (
398
+ "spring-boot-starter-security" in text
399
+ or "spring-security-core" in text
400
+ or "spring-security-web" in text
401
+ or "spring-security-config" in text
402
+ ):
365
403
  frameworks.append(FrameworkDetection(name="Spring Security", source=source))
366
404
  if "spring-boot-starter-data-jpa" in text or "spring-data-jpa" in text:
367
405
  frameworks.append(FrameworkDetection(name="Spring Data JPA", source=source))