sourcecode 2.5.13__py3-none-any.whl → 2.5.15__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
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.13"
7
+ __version__ = "2.5.15"
@@ -398,9 +398,17 @@ def ir_dict_to_canonical(
398
398
  # real REST surface. The CIR endpoint list feeds `spring-audit` (endpoints_analyzed),
399
399
  # `impact` and `validation`; it must apply the SAME filter so its count reconciles
400
400
  # with `endpoints` instead of being silently inflated by those pseudo-routes.
401
+ # Endpoints declared in a test-fixture / test-utility module are tagged
402
+ # scope="test_util" on route_surface by build_repo_ir. They are excluded from the
403
+ # canonical endpoint list for the same reason as the FQN filter above: the CIR
404
+ # surface feeds impact-chain, pr-impact, review-pr and spring-audit, and test
405
+ # scaffolding is not production HTTP exposure. route_surface itself keeps them
406
+ # (raw evidence); only this canonical projection drops them.
401
407
  _seen_ids: set[str] = set()
402
408
  raw_endpoints: list[CanonicalEndpoint] = []
403
409
  for r in route_surface:
410
+ if r.get("scope") == "test_util":
411
+ continue
404
412
  if _FQN_PATH_RE.search(r.get("path", "") or ""):
405
413
  continue
406
414
  ep = _route_to_canonical_endpoint(r)
sourcecode/classifier.py CHANGED
@@ -37,6 +37,26 @@ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoe
37
37
  _CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
38
38
  _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
39
39
 
40
+ # Source-file extensions per stack — single table shared by the confidence
41
+ # enrichment (_count_extension_hits) and the primary-stack evidence tiebreaker
42
+ # (select_primary_stack). Kept in one place so the two never diverge.
43
+ _STACK_EXTENSIONS: dict[str, tuple[str, ...]] = {
44
+ "python": (".py",),
45
+ "nodejs": (".js", ".ts", ".tsx"),
46
+ "go": (".go",),
47
+ "rust": (".rs",),
48
+ "java": (".java",),
49
+ "php": (".php",),
50
+ "ruby": (".rb",),
51
+ "dart": (".dart",),
52
+ "dotnet": (".cs", ".fs"),
53
+ "elixir": (".ex", ".exs"),
54
+ "kotlin": (".kt", ".kts"),
55
+ "scala": (".scala",),
56
+ "terraform": (".tf",),
57
+ "cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
58
+ }
59
+
40
60
  # Center-of-gravity gate for the "api" decision (neo4j field test).
41
61
  # The historical rule was pure PRESENCE: any API framework on the classpath →
42
62
  # project_type="api". That mislabels a large engine/platform that merely exposes a
@@ -63,6 +83,59 @@ _API_SURFACE_ENTRY_KINDS = frozenset({
63
83
  ConfidenceLevel = Literal["high", "medium", "low"]
64
84
 
65
85
 
86
+ def _primary_ext_hits(stack_name: str, file_paths: Sequence[str]) -> int:
87
+ exts = _STACK_EXTENSIONS.get(stack_name, ())
88
+ if not exts:
89
+ return 0
90
+ return sum(1 for p in file_paths if p.endswith(exts))
91
+
92
+
93
+ def select_primary_stack(
94
+ stacks: Sequence[StackDetection],
95
+ project_type: str | None,
96
+ file_paths: Sequence[str] | None = None,
97
+ ) -> list[StackDetection]:
98
+ """Single authority for which stack is primary.
99
+
100
+ Marks exactly ONE stack instance ``primary=True`` (all others ``False``) on the
101
+ FINAL assembled stack set (root + workspace stacks). Selection order:
102
+ ``(priority, confidence, manifest, source-file volume)``. The volume component is
103
+ a pure tiebreaker — it decides only when the prior three tie (e.g. a polyglot repo
104
+ where a Java and a .NET stack are both manifest-detected API stacks), so a 1-file
105
+ .NET example never outranks 86 Java files. Being last, it never reorders stacks
106
+ that already differ on the earlier components.
107
+
108
+ This is the ONLY place a primary decision is made. Every consumer must read
109
+ ``stack.primary`` — none may re-select or invent a ``stacks[0]`` fallback.
110
+ """
111
+ stack_list = list(stacks)
112
+ if not stack_list:
113
+ return stack_list
114
+ paths = list(file_paths or [])
115
+
116
+ def sort_key(stack: StackDetection) -> tuple[int, int, int, int]:
117
+ score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
118
+ manifest_weight = 1 if stack.manifests else 0
119
+ priority = 0
120
+ # Backend server-side stacks with manifest evidence outrank frontend stacks
121
+ # in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
122
+ if (project_type in {"fullstack", "api"}
123
+ and stack.stack in _API_STACKS
124
+ and stack.manifests):
125
+ priority = 4
126
+ elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
127
+ or project_type == "api" and stack.stack in _API_STACKS
128
+ or project_type == "cli" and any(
129
+ framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
130
+ )
131
+ or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
132
+ priority = 3
133
+ return (priority, score, manifest_weight, _primary_ext_hits(stack.stack, paths))
134
+
135
+ winner = max(stack_list, key=sort_key)
136
+ return [replace(s, primary=(s is winner)) for s in stack_list]
137
+
138
+
66
139
  class TypeClassifier:
67
140
  """Clasifica project_type y enriquece stacks con confianza/primary."""
68
141
 
@@ -74,15 +147,12 @@ class TypeClassifier:
74
147
  ) -> tuple[list[StackDetection], str | None]:
75
148
  enriched = [self._enrich_stack(file_tree, stack, entry_points) for stack in stacks]
76
149
  project_type = self._classify_project_type(file_tree, enriched, entry_points)
77
- primary_stack = self._select_primary_stack(enriched, project_type)
78
-
79
- final_stacks: list[StackDetection] = []
80
- primary_assigned = False
81
- for stack in enriched:
82
- is_primary = stack.stack == primary_stack and not primary_assigned
83
- if is_primary:
84
- primary_assigned = True
85
- final_stacks.append(replace(stack, primary=is_primary))
150
+ # Delegate to the single primary-stack authority. enrich only ever sees the
151
+ # ROOT stacks; the authority runs again at model finalization over the full
152
+ # root+workspace set (that call is the one consumers read).
153
+ final_stacks = select_primary_stack(
154
+ enriched, project_type, flatten_file_tree(file_tree)
155
+ )
86
156
  return final_stacks, project_type
87
157
 
88
158
  def _enrich_stack(
@@ -371,50 +441,8 @@ class TypeClassifier:
371
441
  has_api = True
372
442
  return has_web and has_api
373
443
 
374
- def _select_primary_stack(
375
- self, stacks: Sequence[StackDetection], project_type: str | None
376
- ) -> str | None:
377
- if not stacks:
378
- return None
379
-
380
- def sort_key(stack: StackDetection) -> tuple[int, int, int]:
381
- score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
382
- manifest_weight = 1 if stack.manifests else 0
383
- priority = 0
384
- # Backend server-side stacks with manifest evidence outrank frontend stacks
385
- # in fullstack projects (e.g. Java+Spring wins over nodejs+Angular).
386
- if (project_type in {"fullstack", "api"}
387
- and stack.stack in _API_STACKS
388
- and stack.manifests):
389
- priority = 4
390
- elif (project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"}
391
- or project_type == "api" and stack.stack in _API_STACKS
392
- or project_type == "cli" and any(
393
- framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
394
- )
395
- or project_type == "cli" and stack.stack in {"cpp", "dotnet"}):
396
- priority = 3
397
- return (priority, score, manifest_weight)
398
-
399
- return max(stacks, key=sort_key).stack
400
-
401
444
  def _count_extension_hits(self, file_tree: dict[str, Any], stack: str) -> int:
402
- extensions = {
403
- "python": (".py",),
404
- "nodejs": (".js", ".ts", ".tsx"),
405
- "go": (".go",),
406
- "rust": (".rs",),
407
- "java": (".java",),
408
- "php": (".php",),
409
- "ruby": (".rb",),
410
- "dart": (".dart",),
411
- "dotnet": (".cs", ".fs"),
412
- "elixir": (".ex", ".exs"),
413
- "kotlin": (".kt", ".kts"),
414
- "scala": (".scala",),
415
- "terraform": (".tf",),
416
- "cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
417
- }.get(stack, ())
445
+ extensions = _STACK_EXTENSIONS.get(stack, ())
418
446
  return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
419
447
 
420
448
  def _score_to_confidence(self, score: int) -> ConfidenceLevel:
sourcecode/cli.py CHANGED
@@ -1965,6 +1965,13 @@ def main(
1965
1965
  analyzer_fingerprints=_fingerprints,
1966
1966
  traversal_topology=_topology.as_dict(),
1967
1967
  )
1968
+ # Single primary-stack authority over the FINAL assembled set (root + workspace
1969
+ # stacks). enrich marked the root stacks; workspace stacks were appended after,
1970
+ # so re-run the one authority here — idempotent — so every consumer of sm.stacks
1971
+ # reads a coherent primary flag.
1972
+ from sourcecode.classifier import select_primary_stack as _select_primary_stack
1973
+ from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
1974
+ stacks = _select_primary_stack(stacks, project_type, _flatten_ft(file_tree))
1968
1975
  sm = SourceMap(
1969
1976
  metadata=metadata,
1970
1977
  file_tree=file_tree,
@@ -67,6 +67,16 @@ _BOOTSTRAP_ANNOTATION_RE = re.compile(
67
67
  r'EnableAutoConfiguration|SpringBootConfiguration)\b'
68
68
  r'|\bextends\s+SpringBootServletInitializer\b'
69
69
  )
70
+ # Comment strippers — a main()/bootstrap annotation inside a javadoc example or a
71
+ # comment is NOT an entry point (e.g. Broadleaf's *AutoConfiguration classes whose
72
+ # javadoc shows `public static void main` and mentions @SpringBootApplication).
73
+ # Detection of bootstrap entries must run on de-commented source.
74
+ _BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
75
+ _LINE_COMMENT_RE = re.compile(r'//[^\n]*')
76
+
77
+
78
+ def _strip_java_comments(source: str) -> str:
79
+ return _LINE_COMMENT_RE.sub(' ', _BLOCK_COMMENT_RE.sub(' ', source))
70
80
 
71
81
  # --- Keycloak / Quarkus SPI ---
72
82
  # Matches "implements EventListenerProvider" or "implements Foo, EventListenerProvider, Bar"
@@ -533,12 +543,21 @@ class JavaDetector(AbstractDetector):
533
543
  _other_files = [p for p in _non_test if "Controller" not in p]
534
544
  scan_candidates = _ctrl_files + _other_files[:max(0, _MAX_JAVA_ENTRY_SCAN - len(_ctrl_files))]
535
545
 
546
+ # Paths already emitted as application entries by the name-gated step-1 scan,
547
+ # so the annotation scan (which now also recovers main()-bearing files) does
548
+ # not list the same bootstrap twice.
549
+ _app_paths = {ep.path for ep in entry_points if ep.kind == "application"}
536
550
  annotation_eps: list[EntryPoint] = []
537
551
  for rel_path in scan_candidates:
538
552
  if len(annotation_eps) >= _MAX_ANNOTATION_ENTRY_POINTS:
539
553
  break
540
554
  abs_path = context.root / rel_path
541
- annotation_eps.extend(self._scan_java_file_for_entry_points(abs_path, rel_path))
555
+ for _ep in self._scan_java_file_for_entry_points(abs_path, rel_path):
556
+ if _ep.kind == "application":
557
+ if _ep.path in _app_paths:
558
+ continue
559
+ _app_paths.add(_ep.path)
560
+ annotation_eps.append(_ep)
542
561
  entry_points.extend(annotation_eps)
543
562
 
544
563
  # 3. web.xml servlet/filter declarations
@@ -611,6 +630,7 @@ class JavaDetector(AbstractDetector):
611
630
  content = abs_path.read_text(encoding="utf-8", errors="replace")
612
631
  except OSError:
613
632
  return None
633
+ content = _strip_java_comments(content)
614
634
  if _BOOTSTRAP_ANNOTATION_RE.search(content):
615
635
  return "bootstrap_annotation"
616
636
  if _MAIN_METHOD_RE.search(content):
@@ -678,6 +698,20 @@ class JavaDetector(AbstractDetector):
678
698
  and "ControllerAdvice" not in content
679
699
  and not any(ann in content for ann in _CUSTOM_SECURITY_ANNOTATIONS)
680
700
  and not has_jax_rs and not has_cdi and not has_spi):
701
+ # Not a controller/security/DI file — but it may still be an application
702
+ # entry point. The name-gated step-1 scan only reads *Application.java /
703
+ # *Main.java, so a real main(String[]) in a differently-named class (e.g.
704
+ # neo4j's Neo4jCommunity.java server bootstrap) would otherwise be invisible,
705
+ # leaving a client CLI named Main.java as the sole reported bootstrap. Reuse
706
+ # the content already read here; strip comments first so a main()/annotation
707
+ # in a javadoc example is never mistaken for a real entry point.
708
+ _decommented = _strip_java_comments(content)
709
+ if _BOOTSTRAP_ANNOTATION_RE.search(_decommented):
710
+ return [EntryPoint(path=rel_path, stack="java", kind="application",
711
+ source="bootstrap_annotation")]
712
+ if _MAIN_METHOD_RE.search(_decommented):
713
+ return [EntryPoint(path=rel_path, stack="java", kind="application",
714
+ source="main_method")]
681
715
  return []
682
716
 
683
717
  if _REST_CONTROLLER_RE.search(content):
@@ -30,7 +30,10 @@ ENDPOINT_SURFACE_RECONCILIATION: str = (
30
30
  "handler after inheritance projection and JAX-RS sub-resource-locator composition. "
31
31
  "It is NOT the canonical API surface — it still contains framework dynamic-admin "
32
32
  "routes whose path is a Java FQN (e.g. /org.broadleafcommerce...), which the "
33
- "`endpoints` command excludes. Endpoint counts differ across commands BY DESIGN "
33
+ "`endpoints` command excludes, and endpoints from test-fixture / test-utility "
34
+ "modules (tagged scope=\"test_util\"), which the canonical CIR surface excludes so "
35
+ "impact-chain / pr-impact / spring-audit do not report test scaffolding as "
36
+ "production HTTP exposure. Endpoint counts differ across commands BY DESIGN "
34
37
  "and are not contradictory; expected ordering for one repo is "
35
38
  "repo-ir.route_surface (raw, includes FQN-shaped framework routes) >= "
36
39
  "endpoints.total (canonical REST/API surface, FQN-shaped routes filtered out) >= "
@@ -168,6 +168,37 @@ def is_test_fixture_module_path(path: str) -> bool:
168
168
  return False
169
169
 
170
170
 
171
+ _EXAMPLE_MODULE_TOKENS = frozenset({
172
+ "example", "examples", "demo", "demos", "sample", "samples", "showcase",
173
+ })
174
+
175
+
176
+ def is_example_module_path(path: str) -> bool:
177
+ """Return True when *path* is inside an example/demo MODULE directory.
178
+
179
+ Structural: a directory segment above the source root carries a hyphen/underscore
180
+ -delimited example/demo token (e.g. ``eureka-examples/src/main/java/...``). Package
181
+ components below the source root (``io/confluent/examples/...``) are NOT matched —
182
+ only a module directory is a functional role; a package of the same name is not.
183
+ """
184
+ norm = path.replace("\\", "/").lower()
185
+ # Prepend "/" so a relative path ("src/main/...") matches the same boundary as an
186
+ # absolute one; without this the head-truncation silently failed and package
187
+ # components below the source root (e.g. ".../samples/petclinic/...") false-matched.
188
+ probe = "/" + norm
189
+ head = norm
190
+ for root in ("/src/main/", "/src/test/", "/src/"):
191
+ idx = probe.find(root)
192
+ if idx >= 0:
193
+ head = probe[:idx]
194
+ break
195
+ for seg in head.split("/"):
196
+ toks = seg.replace("_", "-").split("-")
197
+ if any(t in _EXAMPLE_MODULE_TOKENS for t in toks):
198
+ return True
199
+ return False
200
+
201
+
171
202
  def is_vendor_path(path: str) -> bool:
172
203
  """Return True when *path* is inside a vendored / third-party directory.
173
204
 
@@ -25,7 +25,10 @@ from typing import Any, Iterable, Optional
25
25
  from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
26
26
  from sourcecode.endpoint_metrics import ENDPOINT_SURFACE_RECONCILIATION
27
27
  from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
28
- from sourcecode.path_filters import is_test_path as _is_test_path
28
+ from sourcecode.path_filters import (
29
+ is_test_path as _is_test_path,
30
+ is_test_fixture_module_path as _is_test_fixture_module_path,
31
+ )
29
32
  from sourcecode.security_config import (
30
33
  CustomSecuritySpec,
31
34
  capture_markers as _capture_markers,
@@ -4176,6 +4179,23 @@ def _assemble(
4176
4179
  _route_surface = _build_route_surface(
4177
4180
  sorted_syms, route_diffs, extends_map=_extends_map, custom_security=custom_security
4178
4181
  )
4182
+ # Tag routes that originate in a test-fixture / test-utility module (dummy web
4183
+ # services, JAX-RS @Path fixtures shipped under src/main of a *-test-utils /
4184
+ # testsuite / test-framework module). route_surface stays RAW evidence — nothing
4185
+ # is dropped here; the tag lets the CIR endpoint projection (ir_dict_to_canonical)
4186
+ # exclude them so every CIR consumer (impact-chain, pr-impact, review-pr,
4187
+ # spring-audit) shares one production-endpoint definition with the `endpoints`
4188
+ # command, while repo-ir can still see the full surface. Mirrors the scope tag
4189
+ # applied in extract_java_endpoints.
4190
+ _rs_fqn_file = {
4191
+ s.symbol: (s.source_file or s.declaring_file)
4192
+ for s in sorted_syms
4193
+ if s.type in ("class", "interface")
4194
+ }
4195
+ for _r in _route_surface:
4196
+ _rf = _rs_fqn_file.get(_r.get("effective_class", ""), "")
4197
+ if _rf and _is_test_fixture_module_path(_rf):
4198
+ _r["scope"] = "test_util"
4179
4199
  _analysis_gaps = _compute_analysis_gaps(
4180
4200
  sorted_syms, spring_summary, _route_surface, sorted_rels,
4181
4201
  unparsed_type_files=unparsed_type_files,
@@ -6397,6 +6417,12 @@ def compute_blast_radius(
6397
6417
 
6398
6418
  endpoints_affected: list[dict] = []
6399
6419
  for ep in route_surface:
6420
+ # Test-fixture routes (tagged by build_repo_ir) are not production HTTP
6421
+ # exposure — skip them so the legacy `impact` command shares the single
6422
+ # production-endpoint definition used by the CIR surfaces. Nothing else about
6423
+ # blast-radius selection changes; FQN-admin routes are untouched.
6424
+ if ep.get("scope") == "test_util":
6425
+ continue
6400
6426
  ep_class = ep.get("effective_class") or ep.get("controller") or ep.get("class") or ""
6401
6427
  ep_symbol = ep.get("symbol") or ""
6402
6428
  ep_handler = (
sourcecode/serializer.py CHANGED
@@ -700,7 +700,11 @@ def _security_surface_from_eps(
700
700
  def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
701
701
  """Separate Java entry points into bootstrap / security / controllers groups."""
702
702
  from pathlib import Path as _Path
703
- bootstrap: list[str] = []
703
+ from sourcecode.path_filters import (
704
+ is_example_module_path,
705
+ is_test_fixture_module_path,
706
+ )
707
+ bootstrap_entries: list[tuple[str, str]] = [] # (path, source)
704
708
  security: list[str] = []
705
709
  controllers: list[dict] = []
706
710
  seen_b: set[str] = set()
@@ -720,9 +724,19 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
720
724
  # filename alone — XSD-generated Application.java model classes must not
721
725
  # appear here.
722
726
  if kind == "application":
727
+ # Benchmark/example/tool mains (classified auxiliary/development) are real
728
+ # entry points but not the app's deployment bootstrap — keep them out.
729
+ if getattr(ep, "classification", None) in ("auxiliary", "development"):
730
+ continue
731
+ # Same for mains that live in an example/demo or test-fixture MODULE. This
732
+ # filter runs here (not in the detector) because workspace sub-scans prefix
733
+ # the module name onto the path only after detection — the full path with
734
+ # the module dir is visible only at this serialization stage.
735
+ if is_example_module_path(path) or is_test_fixture_module_path(path):
736
+ continue
723
737
  if path not in seen_b:
724
738
  seen_b.add(path)
725
- bootstrap.append(path)
739
+ bootstrap_entries.append((path, getattr(ep, "source", "")))
726
740
  elif kind == "filter" or any(k in stem for k in ("Filter", "Security", "Auth", "Jwt", "WebSecurity")):
727
741
  if path not in seen_s:
728
742
  seen_s.add(path)
@@ -736,12 +750,31 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
736
750
  item["http_path"] = http_path
737
751
  controllers.append(item)
738
752
 
753
+ # Order deployment-annotation entries (@SpringBootApplication/@QuarkusMain/…) ahead
754
+ # of plain main() classes, so a repo's real bootstrap leads even when many main()
755
+ # methods exist; then cap for readability while reporting the true total. A
756
+ # multi-main repo (e.g. neo4j: cypher-shell CLI + Neo4jCommunity server + admin
757
+ # tools) surfaces its server entry and its multiplicity instead of one arbitrary pick.
758
+ bootstrap_entries.sort(key=lambda pe: 0 if pe[1] == "bootstrap_annotation" else 1)
759
+ _bootstrap_total = len(bootstrap_entries)
760
+ _BOOTSTRAP_CAP = 10
761
+ bootstrap = [p for p, _src in bootstrap_entries[:_BOOTSTRAP_CAP]]
762
+
739
763
  if not bootstrap and not security:
740
764
  return None
741
765
 
742
766
  result: dict[str, Any] = {}
743
767
  if bootstrap:
744
768
  result["bootstrap"] = bootstrap
769
+ if _bootstrap_total > len(bootstrap):
770
+ result["bootstrap_note"] = (
771
+ f"{_bootstrap_total} application entry points detected; showing "
772
+ f"{len(bootstrap)} (deployment-annotated entries first)."
773
+ )
774
+ elif _bootstrap_total > 1:
775
+ result["bootstrap_note"] = (
776
+ f"{_bootstrap_total} application entry points detected."
777
+ )
745
778
  if security:
746
779
  result["security"] = security
747
780
  if controllers:
sourcecode/summarizer.py CHANGED
@@ -362,11 +362,16 @@ class ProjectSummarizer:
362
362
 
363
363
  parts = [lead]
364
364
 
365
- # Stack with frameworks — keep brief, skip internal module listings
365
+ # Stack with frameworks — keep brief, skip internal module listings.
366
+ # Read the primary flag set by the single authority (classifier.select_primary_stack);
367
+ # never re-select here — a second selector would let the summary name a different
368
+ # stack than architecture_summary / stacks[].primary (the polyglot divergence).
366
369
  non_tooling_stacks = self._filter_non_tooling_stacks(sm)
367
370
  primary = None
368
371
  if non_tooling_stacks:
369
- primary = self._select_summary_primary_stack(non_tooling_stacks)
372
+ primary = next(
373
+ (s for s in non_tooling_stacks if s.primary), non_tooling_stacks[0]
374
+ )
370
375
  frameworks = [fw.name for fw in primary.frameworks[:2]]
371
376
  if frameworks:
372
377
  parts.append(f"Stack: {primary.stack.capitalize()} ({', '.join(frameworks)})")
@@ -580,21 +585,6 @@ class ProjectSummarizer:
580
585
  ]
581
586
  return filtered or sm.stacks
582
587
 
583
- def _select_summary_primary_stack(self, stacks: list) -> Any:
584
- def score(stack: Any) -> tuple[int, int, int, int]:
585
- root_manifest_hits = 0
586
- if self.root is not None:
587
- root_manifest_hits = sum(
588
- 1 for manifest in stack.manifests
589
- if (self.root / manifest).is_file()
590
- )
591
- framework_hits = len(stack.frameworks)
592
- primary_hint = 1 if stack.primary else 0
593
- confidence = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
594
- return (root_manifest_hits, framework_hits, primary_hint, confidence)
595
-
596
- return max(stacks, key=score)
597
-
598
588
  def _is_tooling_path(self, path: str | None) -> bool:
599
589
  if not path:
600
590
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.13
3
+ Version: 2.5.15
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
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=M8eYjrmWCXZgccRJ042BhY9QS6VmNX4SDwD_gMX9cvo,309
1
+ sourcecode/__init__.py,sha256=TA67wvVQzG1GVLowfrLngK0bKQZu-eJyvWrAm5PiAdg,309
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
4
4
  sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
@@ -7,10 +7,10 @@ sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,5
7
7
  sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
8
8
  sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
9
9
  sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,2574
10
- sourcecode/canonical_ir.py,sha256=5hLAhhSpXNOdZAvPt-J-rXccJE6M3A6gVhmhU0Tym6Y,29269
10
+ sourcecode/canonical_ir.py,sha256=LdP_Ri3rl0M5p-MY0ypVvQwhq1WuUF1FmaHid7zyzEg,29807
11
11
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
12
- sourcecode/classifier.py,sha256=rkMapdklDuIv3rXrNEwwL65UldkvTn8VuKa73GPoSDo,18849
13
- sourcecode/cli.py,sha256=mlMgyRukRoHXYdAYHBN4z06LszwdRo0QLdhs0KYtup8,323804
12
+ sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
13
+ sourcecode/cli.py,sha256=LKK4czUeENVlcxHpSd5xao2HnAgtbRSYo8OoSwcZeZg,324334
14
14
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
15
15
  sourcecode/confidence_analyzer.py,sha256=vnbPI-20FnHdjO6STxHW8fbaxmB4A7y58io63ibFZjc,21586
16
16
  sourcecode/context_cache.py,sha256=maws79rLKDwyZ7c9Q2LwlIxqZHRFlaKzErtO3mglWoA,25036
@@ -24,7 +24,7 @@ sourcecode/dependency_analyzer.py,sha256=fQCaWQ7_BNcVgZv8gweJCwJ1CGlXsz8nw_tqN3a
24
24
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
25
25
  sourcecode/dynamic_argument_surface.py,sha256=iRzrJhs57UXrGvLhNPiTA-e_sdO6ka-wNqc0wIiegiM,2554
26
26
  sourcecode/endpoint_literals.py,sha256=Qf4gTZzvNSFDGDuOvF0YRL9NvaYKKj24klhR5LIOaXc,3263
27
- sourcecode/endpoint_metrics.py,sha256=wkAonXe34ItHlvlGur3YRFUrcZfg6GdiiwiE4bMa21Y,2590
27
+ sourcecode/endpoint_metrics.py,sha256=sLSLUIgiIyvNdOynaxVgHd9SjRF_DbN3QxevaquykaU,2840
28
28
  sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
29
29
  sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
30
30
  sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
@@ -45,7 +45,7 @@ sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7
45
45
  sourcecode/migrate_check.py,sha256=HJhvHuvXDGTJSRQU9_AMx0L94h2TImC1DCGs3m3RTu0,108199
46
46
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
47
47
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
48
- sourcecode/path_filters.py,sha256=809P9hj_QChNLg8wk4HuKcFUZmnDKNz0sKIMbmt_FNE,9138
48
+ sourcecode/path_filters.py,sha256=qPKO7kRmVp2y9zjLNSuAVCbcpZrIInHE4QulLUlzPFI,10412
49
49
  sourcecode/pr_comment_renderer.py,sha256=KmcjMruhR44gjzMDJwjBSkWP9QEvh8xWBLyxzxoRbj0,14542
50
50
  sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
51
51
  sourcecode/prepare_context.py,sha256=-Xu33qCETuqw-OFfT3ukvAtxde7krCjQpn7wR3pbdOg,224290
@@ -56,7 +56,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
56
56
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
57
57
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
58
58
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
59
- sourcecode/repository_ir.py,sha256=bNSUKkWQEvFQ2wSZPRdn_9cZfx6TRcPzGtM2EgMm49M,311400
59
+ sourcecode/repository_ir.py,sha256=3hpxzOrudflSteYDLMQHK7KWdXg6mhhVwG94JSn7FHY,312825
60
60
  sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
61
61
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
62
62
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -67,7 +67,7 @@ sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4Bb
67
67
  sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
68
68
  sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
69
69
  sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
70
- sourcecode/serializer.py,sha256=FmQo7eojA_z3ptLW-Vgze5mrEJ6FabcZqiwbQWWTgFI,130056
70
+ sourcecode/serializer.py,sha256=UCGwxPZw9jS_amhU4om9a4Z6VOGOpttNuPpLuhGnuuU,132052
71
71
  sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
72
72
  sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
73
73
  sourcecode/spring_impact.py,sha256=MkUI27OCOhkjvBoA4f9X1eDBIY0SmyPuLVCWIfZgOcE,72430
@@ -75,7 +75,7 @@ sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16
75
75
  sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
76
76
  sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
77
77
  sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
78
- sourcecode/summarizer.py,sha256=SWj-y5BJ_Qw89ljLg-KXaLsvU87usOw-raTckPutCk4,26976
78
+ sourcecode/summarizer.py,sha256=sr0-tfecFKCr-fSkPPWbl-t9HC7SY2ZxkjnnXX8DB2A,26621
79
79
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
80
80
  sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
81
81
  sourcecode/validation_inference.py,sha256=UtmYv-d4TEzT1ihGEipAhtXDrwq0XtVtjMpb3i7NsNc,10815
@@ -91,7 +91,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
91
91
  sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
92
92
  sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
93
93
  sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
94
- sourcecode/detectors/java.py,sha256=ut8czxGzhFUxRAS5LeG78wev8kJolC7LG8IESDJZTNk,42349
94
+ sourcecode/detectors/java.py,sha256=t6uDee_4pTmkbCQ5aXXqtVfoPLnvVBhnxYYVrv5qrdk,44499
95
95
  sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
96
96
  sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
97
97
  sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
@@ -137,8 +137,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
137
137
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
138
138
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
139
139
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
140
- sourcecode-2.5.13.dist-info/METADATA,sha256=hAqH0Dxo8V6o4vDdpqalzX3njpQNvHIO8-t5zglFo68,10852
141
- sourcecode-2.5.13.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
142
- sourcecode-2.5.13.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
143
- sourcecode-2.5.13.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
144
- sourcecode-2.5.13.dist-info/RECORD,,
140
+ sourcecode-2.5.15.dist-info/METADATA,sha256=lClKdlUiQRzl134vAQzx_Nvdk0pfm5mGK0pfIT0FIxs,10852
141
+ sourcecode-2.5.15.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
142
+ sourcecode-2.5.15.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
143
+ sourcecode-2.5.15.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
144
+ sourcecode-2.5.15.dist-info/RECORD,,