sourcecode 1.63.0__py3-none-any.whl → 1.65.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.63.0"
3
+ __version__ = "1.65.0"
@@ -36,7 +36,7 @@ from typing import Optional
36
36
 
37
37
 
38
38
  # Sub-schema version for the `hibernate` output section. Bump on shape changes.
39
- HIBERNATE_SCHEMA_VERSION = "2.0"
39
+ HIBERNATE_SCHEMA_VERSION = "2.1"
40
40
 
41
41
 
42
42
  # ---------------------------------------------------------------------------
@@ -107,14 +107,24 @@ _JPA_DEPRECATED_RE = re.compile(
107
107
  )
108
108
 
109
109
  # Layer 2 — JPA Criteria API + legacy Hibernate Criteria.
110
+ # BUG #1: dropped over-generic tokens (\bPredicate\b, \bRoot\s*<, \bCriterion\b,
111
+ # \bConjunction\b, \bDisjunction\b) that collide with project-owned domain classes
112
+ # (e.g. Alfresco's own query model has Predicate / Conjunction / Disjunction /
113
+ # Criterion classes with NO Hibernate dependency). Real JPA Criteria code always
114
+ # carries CriteriaBuilder / CriteriaQuery, so detection is preserved via those.
110
115
  _CRITERIA_JPA_RE = re.compile(
111
116
  r"\bCriteriaBuilder\b|\bCriteriaQuery\b|\bCriteriaUpdate\b|"
112
- r"\bCriteriaDelete\b|\bRoot\s*<|\bPredicate\b|persistence\.criteria"
117
+ r"\bCriteriaDelete\b|persistence\.criteria"
113
118
  )
119
+ # Legacy Hibernate Criteria. org.hibernate.Criteria / org.hibernate.criterion.*
120
+ # are FQN-anchored (safe). The bare-name tokens (createCriteria, Restrictions,
121
+ # Projections, DetachedCriteria) only count when the file actually imports
122
+ # org.hibernate (see _file_imports_hibernate gate) — they are otherwise common
123
+ # enough to collide with unrelated builder APIs.
114
124
  _CRITERIA_LEGACY_RE = re.compile(
115
- r"org\.hibernate\.Criteria\b|\.createCriteria\s*\(|\bDetachedCriteria\b|"
116
- r"\bRestrictions\.|\bProjections\.|\bCriterion\b|"
117
- r"\bConjunction\b|\bDisjunction\b"
125
+ r"org\.hibernate\.Criteria\b|org\.hibernate\.criterion\b|"
126
+ r"\.createCriteria\s*\(|\bDetachedCriteria\b|"
127
+ r"\bRestrictions\.|\bProjections\."
118
128
  )
119
129
 
120
130
  # Layer 3 — HQL / native string queries. createQuery requires a string literal so
@@ -131,20 +141,187 @@ _HQL_CONCAT_RE = re.compile(
131
141
  )
132
142
 
133
143
  # Layer 4 — Hibernate SPI / internal API (CRITICAL blocker).
134
- _SPI_RE = re.compile(
135
- r"\b(?:implements|extends)\s+\w*(?:UserType|CompositeUserType|UserCollectionType)\b|"
136
- r"\bimplements\s+\w*UserType\b|"
144
+ # BUG #1: split into FQN-anchored matches (proof of Hibernate origin — always
145
+ # counted) and bare-name matches (e.g. `implements XInterceptor`, `extends
146
+ # XEventListener`) that collide with project-owned AOP interceptors / event
147
+ # listeners. Bare-name matches only count when the file actually imports
148
+ # org.hibernate (see _file_imports_hibernate gate).
149
+ _SPI_FQN_RE = re.compile(
137
150
  r"\borg\.hibernate\.(?:type|engine|internal|persister|metamodel|"
138
- r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast)\b|"
151
+ r"boot\.spi|boot\.internal|event|tuple|property\.access|loader|sql\.ast|"
152
+ r"usertype)\b|"
139
153
  r"\bEmptyInterceptor\b|"
154
+ r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
155
+ r"\bSharedSessionContractImplementor\b"
156
+ )
157
+ _SPI_SIMPLE_RE = re.compile(
158
+ r"\b(?:implements|extends)\s+\w*(?:UserType|CompositeUserType|UserCollectionType)\b|"
140
159
  r"\bimplements\s+\w*Interceptor\b|"
141
160
  r"\b(?:implements|extends)\s+\w*EventListener\b|"
142
- r"\bSessionFactoryImpl\b|\bSessionImplementor\b|"
143
- r"\bSharedSessionContractImplementor\b|"
144
161
  r"\bsetPropertyAccessStrategy\b|\bPropertyAccessStrategy\b|"
145
162
  r"\bImplicitNamingStrategy\b|\bPhysicalNamingStrategy\b"
146
163
  )
