sourcecode 3.2.0__py3-none-any.whl → 3.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

@@ -1090,20 +1090,20 @@ class TaskContextBuilder:
1090
1090
  def _try_ris_fast_path(self, task_name: str, spec: "TaskSpec") -> Optional[TaskOutput]:
1091
1091
  """Return TaskOutput from a warm RIS without running the full scan.
1092
1092
 
1093
- Only activated for onboard/explain when git HEAD matches stored RIS.
1093
+ Only activated for onboard/explain when the stored RIS describes the tree
1094
+ on disk — the same freshness authority every cache layer keys on, not the
1095
+ committed HEAD (which served this fast path through uncommitted edits).
1094
1096
  Falls through (returns None) on any error or cache miss.
1095
1097
  """
1096
1098
  try:
1097
- import subprocess as _sp
1098
1099
  from sourcecode.ris import load_ris as _lris
1100
+ from sourcecode.ris import _tree_signature as _ris_sig
1099
1101
  _ris = _lris(self.root)
1100
1102
  if _ris is None or not _ris.compact_summary:
1101
1103
  return None
1102
- _r = _sp.run(
1103
- ["git", "-C", str(self.root), "rev-parse", "--short", "HEAD"],
1104
- capture_output=True, text=True, timeout=2,
1105
- )
1106
- if _r.returncode != 0 or _r.stdout.strip() != _ris.git_head:
1104
+ _stored_sig = (_ris.metadata or {}).get("tree_signature") or ""
1105
+ _current_sig = _ris_sig(self.root)
1106
+ if not _stored_sig or not _current_sig or _stored_sig != _current_sig:
1107
1107
  return None
1108
1108
 
1109
1109
  compact = _ris.compact_summary
@@ -1905,17 +1905,46 @@ class TaskContextBuilder:
1905
1905
  for p in test_set
1906
1906
  } | {Path(p).stem for p in test_set}
1907
1907
  _untested_changed = [f for f in _changed_src if Path(f).stem not in _test_stems]
1908
- _test_risk_level = (
1909
- "high" if len(_untested_changed) > 3
1910
- else "medium" if _untested_changed
1911
- else "low"
1912
- )
1908
+ # A diff that changes no source file has not been assessed for test
1909
+ # coverage — it is not low risk. Reported as `low`, a documentation-only
1910
+ # diff in a repository with no tests at all read as "covered", which is
1911
+ # the traffic light inverted where a reviewer relies on it (ADR-0008 R9:
1912
+ # `unknown` where the honest answer is unknown, never a manufactured
1913
+ # best case).
1914
+ if not _changed_src:
1915
+ _test_risk_level = "unknown"
1916
+ _test_risk_basis = (
1917
+ "no source files changed in this diff — test coverage risk "
1918
+ "not assessed"
1919
+ )
1920
+ else:
1921
+ _test_risk_level = (
1922
+ "high" if len(_untested_changed) > 3
1923
+ else "medium" if _untested_changed
1924
+ else "low"
1925
+ )
1926
+ _test_risk_basis = (
1927
+ f"{len(_untested_changed)} of {len(_changed_src)} changed source "
1928
+ f"files have no matching test file by stem"
1929
+ )
1913
1930
  _pr_test_coverage_risk = {
1914
1931
  "changed_files_without_tests": _untested_changed[:10],
1915
1932
  "risk_level": _test_risk_level,
1916
1933
  "epistemic_level": "INFERRED (LOW CONFIDENCE)",
1917
- "basis": f"{len(_untested_changed)} changed source files have no matching test file by stem",
1934
+ "basis": _test_risk_basis,
1935
+ "changed_source_files": len(_changed_src),
1918
1936
  }
