sourcecode 2.5.13__py3-none-any.whl → 2.5.16__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 +1 -1
- sourcecode/ast_extractor.py +12 -11
- sourcecode/canonical_ir.py +8 -0
- sourcecode/classifier.py +80 -52
- sourcecode/cli.py +18 -0
- sourcecode/context_graph.py +35 -0
- sourcecode/detectors/java.py +35 -1
- sourcecode/endpoint_metrics.py +4 -1
- sourcecode/path_filters.py +31 -0
- sourcecode/perf.py +268 -0
- sourcecode/repository_ir.py +39 -1
- sourcecode/serializer.py +35 -2
- sourcecode/summarizer.py +7 -17
- {sourcecode-2.5.13.dist-info → sourcecode-2.5.16.dist-info}/METADATA +1 -1
- {sourcecode-2.5.13.dist-info → sourcecode-2.5.16.dist-info}/RECORD +18 -17
- {sourcecode-2.5.13.dist-info → sourcecode-2.5.16.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.13.dist-info → sourcecode-2.5.16.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.13.dist-info → sourcecode-2.5.16.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/ast_extractor.py
CHANGED
|
@@ -967,25 +967,26 @@ def _java_contract_from_graph(rel_path: str, graph: "Any") -> FileContract:
|
|
|
967
967
|
nodes/edges instead of re-parsing source. Method signatures are the graph's
|
|
968
968
|
canonical compact form (`(Type)->Return`); annotations are node-grounded.
|
|
969
969
|
"""
|
|
970
|
-
file_syms =
|
|
970
|
+
file_syms = graph.symbols_in_file(rel_path)
|
|
971
971
|
type_syms = [s for s in file_syms if s.kind in ("class", "interface", "enum", "annotation")]
|
|
972
972
|
type_fqns = {s.fqn for s in type_syms}
|
|
973
973
|
|
|
974
974
|
# extends / implements edges out of this file's types (targets are simple
|
|
975
975
|
# names for out-of-repo supertypes, resolved FQNs for in-repo ones — the
|
|
976
|
-
# same forms the old regex captured for the unresolved case).
|
|
976
|
+
# same forms the old regex captured for the unresolved case). Only edges
|
|
977
|
+
# sourced at this file's types matter, so index straight to them instead of
|
|
978
|
+
# scanning the whole-repo relation set once per file.
|
|
977
979
|
extends_of: dict[str, list[str]] = {}
|
|
978
980
|
implements_of: dict[str, list[str]] = {}
|
|
979
981
|
import_targets: set[str] = set()
|
|
980
|
-
for
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
import_targets.add(r.target)
|
|
982
|
+
for fqn in type_fqns:
|
|
983
|
+
for r in graph.relations_from(fqn):
|
|
984
|
+
if r.kind == "extends":
|
|
985
|
+
extends_of.setdefault(r.source, []).append(r.target)
|
|
986
|
+
elif r.kind == "implements":
|
|
987
|
+
implements_of.setdefault(r.source, []).append(r.target)
|
|
988
|
+
elif r.kind == "imports":
|
|
989
|
+
import_targets.add(r.target)
|
|
989
990
|
|
|
990
991
|
exports: list[ExportRecord] = []
|
|
991
992
|
types: list[TypeDefinition] = []
|
sourcecode/canonical_ir.py
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
@@ -15,6 +15,7 @@ from sourcecode import __version__
|
|
|
15
15
|
from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
16
16
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
17
17
|
from sourcecode.progress import Progress
|
|
18
|
+
from sourcecode import perf
|
|
18
19
|
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION
|
|
19
20
|
from sourcecode.repository_ir import extract_java_endpoints as _extract_java_endpoints
|
|
20
21
|
|
|
@@ -1703,6 +1704,7 @@ def main(
|
|
|
1703
1704
|
_progress = Progress()
|
|
1704
1705
|
_progress.start("scanning files")
|
|
1705
1706
|
|
|
1707
|
+
_perf_discovery = perf.start()
|
|
1706
1708
|
scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth,
|
|
1707
1709
|
extra_excludes=_extra_excludes)
|
|
1708
1710
|
raw_tree = scanner.scan_tree()
|
|
@@ -1710,6 +1712,8 @@ def main(
|
|
|
1710
1712
|
_progress.update("parsing manifests")
|
|
1711
1713
|
# 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
|
|
1712
1714
|
file_tree = filter_sensitive_files(raw_tree, redactor)
|
|
1715
|
+
perf.stop("discovery", _perf_discovery)
|
|
1716
|
+
_perf_detection = perf.start()
|
|
1713
1717
|
detector = ProjectDetector(build_default_detectors())
|
|
1714
1718
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
1715
1719
|
|
|
@@ -1951,6 +1955,8 @@ def main(
|
|
|
1951
1955
|
else None
|
|
1952
1956
|
)
|
|
1953
1957
|
|
|
1958
|
+
perf.stop("detection", _perf_detection)
|
|
1959
|
+
_perf_analysis = perf.start()
|
|
1954
1960
|
# 3. Build schema
|
|
1955
1961
|
# Compute analyzer fingerprints: short hashes of each analyzer's key rule
|
|
1956
1962
|
# constants so that a rule change is always visible in the output, regardless
|
|
@@ -1965,6 +1971,13 @@ def main(
|
|
|
1965
1971
|
analyzer_fingerprints=_fingerprints,
|
|
1966
1972
|
traversal_topology=_topology.as_dict(),
|
|
1967
1973
|
)
|
|
1974
|
+
# Single primary-stack authority over the FINAL assembled set (root + workspace
|
|
1975
|
+
# stacks). enrich marked the root stacks; workspace stacks were appended after,
|
|
1976
|
+
# so re-run the one authority here — idempotent — so every consumer of sm.stacks
|
|
1977
|
+
# reads a coherent primary flag.
|
|
1978
|
+
from sourcecode.classifier import select_primary_stack as _select_primary_stack
|
|
1979
|
+
from sourcecode.tree_utils import flatten_file_tree as _flatten_ft
|
|
1980
|
+
stacks = _select_primary_stack(stacks, project_type, _flatten_ft(file_tree))
|
|
1968
1981
|
sm = SourceMap(
|
|
1969
1982
|
metadata=metadata,
|
|
1970
1983
|
file_tree=file_tree,
|
|
@@ -2404,8 +2417,10 @@ def main(
|
|
|
2404
2417
|
err=True,
|
|
2405
2418
|
)
|
|
2406
2419
|
|
|
2420
|
+
perf.stop("analysis", _perf_analysis)
|
|
2407
2421
|
# 4. Serialize
|
|
2408
2422
|
_progress.update("serializing output")
|
|
2423
|
+
_perf_serialize = perf.start()
|
|
2409
2424
|
if _is_contract_mode and not agent:
|
|
2410
2425
|
from sourcecode.serializer import contract_view as _contract_view
|
|
2411
2426
|
_depth = _CONTRACT_DEPTH.get(mode, "minimal")
|
|
@@ -2448,6 +2463,9 @@ def main(
|
|
|
2448
2463
|
raw_dict = redact_dict(raw_dict)
|
|
2449
2464
|
content = _serialize_dict(raw_dict, format)
|
|
2450
2465
|
|
|
2466
|
+
perf.stop("serialize", _perf_serialize)
|
|
2467
|
+
perf.flush_recorder()
|
|
2468
|
+
|
|
2451
2469
|
# 5. Telemetry (fire-and-forget, never blocks)
|
|
2452
2470
|
try:
|
|
2453
2471
|
from sourcecode import telemetry as _tel
|
sourcecode/context_graph.py
CHANGED
|
@@ -408,6 +408,7 @@ class ContextGraph:
|
|
|
408
408
|
"_cir", "_nodes_by_fqn", "_nodes", "_build_ms",
|
|
409
409
|
"_body_index", "_literal_index", "_guard_index", "_span_index",
|
|
410
410
|
"_class_type_index", "_relations_all", "_symbols_sorted",
|
|
411
|
+
"_symbols_by_file", "_relations_by_source",
|
|
411
412
|
)
|
|
412
413
|
|
|
413
414
|
def __init__(self, cir: CanonicalRepositoryIR, *, build_ms: float = 0.0) -> None:
|
|
@@ -439,6 +440,12 @@ class ContextGraph:
|
|
|
439
440
|
# filtered cheaply thereafter.
|
|
440
441
|
self._relations_all: Optional[tuple[Relation, ...]] = None
|
|
441
442
|
self._symbols_sorted: Optional[tuple[Symbol, ...]] = None
|
|
443
|
+
# Per-key groupings for per-file consumers (`_java_contract_from_graph`
|
|
444
|
+
# runs once per file). Without these, each file filtered the whole-repo
|
|
445
|
+
# symbol/relation set — the residual O(files × total) scan that the
|
|
446
|
+
# memoized projections above did not remove. Built once, O(1) lookup.
|
|
447
|
+
self._symbols_by_file: Optional[dict[str, list[Symbol]]] = None
|
|
448
|
+
self._relations_by_source: Optional[dict[str, list[Relation]]] = None
|
|
442
449
|
|
|
443
450
|
# -- construction -------------------------------------------------------
|
|
444
451
|
|
|
@@ -518,6 +525,34 @@ class ContextGraph:
|
|
|
518
525
|
and (name_contains is None or name_contains in s.fqn)
|
|
519
526
|
]
|
|
520
527
|
|
|
528
|
+
def symbols_in_file(self, source_file: str) -> list[Symbol]:
|
|
529
|
+
"""All symbols declared in one file, FQN-sorted — the same order and set
|
|
530
|
+
a no-filter ``symbols()`` scan filtered to this file would produce, but
|
|
531
|
+
O(1) after a one-time O(total) grouping instead of O(total) per call.
|
|
532
|
+
"""
|
|
533
|
+
if self._symbols_by_file is None:
|
|
534
|
+
if self._symbols_sorted is None:
|
|
535
|
+
self._symbols_sorted = tuple(sorted(self._nodes, key=lambda s: s.fqn))
|
|
536
|
+
idx: dict[str, list[Symbol]] = {}
|
|
537
|
+
for s in self._symbols_sorted:
|
|
538
|
+
idx.setdefault(s.source_file, []).append(s)
|
|
539
|
+
self._symbols_by_file = idx
|
|
540
|
+
return self._symbols_by_file.get(source_file, [])
|
|
541
|
+
|
|
542
|
+
def relations_from(self, source_fqn: str) -> list[Relation]:
|
|
543
|
+
"""All relations whose source is ``source_fqn``, in the IR's stable edge
|
|
544
|
+
order. O(1) after a one-time O(edges) grouping — replaces per-file scans
|
|
545
|
+
of the whole relation set.
|
|
546
|
+
"""
|
|
547
|
+
if self._relations_by_source is None:
|
|
548
|
+
if self._relations_all is None:
|
|
549
|
+
self._relations_all = tuple(_edge_to_relation(e) for e in self._cir.call_graph)
|
|
550
|
+
idx: dict[str, list[Relation]] = {}
|
|
551
|
+
for r in self._relations_all:
|
|
552
|
+
idx.setdefault(r.source, []).append(r)
|
|
553
|
+
self._relations_by_source = idx
|
|
554
|
+
return self._relations_by_source.get(source_fqn, [])
|
|
555
|
+
|
|
521
556
|
def types(self) -> list[Symbol]:
|
|
522
557
|
"""All class/interface/enum/annotation symbols, sorted by FQN."""
|
|
523
558
|
return sorted(
|
sourcecode/detectors/java.py
CHANGED
|
@@ -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
|
-
|
|
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):
|
sourcecode/endpoint_metrics.py
CHANGED
|
@@ -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
|
|
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) >= "
|
sourcecode/path_filters.py
CHANGED
|
@@ -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
|
|
sourcecode/perf.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Performance instrumentation and baseline schema — see ADR-0006.
|
|
2
|
+
|
|
3
|
+
Two responsibilities, kept separate:
|
|
4
|
+
|
|
5
|
+
* An **in-process recorder** that times the frozen phase taxonomy (§3). It is
|
|
6
|
+
*zero-cost when disabled* (ADR-0006 §5.5): with ``SOURCECODE_PERF`` unset,
|
|
7
|
+
:func:`span` yields without reading a clock, allocating a span, or writing a
|
|
8
|
+
dict. A production ``ask`` invocation pays nothing for the harness existing.
|
|
9
|
+
|
|
10
|
+
* **Pure functions** — percentiles and the ``perf-baseline-v1`` cell builder —
|
|
11
|
+
that the fleet harness (``scripts/perf_harness.py``) uses to turn a set of raw
|
|
12
|
+
per-run measurements into one frozen-schema artifact.
|
|
13
|
+
|
|
14
|
+
The phase taxonomy and the output schema are the frozen contract; this module is
|
|
15
|
+
the replaceable collector.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import socket
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import Iterator, Mapping, Sequence
|
|
30
|
+
|
|
31
|
+
# ── Frozen contract constants (ADR-0006) ────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
SCHEMA_VERSION = "perf-baseline-v1"
|
|
34
|
+
|
|
35
|
+
#: The frozen phase taxonomy. Order is contractual — see ADR-0006 §3.
|
|
36
|
+
#: These are the pipeline's *measured* contiguous seams, not compiler-shaped
|
|
37
|
+
#: guesses: repo discovery, what-is-this-repo detection+enrichment, model
|
|
38
|
+
#: assembly + semantic/contract extraction, and output rendering.
|
|
39
|
+
PHASES: tuple[str, ...] = ("discovery", "detection", "analysis", "serialize")
|
|
40
|
+
|
|
41
|
+
#: Env var that arms the in-process recorder.
|
|
42
|
+
ENV_ENABLE = "SOURCECODE_PERF"
|
|
43
|
+
#: Env var naming the JSONL file the recorder appends its phase timings to.
|
|
44
|
+
ENV_LOG = "SOURCECODE_PERF_LOG"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ── In-process recorder (zero-cost when disabled) ───────────────────────────
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _Recorder:
|
|
51
|
+
"""Accumulates elapsed milliseconds per named phase for one process."""
|
|
52
|
+
|
|
53
|
+
__slots__ = ("_spans",)
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._spans: dict[str, float] = {}
|
|
57
|
+
|
|
58
|
+
def add(self, name: str, ms: float) -> None:
|
|
59
|
+
self._spans[name] = self._spans.get(name, 0.0) + ms
|
|
60
|
+
|
|
61
|
+
def as_dict(self) -> dict[str, float]:
|
|
62
|
+
return dict(self._spans)
|
|
63
|
+
|
|
64
|
+
def flush(self, path: str) -> None:
|
|
65
|
+
"""Append the accumulated phase timings as one JSONL record."""
|
|
66
|
+
with open(path, "a", encoding="utf-8") as fh:
|
|
67
|
+
fh.write(json.dumps({"phase_ms": self._spans}) + "\n")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_recorder() -> _Recorder | None:
|
|
71
|
+
"""Return a recorder iff the environment arms it, else ``None``.
|
|
72
|
+
|
|
73
|
+
Resolved once and cached. Kept out of :func:`span`'s hot path so the
|
|
74
|
+
disabled case is a single ``is None`` check.
|
|
75
|
+
"""
|
|
76
|
+
if _resolve_recorder._cached is not _UNSET: # type: ignore[attr-defined]
|
|
77
|
+
return _resolve_recorder._cached # type: ignore[attr-defined]
|
|
78
|
+
rec = _Recorder() if os.environ.get(ENV_ENABLE) == "1" else None
|
|
79
|
+
_resolve_recorder._cached = rec # type: ignore[attr-defined]
|
|
80
|
+
return rec
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_UNSET = object()
|
|
84
|
+
_resolve_recorder._cached = _UNSET # type: ignore[attr-defined]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@contextmanager
|
|
88
|
+
def span(name: str) -> Iterator[None]:
|
|
89
|
+
"""Time a phase region. No-op (and clock-free) when perf is disabled.
|
|
90
|
+
|
|
91
|
+
``name`` should be one of :data:`PHASES`, but any name is accepted so the
|
|
92
|
+
caller is never coupled to import order; unknown names simply won't map into
|
|
93
|
+
a baseline cell.
|
|
94
|
+
"""
|
|
95
|
+
rec = _resolve_recorder()
|
|
96
|
+
if rec is None:
|
|
97
|
+
# Zero-cost path: no perf_counter, no object, no dict write.
|
|
98
|
+
yield
|
|
99
|
+
return
|
|
100
|
+
t0 = time.perf_counter()
|
|
101
|
+
try:
|
|
102
|
+
yield
|
|
103
|
+
finally:
|
|
104
|
+
rec.add(name, (time.perf_counter() - t0) * 1000.0)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def start() -> float | None:
|
|
108
|
+
"""Open a phase measurement without indenting the region it spans.
|
|
109
|
+
|
|
110
|
+
Pairs with :func:`stop`. Returns a start timestamp, or ``None`` when perf is
|
|
111
|
+
disabled — in which case no clock is read. Use this instead of :func:`span`
|
|
112
|
+
to instrument a long, branchy region in place without wrapping it in a
|
|
113
|
+
``with`` block (which would force a re-indent of hundreds of lines).
|
|
114
|
+
"""
|
|
115
|
+
return time.perf_counter() if _resolve_recorder() is not None else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def stop(name: str, t0: float | None) -> None:
|
|
119
|
+
"""Close a :func:`start` measurement, accumulating into phase ``name``.
|
|
120
|
+
|
|
121
|
+
A no-op when perf is disabled (``t0 is None``). Because the recorder sums by
|
|
122
|
+
name, the same phase may be opened and closed at several scattered call
|
|
123
|
+
sites and the times add up.
|
|
124
|
+
"""
|
|
125
|
+
if t0 is None:
|
|
126
|
+
return
|
|
127
|
+
rec = _resolve_recorder()
|
|
128
|
+
if rec is not None:
|
|
129
|
+
rec.add(name, (time.perf_counter() - t0) * 1000.0)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def flush_recorder() -> None:
|
|
133
|
+
"""Write accumulated phase timings to ``$SOURCECODE_PERF_LOG`` if armed."""
|
|
134
|
+
rec = _resolve_recorder()
|
|
135
|
+
path = os.environ.get(ENV_LOG)
|
|
136
|
+
if rec is not None and path:
|
|
137
|
+
rec.flush(path)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _reset_for_tests() -> None:
|
|
141
|
+
"""Clear the cached recorder so tests can re-resolve after mutating env."""
|
|
142
|
+
_resolve_recorder._cached = _UNSET # type: ignore[attr-defined]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── Pure statistics ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def percentile(values: Sequence[float], q: float) -> float:
|
|
149
|
+
"""Linear-interpolated percentile, ``q`` in ``[0, 100]``.
|
|
150
|
+
|
|
151
|
+
``percentile(v, 50)`` is the median. Used for both p50 (central tendency —
|
|
152
|
+
never the mean, ADR-0006 §4) and p95 (tail).
|
|
153
|
+
"""
|
|
154
|
+
if not values:
|
|
155
|
+
raise ValueError("percentile of empty sequence")
|
|
156
|
+
if not 0.0 <= q <= 100.0:
|
|
157
|
+
raise ValueError(f"q out of range: {q}")
|
|
158
|
+
s = sorted(values)
|
|
159
|
+
if len(s) == 1:
|
|
160
|
+
return float(s[0])
|
|
161
|
+
rank = (q / 100.0) * (len(s) - 1)
|
|
162
|
+
lo = int(rank)
|
|
163
|
+
hi = min(lo + 1, len(s) - 1)
|
|
164
|
+
frac = rank - lo
|
|
165
|
+
return float(s[lo] + (s[hi] - s[lo]) * frac)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def p50(values: Sequence[float]) -> float:
|
|
169
|
+
return percentile(values, 50.0)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def p95(values: Sequence[float]) -> float:
|
|
173
|
+
return percentile(values, 95.0)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _stat_block(values: Sequence[float], *, tail: bool = True) -> dict[str, float]:
|
|
177
|
+
block = {"p50": round(p50(values), 3), "p95": round(p95(values), 3)}
|
|
178
|
+
if not tail:
|
|
179
|
+
block.pop("p95")
|
|
180
|
+
return block
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Environment fingerprint ─────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def collect_env() -> dict[str, object]:
|
|
187
|
+
"""Capture the fields that make a baseline comparable only to same-host runs.
|
|
188
|
+
|
|
189
|
+
``hostname_hash`` (not the raw hostname) groups same-machine runs without
|
|
190
|
+
leaking the machine name. Cross-host absolute numbers are recorded, never
|
|
191
|
+
diffed (ADR-0006 §5.4).
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
host = socket.gethostname()
|
|
195
|
+
except OSError:
|
|
196
|
+
host = "unknown"
|
|
197
|
+
return {
|
|
198
|
+
"python": platform.python_version(),
|
|
199
|
+
"os": sys.platform,
|
|
200
|
+
"cpu": platform.processor() or platform.machine() or "unknown",
|
|
201
|
+
"cores": os.cpu_count() or 0,
|
|
202
|
+
"hostname_hash": hashlib.sha256(host.encode("utf-8")).hexdigest()[:8],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ── perf-baseline-v1 cell builder ───────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def build_baseline_cell(
|
|
210
|
+
*,
|
|
211
|
+
tool_version: str,
|
|
212
|
+
command: str,
|
|
213
|
+
repo: Mapping[str, object],
|
|
214
|
+
mode: str,
|
|
215
|
+
wall_ms_runs: Sequence[float],
|
|
216
|
+
rss_bytes_runs: Sequence[float],
|
|
217
|
+
phase_ms_runs: Sequence[Mapping[str, float]] | None = None,
|
|
218
|
+
env: Mapping[str, object] | None = None,
|
|
219
|
+
captured_at: str | None = None,
|
|
220
|
+
) -> dict:
|
|
221
|
+
"""Assemble one frozen-schema ``perf-baseline-v1`` cell from raw runs.
|
|
222
|
+
|
|
223
|
+
``wall_ms_runs`` / ``rss_bytes_runs`` hold one value per run. ``phase_ms_runs``
|
|
224
|
+
is one ``{phase: ms}`` mapping per run; when absent, phase timings are emitted
|
|
225
|
+
as ``null`` with an honest ``instrumentation: "absent"`` marker rather than a
|
|
226
|
+
fabricated zero.
|
|
227
|
+
"""
|
|
228
|
+
if not wall_ms_runs:
|
|
229
|
+
raise ValueError("wall_ms_runs must be non-empty")
|
|
230
|
+
|
|
231
|
+
phase_ms: dict[str, object] = {}
|
|
232
|
+
if phase_ms_runs:
|
|
233
|
+
for ph in PHASES:
|
|
234
|
+
series = [float(r[ph]) for r in phase_ms_runs if ph in r]
|
|
235
|
+
phase_ms[ph] = _stat_block(series) if series else None
|
|
236
|
+
# Un-attributed time must stay visible: total minus the sum of phases,
|
|
237
|
+
# never hidden inside a phase (ADR-0006 §3).
|
|
238
|
+
unattributed = []
|
|
239
|
+
for i, wall in enumerate(wall_ms_runs):
|
|
240
|
+
if i < len(phase_ms_runs):
|
|
241
|
+
attributed = sum(float(v) for v in phase_ms_runs[i].values())
|
|
242
|
+
unattributed.append(wall - attributed)
|
|
243
|
+
phase_ms["unattributed_p50"] = (
|
|
244
|
+
round(p50(unattributed), 3) if unattributed else None
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
for ph in PHASES:
|
|
248
|
+
phase_ms[ph] = None
|
|
249
|
+
phase_ms["instrumentation"] = "absent"
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"schema_version": SCHEMA_VERSION,
|
|
253
|
+
"tool_version": tool_version,
|
|
254
|
+
"captured_at": captured_at or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
255
|
+
"command": command,
|
|
256
|
+
"repo": dict(repo),
|
|
257
|
+
"mode": mode,
|
|
258
|
+
"runs": len(wall_ms_runs),
|
|
259
|
+
"wall_ms": {
|
|
260
|
+
"p50": round(p50(wall_ms_runs), 3),
|
|
261
|
+
"p95": round(p95(wall_ms_runs), 3),
|
|
262
|
+
"min": round(float(min(wall_ms_runs)), 3),
|
|
263
|
+
"max": round(float(max(wall_ms_runs)), 3),
|
|
264
|
+
},
|
|
265
|
+
"phase_ms": phase_ms,
|
|
266
|
+
"rss_peak_bytes": _stat_block(rss_bytes_runs) if rss_bytes_runs else None,
|
|
267
|
+
"env": dict(env) if env is not None else collect_env(),
|
|
268
|
+
}
|
sourcecode/repository_ir.py
CHANGED
|
@@ -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
|
|
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,
|
|
@@ -5098,11 +5118,23 @@ def build_repo_ir(
|
|
|
5098
5118
|
_r["security_annotations"] = {"policy": "xml_or_filter_chain"}
|
|
5099
5119
|
|
|
5100
5120
|
# L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
|
|
5121
|
+
# symbol_kinds is the deterministic per-kind breakdown; classes/methods are the
|
|
5122
|
+
# two rollups the perf harness consumes for cost-per-symbol (ADR-0006). The IR
|
|
5123
|
+
# owns the taxonomy, so the rollup lives here rather than in the harness.
|
|
5124
|
+
_kind_counts: dict[str, int] = {}
|
|
5125
|
+
for _s in all_symbols:
|
|
5126
|
+
_k = _s.symbol_kind or "unknown"
|
|
5127
|
+
_kind_counts[_k] = _kind_counts.get(_k, 0) + 1
|
|
5128
|
+
_TYPE_KINDS = ("class", "interface", "enum", "annotation", "record")
|
|
5129
|
+
_METHOD_KINDS = ("method", "constructor")
|
|
5101
5130
|
ir["analysis_meta"] = {
|
|
5102
5131
|
"files_read": _meta_files_read,
|
|
5103
5132
|
"lines_read": _meta_lines_read,
|
|
5104
5133
|
"symbols_analyzed": len(all_symbols),
|
|
5105
5134
|
"token_estimate": _meta_chars_read // 4, # 4 chars ≈ 1 token (rough approximation)
|
|
5135
|
+
"symbol_kinds": {k: _kind_counts[k] for k in sorted(_kind_counts)},
|
|
5136
|
+
"classes": sum(_kind_counts.get(k, 0) for k in _TYPE_KINDS),
|
|
5137
|
+
"methods": sum(_kind_counts.get(k, 0) for k in _METHOD_KINDS),
|
|
5106
5138
|
}
|
|
5107
5139
|
if emit_body_facts:
|
|
5108
5140
|
# Additive, in-memory only: not a cir_hash input, not serialized by the
|
|
@@ -6397,6 +6429,12 @@ def compute_blast_radius(
|
|
|
6397
6429
|
|
|
6398
6430
|
endpoints_affected: list[dict] = []
|
|
6399
6431
|
for ep in route_surface:
|
|
6432
|
+
# Test-fixture routes (tagged by build_repo_ir) are not production HTTP
|
|
6433
|
+
# exposure — skip them so the legacy `impact` command shares the single
|
|
6434
|
+
# production-endpoint definition used by the CIR surfaces. Nothing else about
|
|
6435
|
+
# blast-radius selection changes; FQN-admin routes are untouched.
|
|
6436
|
+
if ep.get("scope") == "test_util":
|
|
6437
|
+
continue
|
|
6400
6438
|
ep_class = ep.get("effective_class") or ep.get("controller") or ep.get("class") or ""
|
|
6401
6439
|
ep_symbol = ep.get("symbol") or ""
|
|
6402
6440
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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,20 +1,20 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=1AEidIhoB0bOc0Hb9b3t-h8cPmjSPMex9CxZXJhKi98,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
|
|
5
5
|
sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
|
|
6
|
-
sourcecode/ast_extractor.py,sha256=
|
|
6
|
+
sourcecode/ast_extractor.py,sha256=gBxHVDdmY5o6lcBxawYYA-2CpGbcJ8EEAewGLi5CfQ4,51477
|
|
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=
|
|
10
|
+
sourcecode/canonical_ir.py,sha256=LdP_Ri3rl0M5p-MY0ypVvQwhq1WuUF1FmaHid7zyzEg,29807
|
|
11
11
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
12
|
-
sourcecode/classifier.py,sha256=
|
|
13
|
-
sourcecode/cli.py,sha256=
|
|
12
|
+
sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
|
|
13
|
+
sourcecode/cli.py,sha256=QH7K8Uw19z5o7eXFPvVcC8ZA1KNUHS0bVL_IdOYC2CQ,324702
|
|
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
|
|
17
|
-
sourcecode/context_graph.py,sha256=
|
|
17
|
+
sourcecode/context_graph.py,sha256=i2kOX3XiaCqcR20ojbUnqoX-hJbMyLNYc7rl-n15bwE,46426
|
|
18
18
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
19
19
|
sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
|
|
20
20
|
sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
|
|
@@ -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=
|
|
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,8 @@ 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=
|
|
48
|
+
sourcecode/path_filters.py,sha256=qPKO7kRmVp2y9zjLNSuAVCbcpZrIInHE4QulLUlzPFI,10412
|
|
49
|
+
sourcecode/perf.py,sha256=GAcEoouPIlPMCQIcHNToxK6K3WdIR-lj9aFg4prOYJI,9743
|
|
49
50
|
sourcecode/pr_comment_renderer.py,sha256=KmcjMruhR44gjzMDJwjBSkWP9QEvh8xWBLyxzxoRbj0,14542
|
|
50
51
|
sourcecode/pr_impact.py,sha256=dCDVw83EDbyVf6F9ZmEQmsFz8ruVH7d4mpeKQCIZHM0,16805
|
|
51
52
|
sourcecode/prepare_context.py,sha256=-Xu33qCETuqw-OFfT3ukvAtxde7krCjQpn7wR3pbdOg,224290
|
|
@@ -56,7 +57,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
|
56
57
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
57
58
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
58
59
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
59
|
-
sourcecode/repository_ir.py,sha256=
|
|
60
|
+
sourcecode/repository_ir.py,sha256=mLsnKeZkbsEF2Jzl7FOKMtzo4lR9UTyrk2KhbeL4FcE,313566
|
|
60
61
|
sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
|
|
61
62
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
62
63
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
@@ -67,7 +68,7 @@ sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4Bb
|
|
|
67
68
|
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
68
69
|
sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
|
|
69
70
|
sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
|
|
70
|
-
sourcecode/serializer.py,sha256=
|
|
71
|
+
sourcecode/serializer.py,sha256=UCGwxPZw9jS_amhU4om9a4Z6VOGOpttNuPpLuhGnuuU,132052
|
|
71
72
|
sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
|
|
72
73
|
sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
|
|
73
74
|
sourcecode/spring_impact.py,sha256=MkUI27OCOhkjvBoA4f9X1eDBIY0SmyPuLVCWIfZgOcE,72430
|
|
@@ -75,7 +76,7 @@ sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16
|
|
|
75
76
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
76
77
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
77
78
|
sourcecode/spring_tx_analyzer.py,sha256=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
|
|
78
|
-
sourcecode/summarizer.py,sha256=
|
|
79
|
+
sourcecode/summarizer.py,sha256=sr0-tfecFKCr-fSkPPWbl-t9HC7SY2ZxkjnnXX8DB2A,26621
|
|
79
80
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
80
81
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
81
82
|
sourcecode/validation_inference.py,sha256=UtmYv-d4TEzT1ihGEipAhtXDrwq0XtVtjMpb3i7NsNc,10815
|
|
@@ -91,7 +92,7 @@ sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJ
|
|
|
91
92
|
sourcecode/detectors/go.py,sha256=2r66uRQfeTWsqxr4HDhT6vExZErby0t46QXLHVBRv9w,2782
|
|
92
93
|
sourcecode/detectors/heuristic.py,sha256=7cRxrip4yIaggYzZJB6ef8yHKh-gHgiH_pXMFcjlyFU,3723
|
|
93
94
|
sourcecode/detectors/hybrid.py,sha256=IGFRUVsAZ1ooRlFdznCeJAV6vy1yVDx-VyghvLtddXc,9101
|
|
94
|
-
sourcecode/detectors/java.py,sha256=
|
|
95
|
+
sourcecode/detectors/java.py,sha256=t6uDee_4pTmkbCQ5aXXqtVfoPLnvVBhnxYYVrv5qrdk,44499
|
|
95
96
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
96
97
|
sourcecode/detectors/nodejs.py,sha256=Hg3Gmr7yIMJFiLoDwOTk2wtu00wxIs6kZf-oQujTFUA,13187
|
|
97
98
|
sourcecode/detectors/parsers.py,sha256=ug9K31tyHqinmv0HkIVQVjdTZpBv67FYKAEf52YXOSM,3178
|
|
@@ -137,8 +138,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
|
|
|
137
138
|
sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
|
|
138
139
|
sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
|
|
139
140
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
140
|
-
sourcecode-2.5.
|
|
141
|
-
sourcecode-2.5.
|
|
142
|
-
sourcecode-2.5.
|
|
143
|
-
sourcecode-2.5.
|
|
144
|
-
sourcecode-2.5.
|
|
141
|
+
sourcecode-2.5.16.dist-info/METADATA,sha256=zog1kb8UlXASniP5jKFyloD-2tLZ-sLMt80zRHaDqo0,10852
|
|
142
|
+
sourcecode-2.5.16.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
143
|
+
sourcecode-2.5.16.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
144
|
+
sourcecode-2.5.16.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
145
|
+
sourcecode-2.5.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|