147
164
 
165
+ # ---------------------------------------------------------------------------
166
+ # BUG #1 — comment/string stripping + real-import & dependency evidence
167
+ # ---------------------------------------------------------------------------
168
+ #
169
+ # A Hibernate 5→6 verdict is a high-cost blocker. It must be PROVEN from resolved
170
+ # AST signals, never from a substring inside a comment, Javadoc, string literal,
171
+ # or classpath resource path. Two guards enforce this:
172
+ # 1. _strip_comments_strings() — all pattern scanning runs on code with comments
173
+ # and string/char literal CONTENT removed (quotes/newlines preserved so line
174
+ # numbers and call-shape tokens survive).
175
+ # 2. _collect_hibernate_evidence() — detection only proceeds when there is real
176
+ # evidence: an org.hibernate:* build dependency, a parsed `import org.hibernate.*`,
177
+ # or a parsed `import {jakarta,javax}.persistence.*` (JPA provider in play).
178
+
179
+ _JAVA_TOKEN_RE = re.compile(
180
+ r"(//[^\n]*)" # line comment
181
+ r"|(/\*.*?\*/)" # block comment
182
+ r"|(\"(?:\\.|[^\"\\\n])*\")" # string literal
183
+ r"|('(?:\\.|[^'\\\n])*')", # char literal
184
+ re.DOTALL,
185
+ )
186
+
187
+
188
+ def _strip_comments_strings(src: str) -> str:
189
+ """Remove comment and string/char-literal CONTENT, preserving line count.
190
+
191
+ Line comments are blanked in place (the trailing newline is outside the match,
192
+ so line numbers are unaffected). Block comments are replaced by an equal number
193
+ of newlines. String/char literals collapse to empty `""` / `''` tokens so that
194
+ call-shape detection (e.g. createQuery("...")) and concatenation (`"" + x`) still
195
+ match, but no pattern can match a substring that lived inside a literal.
196
+ """
197
+ def _repl(m: re.Match) -> str:
198
+ if m.group(1) is not None:
199
+ return ""
200
+ if m.group(2) is not None:
201
+ return "\n" * m.group(2).count("\n")
202
+ if m.group(3) is not None:
203
+ return '""'
204
+ if m.group(4) is not None:
205
+ return "''"
206
+ return m.group(0)
207
+ return _JAVA_TOKEN_RE.sub(_repl, src)
208
+
209
+
210
+ # Real `import org.hibernate.*` — anchored to an import statement, not a substring.
211
+ _REAL_HIBERNATE_IMPORT_RE = re.compile(
212
+ r"^[ \t]*import\s+(?:static\s+)?org\.hibernate\.", re.MULTILINE
213
+ )
214
+ # Real JPA import (jakarta or javax persistence) — a JPA provider is in play.
215
+ _JPA_IMPORT_RE = re.compile(
216
+ r"^[ \t]*import\s+(?:static\s+)?(?:jakarta|javax)\.persistence\.", re.MULTILINE
217
+ )
218
+ # org.hibernate:* coordinate in a Maven/Gradle build descriptor.
219
+ _HIBERNATE_DEP_RE = re.compile(
220
+ r"<groupId>\s*org\.hibernate(?:\.orm)?\s*</groupId>"
221
+ r"|['\"]org\.hibernate(?:\.orm)?:",
222
+ re.IGNORECASE,
223
+ )
224
+ _BUILD_FILE_NAMES: tuple[str, ...] = ("pom.xml", "build.gradle", "build.gradle.kts")
225
+ _SKIP_BUILD_SCAN_DIRS: frozenset[str] = frozenset(
226
+ {"target", "build", ".git", ".gradle", "node_modules", "__pycache__", "out", "dist", "bin"}
227
+ )
228
+
229
+
230
+ def _file_imports_hibernate(stripped_source: str) -> bool:
231
+ return bool(_REAL_HIBERNATE_IMPORT_RE.search(stripped_source))
232
+
233
+
234
+ def _scan_build_dependency(root: Path) -> tuple[bool, list[str]]:
235
+ """Return (org.hibernate dependency present, sample coordinate strings)."""
236
+ import os
237
+ samples: list[str] = []
238
+ found = False
239
+ for dirpath, dirnames, filenames in os.walk(root):
240
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
241
+ for fname in filenames:
242
+ if fname not in _BUILD_FILE_NAMES:
243
+ continue
244
+ try:
245
+ text = (Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
246
+ except OSError:
247
+ continue
248
+ text = _strip_comments_strings(text) if fname.endswith((".gradle", ".kts")) else text
249
+ for m in _HIBERNATE_DEP_RE.finditer(text):
250
+ found = True
251
+ snippet = m.group(0).strip()
252
+ if snippet not in samples and len(samples) < 5:
253
+ samples.append(snippet)
254
+ return found, samples
255
+
256
+
257
+ # ── BUG #1: effective Hibernate version resolution ─────────────────────────
258
+ # A 5→6 rewrite verdict is only valid when the project is actually on Hibernate 5.
259
+ # Resolve the effective major version from build descriptors, following Maven
260
+ # ${properties} (incl. BOM-managed values declared as <hibernate*.version>) and
261
+ # explicit hibernate-core / hibernate-orm coordinates. When the resolved major is
262
+ # ≥ 6 the 5→6 axis does NOT apply (the migration is already done). When it cannot
263
+ # be resolved the axis degrades to a low-confidence hypothesis (no headline).
264
+ _HIB_VER = r"(\d+\.\d+(?:\.\d+)?(?:[.\-][\w]+)*)" # 6.2.13.Final, 5.6.15, 6.2
265
+ _HIB_PROP_RE = re.compile(
266
+ r"<((?:[\w.\-]*hibernate[\w.\-]*?)\.?version)>\s*" + _HIB_VER + r"\s*</\1>",
267
+ re.IGNORECASE,
268
+ )
269
+ _HIB_COORD_VERSION_RE = re.compile(
270
+ r"<artifactId>\s*hibernate-(?:core|orm)\s*</artifactId>\s*"
271
+ r"(?:<[^>]+>[^<]*</[^>]+>\s*)*?<version>\s*" + _HIB_VER,
272
+ re.IGNORECASE | re.DOTALL,
273
+ )
274
+ _HIB_GRADLE_VERSION_RE = re.compile(
275
+ r"org\.hibernate(?:\.orm)?:hibernate-(?:core|orm):" + _HIB_VER,
276
+ re.IGNORECASE,
277
+ )
278
+
279
+
280
+ def _ver_key(full: str) -> tuple[int, int]:
281
+ parts = full.split(".")
282
+ try:
283
+ major = int(parts[0])
284
+ except (ValueError, IndexError):
285
+ return (0, 0)
286
+ try:
287
+ minor = int(parts[1]) if len(parts) > 1 else 0
288
+ except ValueError:
289
+ minor = 0
290
+ return (major, minor)
291
+
292
+
293
+ def _resolve_hibernate_version(root: Path) -> tuple[Optional[str], Optional[int], str]:
294
+ """Return (full_version_string, major, confidence) for the effective Hibernate.
295
+
296
+ confidence is "high" when a version was resolved from a build descriptor,
297
+ "none" when none could be determined (degrade to hypothesis). The newest
298
+ declared Hibernate wins in a multi-module build.
299
+ """
300
+ import os
301
+ best_full: Optional[str] = None
302
+ for dirpath, dirnames, filenames in os.walk(root):
303
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_BUILD_SCAN_DIRS]
304
+ for fname in filenames:
305
+ if fname not in _BUILD_FILE_NAMES:
306
+ continue
307
+ try:
308
+ text = (Path(dirpath) / fname).read_text(encoding="utf-8", errors="replace")
309
+ except OSError:
310
+ continue
311
+ fulls: list[str] = []
312
+ for m in _HIB_PROP_RE.finditer(text):
313
+ fulls.append(m.group(2))
314
+ for m in _HIB_COORD_VERSION_RE.finditer(text):
315
+ fulls.append(m.group(1))
316
+ for m in _HIB_GRADLE_VERSION_RE.finditer(text):
317
+ fulls.append(m.group(1))
318
+ for full in fulls:
319
+ if best_full is None or _ver_key(full) > _ver_key(best_full):
320
+ best_full = full
321
+ if best_full is None:
322
+ return None, None, "none"
323
+ return best_full, _ver_key(best_full)[0], "high"
324
+
148
325
  # Escalation markers — dynamic / reflection-based persistence construction.