1937
+ if not test_set:
1938
+ # The repository declares no test sources at all: "has a matching
1939
+ # test file by stem" cannot mean anything here, and a `low` would
1940
+ # be read as coverage.
1941
+ _pr_test_coverage_risk["repository_has_test_sources"] = False
1942
+ if _test_risk_level == "low":
1943
+ _pr_test_coverage_risk["risk_level"] = "unknown"
1944
+ _pr_test_coverage_risk["basis"] = (
1945
+ "repository declares no test sources — coverage of the "
1946
+ "changed files cannot be assessed"
1947
+ )
1919
1948
 
1920
1949
  # Pre-classify changed files once — reused for hotspots, order, and runtime/build split
1921
1950
  _pr_changed_cls: dict[str, dict] = {
@@ -3331,19 +3360,15 @@ class TaskContextBuilder:
3331
3360
  return [f for _, _, f in scored[:15]]
3332
3361
 
3333
3362
  def _is_test(self, path: str) -> bool:
3334
- name = Path(path).name.lower()
3335
- return (
3336
- name.startswith("test_")
3337
- or name.endswith("_test.py")
3338
- or name.endswith(".test.ts")
3339
- or name.endswith(".spec.ts")
3340
- or "/tests/" in path
3341
- or "/test/" in path
3342
- or "/spec/" in path
3343
- or (name.endswith("test.java") and name != "test.java")
3344
- or name.endswith("tests.java")
3345
- or (name.startswith("test") and name.endswith(".java") and len(name) > 9)
3346
- )
3363
+ """Delegates to the one authority (ADR-0008 R1).
3364
+
3365
+ This used to match `/test/` anywhere in the path, so production code in
3366
+ a package named `test` under `src/main` counted as a test — inflating
3367
+ coverage in the one direction a reviewer must not be misled about.
3368
+ """
3369
+ from sourcecode.test_sources import is_test_path
3370
+
3371
+ return is_test_path(path)
3347
3372
 
3348
3373
  def _is_source(self, path: str) -> bool:
3349
3374
  return Path(path).suffix.lower() in _SOURCE_EXTENSIONS
@@ -0,0 +1,307 @@
1
+ """reference_facts.py — one authority for "is this type referenced anywhere?".
2
+
3
+ A static call-graph answers "who calls this class" for the calls it can see. It
4
+ cannot see a class a framework instantiates by reflection, wires by name from
5
+ XML, discovers through the service-loader, or dispatches into because of an
6
+ annotation. Those are not edge cases in a Spring/Jakarta repository: they are how
7
+ the entry points work.
8
+
9
+ So this question has **three** answers, not two:
10
+
11
+ * ``referenced`` — the graph carries an incoming edge, or
12
+ another source file names the type
13
+ * ``unknown_framework_dispatch`` — no static caller, but the type carries a
14
+ dispatch signal (published annotation,
15
+ entry-point signature, or its name wired
16
+ from a configuration file). Whether it runs
17
+ is not decidable here
18
+ * ``no_static_callers`` — no incoming edge and no dispatch signal
19
+ found. Still **not** "dead code": it is the
20
+ absence of evidence, stated as such
21
+
22
+ The previous shape of this fact had two consequences the field caught. It counted
23
+ only types with **no edges in either direction**, while calling the result "zero
24
+ static callers" — a class that calls something but is called by nothing was never
25
+ examined. And when nothing survived that narrower filter it published ``0``, which
26
+ reads as "there is no unreferenced code here" rather than "nothing was found by a
27
+ check that cannot see framework dispatch" (ADR-0008 R9, R5-R8: the count comes
28
+ from the measurement, never from the length of a truncated list).
29
+
30
+ VAI: annotation and configuration names appear only as published open-standard
31
+ DATA (Jakarta / Spring / Java SE vocabulary), the same class of table as the
32
+ security-posture matchers. Nothing branches on a project, vendor or product name,
33
+ and a name found in configuration is recorded as evidence of a possible wire —
34
+ never as proof that a class is alive.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import os
39
+ import re
40
+ from dataclasses import dataclass, field
41
+ from pathlib import Path
42
+
43
+ #: Statuses, in the order a reader cares about them. `unknown_framework_dispatch`
44
+ #: sits between the two decided answers because that is what it means.
45
+ REFERENCED = "referenced"
46
+ UNKNOWN_DISPATCH = "unknown_framework_dispatch"
47
+ NO_STATIC_CALLERS = "no_static_callers"
48
+ STATUSES: tuple[str, ...] = (REFERENCED, UNKNOWN_DISPATCH, NO_STATIC_CALLERS)
49
+
50
+ #: Published signatures and annotations that mean "a framework calls this, not
51
+ #: your code". DATA: a table of open-standard names, extended without changing
52
+ #: any logic. A hit makes the answer *unknown*, never "alive".
53
+ _DYNAMIC_ENTRY_SIGNATURE_RE = re.compile(
54
+ r"\(\s*DispatchContext\b" # service-engine service
55
+ r"|HttpServletRequest\s+\w+\s*,\s*HttpServletResponse" # event / servlet handler
56
+ r"|\bpublic\s+static\s+void\s+main\s*\(" # JVM entry point
57
+ r"|@(?:Scheduled|PostConstruct|PreDestroy|EventListener|Bean|"
58
+ r"RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|"
59
+ r"Path|GET|POST|Provider|ApplicationScoped|Singleton|"
60
+ # Stereotypes and persistence: the container instantiates these itself, so a
61
+ # static call-graph is the wrong instrument to declare them unused.
62
+ r"Component|Service|Repository|Controller|RestController|ControllerAdvice|"
63
+ r"Configuration|SpringBootApplication|Aspect|Entity|Embeddable|"
64
+ r"MappedSuperclass|Converter|WebServlet|WebFilter|WebListener)\b"
65
+ )
66
+
67
+ #: Configuration formats where a framework wires a class by name.
68
+ _CONFIG_REF_EXTS: frozenset = frozenset({".xml", ".properties", ".yml", ".yaml", ".groovy"})
69
+ _SKIP_DIRS: frozenset = frozenset({".git", "build", "out", "target", "node_modules", ".gradle"})
70
+ _SCAN_MAX_FILES: int = 12000
71
+ _SCAN_MAX_BYTES: int = 256 * 1024
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class TypeReference:
76
+ """One type's reference status, with the evidence that produced it."""
77
+
78
+ fqn: str
79
+ status: str
80
+ basis: str
81
+ type: str = ""
82
+ role: str = ""
83
+
84
+ def to_dict(self) -> dict:
85
+ return {
86
+ "fqn": self.fqn,
87
+ "type": self.type,
88
+ "role": self.role,
89
+ "status": self.status,
90
+ "basis": self.basis,
91
+ }
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class ReferenceFacts:
96
+ """Every type examined, partitioned into the three answers.
97
+
98
+ `by_status` sums to `classes_examined` — the partition is the point. Counts
99
+ are computed here, so no consumer can derive one from a list it truncated
100
+ for display.
101
+ """
102
+
103
+ classes_examined: int = 0
104
+ by_status: "dict[str, int]" = field(default_factory=dict)
105
+ entries: "tuple[TypeReference, ...]" = ()
106
+
107
+ def of_status(self, status: str) -> "list[TypeReference]":
108
+ return [e for e in self.entries if e.status == status]
109
+
110
+ def count(self, status: str) -> int:
111
+ return int(self.by_status.get(status, 0))
112
+
113
+ @property
114
+ def statement(self) -> str:
115
+ """What the numbers mean, in the words that keep them honest."""
116
+ unknown = self.count(UNKNOWN_DISPATCH)
117
+ none_found = self.count(NO_STATIC_CALLERS)
118
+ if not self.classes_examined:
119
+ return "no class or interface declarations were examined"
120
+ if none_found == 0 and unknown:
121
+ return (
122
+ f"no type is without a caller AND without a dispatch signal; "
123
+ f"{unknown} type(s) have no static caller but carry one, so whether "
124
+ "they are reachable is not decidable from source"
125
+ )
126
+ return (
127
+ f"{none_found} type(s) have no static caller and no dispatch signal "
128
+ f"found; {unknown} more have no static caller but carry one. Neither "
129
+ "group is confirmed dead code"
130
+ )
131
+
132
+ def to_dict(self) -> dict:
133
+ return {
134
+ "classes_examined": self.classes_examined,
135
+ **{s: self.count(s) for s in STATUSES},
136
+ "statement": self.statement,
137
+ "note": (
138
+ "A partition of every class/interface declaration: the three counts "
139
+ "sum to classes_examined. `no_static_callers` means no incoming edge "
140
+ "and no dispatch signal was found — absence of evidence, not evidence "
141
+ "of dead code. `unknown_framework_dispatch` means a framework may "
142
+ "invoke it (annotation, entry-point signature, or a name wired from "
143
+ "configuration) and a static call-graph cannot decide."
144
+ ),
145
+ }
146
+
147
+
148
+ def _read(path: str, limit: int = _SCAN_MAX_BYTES) -> str:
149
+ try:
150
+ with open(path, "r", encoding="utf-8", errors="replace") as handle:
151
+ return handle.read(limit)
152
+ except OSError:
153
+ return ""
154
+
155
+
156
+ #: Identifiers, and `Outer.Nested` qualified references. Two fixed patterns whose
157
+ #: hits are intersected with the candidate names — an alternation over the
158
+ #: candidates instead is 7× slower on a 6 000-type repository and answers the same.
159
+ _IDENTIFIER_RE = re.compile(r"[A-Za-z_$][A-Za-z0-9_$]*")
160
+ _QUALIFIED_TYPE_RE = re.compile(r"\b[A-Z][A-Za-z0-9_$]*\.[A-Z][A-Za-z0-9_$]*")
161
+
162
+
163
+ def _scan_repository(
164
+ root: Path, simple_names: "set[str]", nested: "dict[str, tuple[str, str]]"
165
+ ) -> "tuple[dict[str, str], dict[str, str]]":
166
+ """One walk, two questions — `(config-wired names, nested types other code names)`.
167
+
168
+ Both scans need the whole tree, and walking it twice doubled the cost of the
169
+ command's slowest step for no answer that a single pass cannot give.
170
+
171
+ Names are matched whole-word, never as substrings: `Order` appearing inside
172
+ `OrderItemImpl` is not a wire, and treating it as one silently marks a
173
+ live-looking class dispatched. A type naming itself proves nothing, so its own
174
+ source file is skipped.
175
+ """
176
+ config_hits: dict[str, str] = {}
177
+ nested_hits: dict[str, str] = {}
178
+ pending = set(simple_names)
179
+ by_qualified: dict[str, list[str]] = {}
180
+ for fqn, (qualified, _own) in nested.items():
181
+ by_qualified.setdefault(qualified, []).append(fqn)
182
+
183
+ scan_config = bool(pending)
184
+ scan_java = bool(by_qualified)
185
+ if not scan_config and not scan_java:
186
+ return config_hits, nested_hits
187
+
188
+ scanned = 0
189
+ for dirpath, dirnames, filenames in os.walk(root):
190
+ dirnames[:] = [d for d in dirnames if d not in _SKIP_DIRS]
191
+ for filename in filenames:
192
+ is_java = filename.endswith(".java")
193
+ is_config = os.path.splitext(filename)[1].lower() in _CONFIG_REF_EXTS
194
+ if not ((is_java and scan_java) or (is_config and scan_config)):
195
+ continue
196
+ if scanned >= _SCAN_MAX_FILES:
197
+ break
198
+ full = os.path.join(dirpath, filename)
199
+ text = _read(full)
200
+ scanned += 1
201
+ if not text:
202
+ continue
203
+ rel = os.path.relpath(full, root).replace("\\", "/")
204
+ if is_config and scan_config:
205
+ for hit in set(_IDENTIFIER_RE.findall(text)) & pending:
206
+ pending.discard(hit)
207
+ config_hits[hit] = rel
208
+ scan_config = bool(pending)
209
+ if is_java and scan_java:
210
+ for hit in set(_QUALIFIED_TYPE_RE.findall(text)) & set(by_qualified):
211
+ for fqn in by_qualified[hit]:
212
+ if fqn not in nested_hits and nested[fqn][1] != rel:
213
+ nested_hits[fqn] = rel
214
+ scan_java = len(nested_hits) < len(nested)
215
+ if not scan_config and not scan_java:
216
+ return config_hits, nested_hits
217
+ if scanned >= _SCAN_MAX_FILES:
218
+ break
219
+ return config_hits, nested_hits
220
+
221
+
222
+ def analyze_type_references(nodes: "list[dict]", root: Path) -> ReferenceFacts:
223
+ """The reference status of every class/interface node, and why.
224
+
225
+ `nodes` is the graph's node list; only class and interface declarations are
226
+ examined, because a method with no callers is a different question.
227
+ """
228
+ root = Path(root)
229
+ types = [n for n in nodes or [] if n.get("type") in ("class", "interface") and n.get("fqn")]
230
+ if not types:
231
+ return ReferenceFacts(0, {s: 0 for s in STATUSES}, ())
232
+
233
+ entries: list[TypeReference] = []
234
+ candidates: list[dict] = []
235
+ for node in types:
236
+ if int(node.get("in_degree") or 0) > 0:
237
+ entries.append(TypeReference(
238
+ fqn=str(node["fqn"]), status=REFERENCED,
239
+ basis="incoming edge in the call-graph",
240
+ type=str(node.get("type") or ""), role=str(node.get("role") or "other"),
241
+ ))
242
+ else:
243
+ candidates.append(node)
244
+
245
+ # 1. A dispatch signal in the type's own source.
246
+ dispatch_basis: dict[str, str] = {}
247
+ for node in candidates:
248
+ source = node.get("source_file")
249
+ if not source:
250
+ continue
251
+ text = _read(str(root / str(source)))
252
+ if text and _DYNAMIC_ENTRY_SIGNATURE_RE.search(text):
253
+ dispatch_basis[str(node["fqn"])] = (
254
+ f"a framework-dispatch annotation or entry-point signature in {source}"
255
+ )
256
+
257
+ # 2. The type's name wired from a configuration file, and 3. a nested type
258
+ # another source names as `Outer.Nested` — both answered in one walk.
259
+ by_simple: dict[str, list[dict]] = {}
260
+ nested_candidates: dict[str, tuple[str, str]] = {}
261
+ for node in candidates:
262
+ fqn = str(node["fqn"])
263
+ if fqn in dispatch_basis:
264
+ continue
265
+ simple = fqn.rsplit(".", 1)[-1]
266
+ if simple:
267
+ by_simple.setdefault(simple, []).append(node)
268
+ parts = fqn.split(".")
269
+ if len(parts) >= 2 and parts[-2][:1].isupper():
270
+ nested_candidates[fqn] = (
271
+ f"{parts[-2]}.{parts[-1]}",
272
+ str(node.get("source_file") or "").replace("\\", "/"),
273
+ )
274
+ config_hits, nested_hits = _scan_repository(root, set(by_simple), nested_candidates)
275
+ for simple, config_file in config_hits.items():
276
+ for node in by_simple.get(simple, []):
277
+ dispatch_basis[str(node["fqn"])] = f"named in {config_file}"
278
+
279
+ for node in candidates:
280
+ fqn = str(node["fqn"])
281
+ common = {
282
+ "fqn": fqn,
283
+ "type": str(node.get("type") or ""),
284
+ "role": str(node.get("role") or "other"),
285
+ }
286
+ if fqn in nested_hits:
287
+ entries.append(TypeReference(
288
+ status=REFERENCED,
289
+ basis=f"named as a qualified nested type in {nested_hits[fqn]}",
290
+ **common,
291
+ ))
292
+ elif fqn in dispatch_basis:
293
+ entries.append(TypeReference(
294
+ status=UNKNOWN_DISPATCH, basis=dispatch_basis[fqn], **common,
295
+ ))
296
+ else:
297
+ entries.append(TypeReference(
298
+ status=NO_STATIC_CALLERS,
299
+ basis="no incoming edge, and no dispatch signal was found",
300
+ **common,
301
+ ))
302
+
303
+ entries.sort(key=lambda e: (STATUSES.index(e.status), e.fqn))
304
+ counts = {status: sum(1 for e in entries if e.status == status) for status in STATUSES}
305
+ return ReferenceFacts(
306
+ classes_examined=len(types), by_status=counts, entries=tuple(entries),
307
+ )
sourcecode/ris.py CHANGED
@@ -258,6 +258,7 @@ def _build_from_core(
258
258
  1.0 if agent_data else (0.5 if compact_data else 0.0)
259
259
  ),
260
260
  "partial": not bool(agent_data),
261
+ "tree_signature": _tree_signature(repo_root),
261
262
  },