149
326
  _ABSTRACTION_CLASS_RE = re.compile(
150
327
  r"\b(DynamicEntityDao|DynamicEntityDaoImpl|BasicPersistenceModule|"
@@ -294,11 +471,27 @@ class HibernateStratification:
294
471
  total_effort_range_days: dict = field(default_factory=dict)
295
472
  effort_model: dict = field(default_factory=dict)
296
473
  findings: list[HibernateFinding] = field(default_factory=list)
474
+ # BUG #1: auditable proof behind the detected verdict. confidence is "high"
475
+ # when backed by a build dependency or a parsed org.hibernate import, "none"
476
+ # when detection was vetoed for lack of evidence.
477
+ evidence: dict = field(default_factory=dict)
478
+ # BUG #1: effective Hibernate version + whether the 5→6 axis applies at all.
479
+ # When the resolved major is ≥ 6 the migration is already done → not applicable
480
+ # (never a blocker). When unresolved, the axis is a low-confidence hypothesis.
481
+ effective_version: Optional[str] = None
482
+ version_major: Optional[int] = None
483
+ version_confidence: str = "none"
484
+ migration_applicable: bool = True
297
485
 
298
486
  def to_dict(self) -> dict:
299
487
  return {
300
488
  "schema_version": HIBERNATE_SCHEMA_VERSION,
301
489
  "detected": self.detected,
490
+ "evidence": self.evidence,
491
+ "effective_version": self.effective_version,
492
+ "version_major": self.version_major,
493
+ "version_confidence": self.version_confidence,
494
+ "migration_applicable": self.migration_applicable,
302
495
  "classification": self.classification,
303
496
  "classification_label": self.classification_label,
304
497
  "stratified": True,
@@ -607,15 +800,75 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
607
800
  concat_query_global = False
608
801
  incompatible: set[str] = set()
609
802
 
803
+ # ── BUG #1: evidence gate ───────────────────────────────────────────────
804
+ # Pre-read each Java source ONCE, strip comments + string/char literals, and
805
+ # record real-import evidence. Detection only proceeds when there is genuine
806
+ # proof of Hibernate/JPA — never a substring inside a comment or a classpath
807
+ # resource path (e.g. Alfresco's "org.hibernate.dialect.MySQLInnoDBDialect"
808
+ # appears only in Javadoc + iBatis resource directory names).
809
+ parsed: list[tuple[str, str, bool]] = [] # (rel_path, stripped_source, imports_hibernate)
810
+ any_hibernate_import = False
811
+ any_jpa_import = False
812
+ hibernate_import_sample: Optional[str] = None
610
813
  for rel_path in sorted(file_paths):
611
814
  if not rel_path.endswith(".java"):
612
815
  continue
613
816
  abs_path = root / rel_path
614
817
  try:
615
- source = abs_path.read_text(encoding="utf-8", errors="replace")
818
+ raw = abs_path.read_text(encoding="utf-8", errors="replace")
616
819
  except OSError:
617
820
  continue
821
+ source = _strip_comments_strings(raw)
822
+ imports_hib = _file_imports_hibernate(source)
823
+ if imports_hib:
824
+ any_hibernate_import = True
825
+ if hibernate_import_sample is None:
826
+ hm = _REAL_HIBERNATE_IMPORT_RE.search(source)
827
+ # capture the full import line for the evidence record
828
+ line = source[hm.start():].split("\n", 1)[0].strip().rstrip(";")
829
+ hibernate_import_sample = line
830
+ if _JPA_IMPORT_RE.search(source):
831
+ any_jpa_import = True
832
+ parsed.append((rel_path, source, imports_hib))
833
+
834
+ # No Java sources → nothing to stratify; skip the (potentially large) build walk.
835
+ if not parsed:
836
+ strat = HibernateStratification()
837
+ strat.detected = False
838
+ strat.classification = CLASS_NONE
839
+ strat.classification_label = _CLASS_LABELS[CLASS_NONE]
840
+ strat.evidence = {"dependency_present": False, "dependency_coordinates": [],
841
+ "hibernate_import_present": False, "hibernate_import_sample": None,
842
+ "jpa_import_present": False, "confidence": "none"}
843
+ return strat
618
844
 
845
+ dep_present, dep_samples = _scan_build_dependency(root)
846
+ eff_version, version_major, version_conf = _resolve_hibernate_version(root)
847
+
848
+ has_evidence = dep_present or any_hibernate_import or any_jpa_import
849
+ evidence: dict = {
850
+ "dependency_present": dep_present,
851
+ "dependency_coordinates": dep_samples,
852
+ "hibernate_import_present": any_hibernate_import,
853
+ "hibernate_import_sample": hibernate_import_sample,
854
+ "jpa_import_present": any_jpa_import,
855
+ "effective_version": eff_version,
856
+ "version_major": version_major,
857
+ "version_confidence": version_conf,
858
+ "confidence": "high" if has_evidence else "none",
859
+ }
860
+
861
+ if not has_evidence:
862
+ # No resolved Hibernate/JPA evidence → never a blocker. Any org.hibernate
863
+ # substring lives in comments/strings/classpath paths only.
864
+ strat = HibernateStratification()
865
+ strat.detected = False
866
+ strat.classification = CLASS_NONE
867
+ strat.classification_label = _CLASS_LABELS[CLASS_NONE]
868
+ strat.evidence = evidence
869
+ return strat
870
+
871
+ for rel_path, source, file_imports_hib in parsed:
619
872
  nl = _line_index(source)
620
873
  src_lines = source.split("\n")
621
874
  classes, methods = _index_symbols(source, nl)
@@ -736,7 +989,16 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
736
989
  "Hibernate 6 HQL parser changes")
737
990
 
738
991
  # ── Layer 4: Hibernate SPI / internal ──────────────────────────────
739
- spi_matches = list(_SPI_RE.finditer(source))
992
+ # FQN-anchored SPI matches are proof on their own. Bare-name matches
993
+ # (`implements XInterceptor`, `extends XEventListener`) only count when
994
+ # the file actually imports org.hibernate — otherwise they are the
995
+ # project's own AOP interceptors / event listeners (BUG #1).
996
+ spi_matches = list(_SPI_FQN_RE.finditer(source))
997
+ if file_imports_hib:
998
+ spi_matches = sorted(
999
+ spi_matches + list(_SPI_SIMPLE_RE.finditer(source)),
1000
+ key=lambda m: m.start(),
1001
+ )
740
1002
  if spi_matches:
741
1003
  spi_global = True
742
1004
  mod["has_spi"] = True
@@ -780,6 +1042,13 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
780
1042
 
781
1043
  strat = HibernateStratification()
782
1044
  strat.findings = findings
1045
+ strat.evidence = evidence
1046
+ strat.effective_version = eff_version
1047
+ strat.version_major = version_major
1048
+ strat.version_confidence = version_conf
1049
+ # BUG #1: the 5→6 axis only applies when NOT already on Hibernate 6+. A
1050
+ # resolved major ≥ 6 means the migration is complete — never a blocker.
1051
+ strat.migration_applicable = not (version_major is not None and version_major >= 6)
783
1052
  strat.rewrite_targets = sorted(
784
1053
  targets, key=lambda t: (t.source_file, t.line_start, t.layer, t.id)
785
1054
  )
@@ -789,6 +1058,8 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
789
1058
  )[:25]
790
1059
 
791
1060
  if not findings:
1061
+ # Evidence existed (dependency/import) but no concrete migration pattern
1062
+ # fired — Hibernate present yet nothing in the rewrite/upgrade surface.
792
1063
  strat.detected = False
793
1064
  strat.classification = CLASS_NONE
794
1065
  strat.classification_label = _CLASS_LABELS[CLASS_NONE]
@@ -945,13 +1216,23 @@ def analyze_hibernate(file_paths: list[str], root: Path) -> HibernateStratificat
945
1216
  stops.append("SQL/HQL shape not statically inferable (string concatenation)")
946
1217
  strat.stop_conditions_triggered = stops
947
1218
 
948
- if stops:
1219
+ if not strat.migration_applicable:
1220
+ # Already on Hibernate 6+ — the 5→6 stratification below is retained as an
1221
+ # informational exposure map, but it is NOT a migration and NOT a blocker.
1222
+ strat.classification = CLASS_NONE
1223
+ strat.classification_label = (
1224
+ f"ALREADY ON HIBERNATE {strat.version_major} "
1225
+ f"({strat.effective_version}) — NO 5→6 MIGRATION PENDING"
1226
+ )
1227
+ strat.stop_conditions_triggered = []
1228
+ elif stops:
949
1229
  strat.classification = CLASS_REWRITE