262
263
  )
263
264
 
@@ -295,6 +296,7 @@ def maybe_update_ris(repo_root: Path, core_dict: dict, git_head: str) -> None:
295
296
  "snapshot_source": "existing_snapshot_system",
296
297
  "confidence": float(1.0 if agent_data else (0.5 if compact_data else 0.0)),
297
298
  "partial": not bool(agent_data),
299
+ "tree_signature": _tree_signature(repo_root),
298
300
  },
299
301
  )
300
302
  save_ris(repo_root, updated)
@@ -433,6 +435,21 @@ def _current_git_head(repo_root: Path) -> str:
433
435
  return ""
434
436
 
435
437
 
438
+ def _tree_signature(repo_root: Path) -> str:
439
+ """Tree state this snapshot describes — the one freshness authority.
440
+
441
+ Stored in ``metadata["tree_signature"]`` at write time so staleness compares
442
+ the tree the RIS was built from against the tree on disk now. HEAD alone
443
+ called a snapshot fresh while uncommitted edits made it describe nothing.
444
+ """
445
+ try:
446
+ from sourcecode.cache import worktree_signature
447
+
448
+ return worktree_signature(repo_root)
449
+ except Exception:
450
+ return ""
451
+
452
+
436
453
  def _has_uncommitted_changes(repo_root: Path) -> bool:
437
454
  """Return True if working tree has staged or unstaged changes to tracked files.
438
455
 
@@ -465,8 +482,17 @@ def get_cold_start_context(repo_root: Path) -> dict:
465
482
  return {"status": "no_ris"}
466
483
 
467
484
  current_head = _current_git_head(repo_root)
468
- stale = bool(current_head and ris.git_head and current_head != ris.git_head)
469
485
  uncommitted = _has_uncommitted_changes(repo_root)
486
+ # Staleness is about the tree the snapshot describes, not the commit it was
487
+ # taken at: with HEAD alone a snapshot stayed `stale: false` through every
488
+ # uncommitted edit. Snapshots written before the signature existed fall back
489
+ # to the pair of facts that were available then.
490
+ _stored_sig = (ris.metadata or {}).get("tree_signature") or ""
491
+ _current_sig = _tree_signature(repo_root)
492
+ if _stored_sig and _current_sig:
493
+ stale = _stored_sig != _current_sig
494
+ else:
495
+ stale = bool(current_head and ris.git_head and current_head != ris.git_head) or uncommitted
470
496
 
471
497
  endpoints = ris.api_surface.get("endpoints", [])
472
498
  _is_java = (
sourcecode/serializer.py CHANGED
@@ -564,19 +564,30 @@ def _spring_profiles_context(sm: "SourceMap") -> "Optional[dict[str, Any]]":
564
564
  # profile is routinely the security filter chain.
565
565
  _declared: list[str] = []
566
566
  _conditional: dict[str, list[str]] = {}
567
+ _conditional_beans: list[dict[str, Any]] = []
567
568
  _root = getattr(sm.metadata, "analyzed_path", None)
568
569
  if _root:
569
570
  try:
570
571
  from sourcecode.spring_profiles import (
571
- conditional_beans_from_sources,
572
+ conditional_beans_from_sources_detailed,
572
573
  profiles_from_config,
573
574
  )
574
575
 
575
- # One source pass: the bean map's keys ARE the annotation-declared
576
- # profile names, so there is no second scan for the name list.
577
- _conditional = conditional_beans_from_sources(
576
+ # One source pass, one fact: every rendering below is a projection
577
+ # of this list, so the map cannot disagree with the expressions
578
+ # (ADR-0008 R1). The keys ARE the annotation-declared profile
579
+ # names, so there is no second scan for the name list either.
580
+ _beans = conditional_beans_from_sources_detailed(
578
581
  Path(_root), list(sm.file_paths or [])
579
582
  )
583
+ _conditional_beans = [b.to_dict() for b in _beans]
584
+ _by_profile: dict[str, set[str]] = {}
585
+ for _bean in _beans:
586
+ for _token in _bean.profiles:
587
+ _by_profile.setdefault(_token.strip(), set()).add(_bean.symbol)
588
+ _conditional = {
589
+ _p: sorted(_names) for _p, _names in sorted(_by_profile.items())
590
+ }
580
591
  _declared = sorted(set(
581
592
  profiles_from_config(Path(_root))
582
593
  + [p.lstrip("!").strip() for p in _conditional]
@@ -619,6 +630,19 @@ def _spring_profiles_context(sm: "SourceMap") -> "Optional[dict[str, Any]]":
619
630
  result["declared_in_code_or_config"] = _declared
620
631
  if _conditional:
621
632
  result["profile_conditional_beans"] = _conditional
633
+ if _conditional_beans:
634
+ # The map above buckets by token, so `@Profile("a & !b")` appears
635
+ # under both `a` and `!b` with the operator lost. This list keeps
636
+ # the expression as written — it is what `ask posture` resolves,
637
+ # and the only rendering here that can be read as a condition.
638
+ result["conditional_beans"] = _conditional_beans
639
+ result["conditional_beans_note"] = (
640
+ "Symbols carrying @Profile, with the expression as written. "
641
+ "A symbol listed under a token is conditional on that token as "
642
+ "written — '!m3' means active when m3 is ABSENT. Whether a bean "
643
+ "is actually wired under a given profile set is resolved by "
644
+ "`ask posture --profile <set>`, which is the authority."
645
+ )
622
646
  result["detection_basis"] = [
623
647
  "application-{profile} file naming",
624
648
  "spring.config.activate.on-profile / spring.profiles in config documents",
@@ -2211,11 +2235,14 @@ def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
2211
2235
  if sm.code_notes_summary.adr_count > 0:
2212
2236
  signals["adrs"] = sm.code_notes_summary.adr_count
2213
2237
 
2214
- has_tests = any(
2215
- "/test" in p or "/tests" in p or "/spec" in p or p.startswith("test")
2216
- for p in sm.file_paths
2217
- )
2218
- if has_tests:
2238
+ # One authority for "does this repository have tests" (ADR-0008 R1). The
2239
+ # substring rule this replaces answered `true` for a repository whose
2240
+ # production code lives in a package called `test`, contradicting the
2241
+ # `analysis_gaps` block of the same document.
2242
+ from sourcecode.test_sources import analyze_test_sources
2243
+
2244
+ _test_facts = analyze_test_sources(list(sm.file_paths or []))
2245
+ if _test_facts.has_tests:
2219
2246
  signals["has_tests"] = True
2220
2247
 
2221
2248
  # Semantic hotspots (populated when --semantics was passed)