950
1230
  elif layer_files[LAYER_CRITERIA] or concat_query_global or jpa_deprecated_seen:
951
1231
  strat.classification = CLASS_UPGRADE_CARE
952
1232
  else:
953
1233
  strat.classification = CLASS_UPGRADE
954
- strat.classification_label = _CLASS_LABELS[strat.classification]
1234
+ if strat.migration_applicable:
1235
+ strat.classification_label = _CLASS_LABELS[strat.classification]
955
1236
 
956
1237
  # ── Risk separation: observable vs inferred ─────────────────────────────
957
1238
  observable: list[str] = []
@@ -35,10 +35,28 @@ _ATTR_NAME_RE = re.compile(r'(?:name|value)\s*=\s*"([^"]*)"')
35
35
  _FIRST_LITERAL_RE = re.compile(r'^\s*"([^"]*)"')
36
36
 
37
37
  # token -> (kind, client). Matched as whole-word usage outside imports/comments.
38
+ # Covers Spring AND plain-Java/Jakarta stacks (Quarkus, Micronaut, Keycloak SPI):
39
+ # the detector must not be Spring-centric, or a non-Spring repo with real LDAP /
40
+ # SMTP / HTTP integrations falsely reports "0 integrations".
38
41
  _TOKEN_CLIENTS: "tuple[tuple[str, str, str], ...]" = (
42
+ # Spring HTTP clients
39
43
  ("RestTemplate", "http", "resttemplate"),
40
44
  ("WebClient", "http", "webclient"),
45
+ # Plain-Java / third-party HTTP clients
46
+ ("HttpClient", "http", "jdk-httpclient"), # java.net.http.HttpClient
47
+ ("CloseableHttpClient", "http", "apache-httpclient"),
48
+ ("HttpClients", "http", "apache-httpclient"),
49
+ ("OkHttpClient", "http", "okhttp"),
50
+ # LDAP / directory (Spring + JNDI)
41
51
  ("LdapTemplate", "ldap", "ldaptemplate"),
52
+ ("InitialLdapContext", "ldap", "jndi-ldap"),
53
+ ("InitialDirContext", "ldap", "jndi-ldap"),
54
+ ("LdapContext", "ldap", "jndi-ldap"),
55
+ # Mail / SMTP (JavaMail / Jakarta Mail)
56
+ ("JavaMailSender", "smtp", "spring-mail"),
57
+ ("MimeMessage", "smtp", "javamail"),
58
+ ("Transport", "smtp", "javamail"),
59
+ # JMS / messaging
42
60
  ("JmsTemplate", "jms", "jmstemplate"),
43
61
  ("ActiveMQConnectionFactory", "jms", "activemq"),
44
62
  )
@@ -156,8 +174,21 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
156
174
  for r in records:
157
175
  by_kind[r["kind"]] = by_kind.get(r["kind"], 0) + 1
158
176
 
177
+ # BUG #9: honest confidence. A zero count means "no detectable client
178
+ # construct was found", NOT "this system has no integrations" — runtime-wired
179
+ # clients (DI, config-driven endpoints, JCA connectors) are invisible to static
180
+ # text matching. Report that explicitly instead of an authoritative "0".
181
+ confidence = "observed" if records else "not_analyzed"
159
182
  return {
160
183
  "integrations": records,
161
184
  "by_kind": {k: by_kind[k] for k in sorted(by_kind)},
162
185
  "count": len(records),
186
+ "confidence": confidence,
187
+ "coverage_note": (
188
+ "Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
189
+ "+ JNDI), SMTP (JavaMail), and JMS client constructs by source-text "
190
+ "matching. A count of 0 means no such construct was found, not that the "
191
+ "system has no outbound integrations — runtime/DI-wired clients are not "
192
+ "statically visible."
193
+ ),
163
194
  }