sourcecode 2.5.15__py3-none-any.whl → 2.5.17__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/adaptive_scanner.py +12 -36
- sourcecode/ast_extractor.py +12 -19
- sourcecode/cli.py +11 -0
- sourcecode/context_graph.py +35 -0
- sourcecode/coverage_parser.py +0 -14
- sourcecode/env_analyzer.py +0 -5
- sourcecode/perf.py +268 -0
- sourcecode/repository_ir.py +12 -39
- sourcecode/scanner.py +55 -37
- sourcecode/serializer.py +0 -61
- sourcecode/spring_tx_analyzer.py +0 -24
- {sourcecode-2.5.15.dist-info → sourcecode-2.5.17.dist-info}/METADATA +1 -1
- {sourcecode-2.5.15.dist-info → sourcecode-2.5.17.dist-info}/RECORD +17 -16
- {sourcecode-2.5.15.dist-info → sourcecode-2.5.17.dist-info}/WHEEL +0 -0
- {sourcecode-2.5.15.dist-info → sourcecode-2.5.17.dist-info}/entry_points.txt +0 -0
- {sourcecode-2.5.15.dist-info → sourcecode-2.5.17.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/adaptive_scanner.py
CHANGED
|
@@ -14,12 +14,18 @@ interface, same output format (None = file, dict = directory).
|
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, Optional
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
|
|
19
19
|
from pathspec import GitIgnoreSpec
|
|
20
20
|
|
|
21
21
|
from sourcecode.repo_classifier import RepoTopology
|
|
22
|
-
from sourcecode.scanner import
|
|
22
|
+
from sourcecode.scanner import (
|
|
23
|
+
DEFAULT_EXCLUDES,
|
|
24
|
+
EXCLUDED_FILE_SUFFIXES,
|
|
25
|
+
find_manifest_paths,
|
|
26
|
+
get_or_create_tree_node,
|
|
27
|
+
match_gitignore,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
class AdaptiveScanner:
|
|
@@ -100,9 +106,7 @@ class AdaptiveScanner:
|
|
|
100
106
|
return self._gitignore_spec
|
|
101
107
|
|
|
102
108
|
def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
|
|
103
|
-
|
|
104
|
-
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
105
|
-
return spec.match_file(path_to_match)
|
|
109
|
+
return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
|
|
106
110
|
|
|
107
111
|
# ------------------------------------------------------------------
|
|
108
112
|
# Depth budget computation — the core of adaptive traversal
|
|
@@ -224,40 +228,12 @@ class AdaptiveScanner:
|
|
|
224
228
|
def _get_or_create_node(
|
|
225
229
|
self, tree: dict[str, Any], parts: tuple[str, ...]
|
|
226
230
|
) -> dict[str, Any]:
|
|
227
|
-
|
|
228
|
-
for part in parts:
|
|
229
|
-
if part not in node or node[part] is None:
|
|
230
|
-
node[part] = {}
|
|
231
|
-
node = cast(dict[str, Any], node[part])
|
|
232
|
-
return node
|
|
231
|
+
return get_or_create_tree_node(tree, parts)
|
|
233
232
|
|
|
234
233
|
# ------------------------------------------------------------------
|
|
235
234
|
# Manifest discovery — same interface as FileScanner
|
|
236
235
|
# ------------------------------------------------------------------
|
|
237
236
|
|
|
238
237
|
def find_manifests(self) -> list[str]:
|
|
239
|
-
"""Find manifest files at depth 0-1.
|
|
240
|
-
|
|
241
|
-
Identical logic to FileScanner.find_manifests() — depth-0 root
|
|
242
|
-
manifests plus depth-1 sub-package manifests, hidden dirs excluded.
|
|
243
|
-
"""
|
|
244
|
-
manifests: list[str] = []
|
|
245
|
-
for name in MANIFEST_NAMES:
|
|
246
|
-
candidate = self.root / name
|
|
247
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
248
|
-
manifests.append(str(candidate))
|
|
249
|
-
try:
|
|
250
|
-
for child in self.root.iterdir():
|
|
251
|
-
if (
|
|
252
|
-
child.is_dir()
|
|
253
|
-
and not child.is_symlink()
|
|
254
|
-
and child.name not in self._excludes
|
|
255
|
-
and not child.name.startswith(".")
|
|
256
|
-
):
|
|
257
|
-
for name in MANIFEST_NAMES:
|
|
258
|
-
candidate = child / name
|
|
259
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
260
|
-
manifests.append(str(candidate))
|
|
261
|
-
except PermissionError:
|
|
262
|
-
pass
|
|
263
|
-
return manifests
|
|
238
|
+
"""Find manifest files at depth 0-1 (delegates to the shared helper)."""
|
|
239
|
+
return find_manifest_paths(self.root, self._excludes)
|
sourcecode/ast_extractor.py
CHANGED
|
@@ -176,18 +176,10 @@ def _find_child(node: Any, *types: str) -> Optional[Any]:
|
|
|
176
176
|
return None
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
def _find_all(node: Any, *types: str) -> list[Any]:
|
|
180
|
-
return [child for child in node.children if child.type in types]
|
|
181
|
-
|
|
182
|
-
|
|
183
179
|
def _text(node: Any, src: bytes) -> str:
|
|
184
180
|
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
185
181
|
|
|
186
182
|
|
|
187
|
-
def _named_children(node: Any) -> list[Any]:
|
|
188
|
-
return [c for c in node.children if c.is_named]
|
|
189
|
-
|
|
190
|
-
|
|
191
183
|
# ---------------------------------------------------------------------------
|
|
192
184
|
# Tree-sitter TS/JS extraction
|
|
193
185
|
# ---------------------------------------------------------------------------
|
|
@@ -967,25 +959,26 @@ def _java_contract_from_graph(rel_path: str, graph: "Any") -> FileContract:
|
|
|
967
959
|
nodes/edges instead of re-parsing source. Method signatures are the graph's
|
|
968
960
|
canonical compact form (`(Type)->Return`); annotations are node-grounded.
|
|
969
961
|
"""
|
|
970
|
-
file_syms =
|
|
962
|
+
file_syms = graph.symbols_in_file(rel_path)
|
|
971
963
|
type_syms = [s for s in file_syms if s.kind in ("class", "interface", "enum", "annotation")]
|
|
972
964
|
type_fqns = {s.fqn for s in type_syms}
|
|
973
965
|
|
|
974
966
|
# extends / implements edges out of this file's types (targets are simple
|
|
975
967
|
# names for out-of-repo supertypes, resolved FQNs for in-repo ones — the
|
|
976
|
-
# same forms the old regex captured for the unresolved case).
|
|
968
|
+
# same forms the old regex captured for the unresolved case). Only edges
|
|
969
|
+
# sourced at this file's types matter, so index straight to them instead of
|
|
970
|
+
# scanning the whole-repo relation set once per file.
|
|
977
971
|
extends_of: dict[str, list[str]] = {}
|
|
978
972
|
implements_of: dict[str, list[str]] = {}
|
|
979
973
|
import_targets: set[str] = set()
|
|
980
|
-
for
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
import_targets.add(r.target)
|
|
974
|
+
for fqn in type_fqns:
|
|
975
|
+
for r in graph.relations_from(fqn):
|
|
976
|
+
if r.kind == "extends":
|
|
977
|
+
extends_of.setdefault(r.source, []).append(r.target)
|
|
978
|
+
elif r.kind == "implements":
|
|
979
|
+
implements_of.setdefault(r.source, []).append(r.target)
|
|
980
|
+
elif r.kind == "imports":
|
|
981
|
+
import_targets.add(r.target)
|
|
989
982
|
|
|
990
983
|
exports: list[ExportRecord] = []
|
|
991
984
|
types: list[TypeDefinition] = []
|
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
|
|
@@ -2411,8 +2417,10 @@ def main(
|
|
|
2411
2417
|
err=True,
|
|
2412
2418
|
)
|
|
2413
2419
|
|
|
2420
|
+
perf.stop("analysis", _perf_analysis)
|
|
2414
2421
|
# 4. Serialize
|
|
2415
2422
|
_progress.update("serializing output")
|
|
2423
|
+
_perf_serialize = perf.start()
|
|
2416
2424
|
if _is_contract_mode and not agent:
|
|
2417
2425
|
from sourcecode.serializer import contract_view as _contract_view
|
|
2418
2426
|
_depth = _CONTRACT_DEPTH.get(mode, "minimal")
|
|
@@ -2455,6 +2463,9 @@ def main(
|
|
|
2455
2463
|
raw_dict = redact_dict(raw_dict)
|
|
2456
2464
|
content = _serialize_dict(raw_dict, format)
|
|
2457
2465
|
|
|
2466
|
+
perf.stop("serialize", _perf_serialize)
|
|
2467
|
+
perf.flush_recorder()
|
|
2468
|
+
|
|
2458
2469
|
# 5. Telemetry (fire-and-forget, never blocks)
|
|
2459
2470
|
try:
|
|
2460
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/coverage_parser.py
CHANGED
|
@@ -39,20 +39,6 @@ def _safe_int(value: Optional[str]) -> Optional[int]:
|
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def _decode_numbits(blob: bytes) -> list[int]:
|
|
43
|
-
"""Decodifica bitset little-endian de coverage.py .coverage SQLite.
|
|
44
|
-
|
|
45
|
-
Byte i, bit j set => linea (i*8 + j + 1) ejecutada.
|
|
46
|
-
Ejemplo: bytes([0b00000101]) => [1, 3]
|
|
47
|
-
"""
|
|
48
|
-
return [
|
|
49
|
-
i * 8 + j + 1
|
|
50
|
-
for i, byte in enumerate(blob)
|
|
51
|
-
for j in range(8)
|
|
52
|
-
if byte & (1 << j)
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
|
|
56
42
|
# ---------------------------------------------------------------------------
|
|
57
43
|
# CoverageParser
|
|
58
44
|
# ---------------------------------------------------------------------------
|
sourcecode/env_analyzer.py
CHANGED
|
@@ -534,8 +534,3 @@ class EnvAnalyzer:
|
|
|
534
534
|
def _replace_description(record, description: str):
|
|
535
535
|
from dataclasses import replace
|
|
536
536
|
return replace(record, description=description)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _replace_required(record, required: bool, default: Optional[str]):
|
|
540
|
-
from dataclasses import replace
|
|
541
|
-
return replace(record, required=required, default=default if not record.default else record.default)
|
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
|
@@ -807,11 +807,6 @@ def _pop_closed(class_stack: list[tuple[str, int]], depth: int) -> None:
|
|
|
807
807
|
class_stack.pop()
|
|
808
808
|
|
|
809
809
|
|
|
810
|
-
def _resolve_type(simple: str, import_map: dict[str, str]) -> Optional[str]:
|
|
811
|
-
base = re.sub(r'<.*', '', simple).strip().split('.')[-1]
|
|
812
|
-
return import_map.get(base)
|
|
813
|
-
|
|
814
|
-
|
|
815
810
|
def _resolve_types_from_text(text: str, import_map: dict[str, str]) -> list[str]:
|
|
816
811
|
resolved = []
|
|
817
812
|
for token in re.findall(r'\b([A-Z]\w*)\b', text):
|
|
@@ -3597,40 +3592,6 @@ def _build_evidence_bundles(
|
|
|
3597
3592
|
return bundles
|
|
3598
3593
|
|
|
3599
3594
|
|
|
3600
|
-
def _common_package_prefix(fqns: list[str]) -> str:
|
|
3601
|
-
"""Longest common dot-separated package prefix across a list of FQNs."""
|
|
3602
|
-
if not fqns:
|
|
3603
|
-
return ""
|
|
3604
|
-
# Strip class/method suffix — keep only package parts (lowercase segments)
|
|
3605
|
-
def pkg_parts(fqn: str) -> list[str]:
|
|
3606
|
-
parts = fqn.split(".")
|
|
3607
|
-
# Drop trailing class/method names (PascalCase or after '#')
|
|
3608
|
-
result = []
|
|
3609
|
-
for p in parts:
|
|
3610
|
-
if "#" in p:
|
|
3611
|
-
break
|
|
3612
|
-
if p and p[0].isupper():
|
|
3613
|
-
break
|
|
3614
|
-
result.append(p)
|
|
3615
|
-
return result
|
|
3616
|
-
|
|
3617
|
-
segs = [pkg_parts(f) for f in fqns if pkg_parts(f)]
|
|
3618
|
-
if not segs:
|
|
3619
|
-
return fqns[0].rsplit(".", 1)[0] if "." in fqns[0] else fqns[0]
|
|
3620
|
-
common = segs[0]
|
|
3621
|
-
for s in segs[1:]:
|
|
3622
|
-
new_common = []
|
|
3623
|
-
for a, b in zip(common, s):
|
|
3624
|
-
if a == b:
|
|
3625
|
-
new_common.append(a)
|
|
3626
|
-
else:
|
|
3627
|
-
break
|
|
3628
|
-
common = new_common
|
|
3629
|
-
if not common:
|
|
3630
|
-
break
|
|
3631
|
-
return ".".join(common) if common else fqns[0].rsplit(".", 1)[0]
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
3595
|
def _subsystem_label(package_prefix: str) -> str:
|
|
3635
3596
|
"""Derive short human label enforcing minimum meaningful depth.
|
|
3636
3597
|
|
|
@@ -5118,11 +5079,23 @@ def build_repo_ir(
|
|
|
5118
5079
|
_r["security_annotations"] = {"policy": "xml_or_filter_chain"}
|
|
5119
5080
|
|
|
5120
5081
|
# L-6: inject analysis_meta — files_read, lines_read, symbols_analyzed, token_estimate
|
|
5082
|
+
# symbol_kinds is the deterministic per-kind breakdown; classes/methods are the
|
|
5083
|
+
# two rollups the perf harness consumes for cost-per-symbol (ADR-0006). The IR
|
|
5084
|
+
# owns the taxonomy, so the rollup lives here rather than in the harness.
|
|
5085
|
+
_kind_counts: dict[str, int] = {}
|
|
5086
|
+
for _s in all_symbols:
|
|
5087
|
+
_k = _s.symbol_kind or "unknown"
|
|
5088
|
+
_kind_counts[_k] = _kind_counts.get(_k, 0) + 1
|
|
5089
|
+
_TYPE_KINDS = ("class", "interface", "enum", "annotation", "record")
|
|
5090
|
+
_METHOD_KINDS = ("method", "constructor")
|
|
5121
5091
|
ir["analysis_meta"] = {
|
|
5122
5092
|
"files_read": _meta_files_read,
|
|
5123
5093
|
"lines_read": _meta_lines_read,
|
|
5124
5094
|
"symbols_analyzed": len(all_symbols),
|
|
5125
5095
|
"token_estimate": _meta_chars_read // 4, # 4 chars ≈ 1 token (rough approximation)
|
|
5096
|
+
"symbol_kinds": {k: _kind_counts[k] for k in sorted(_kind_counts)},
|
|
5097
|
+
"classes": sum(_kind_counts.get(k, 0) for k in _TYPE_KINDS),
|
|
5098
|
+
"methods": sum(_kind_counts.get(k, 0) for k in _METHOD_KINDS),
|
|
5126
5099
|
}
|
|
5127
5100
|
if emit_body_facts:
|
|
5128
5101
|
# Additive, in-memory only: not a cir_hash input, not serialized by the
|
sourcecode/scanner.py
CHANGED
|
@@ -115,6 +115,57 @@ def classify_manifest(manifest_path: str, root: Path) -> str:
|
|
|
115
115
|
return "auxiliary"
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
# ── Shared scan primitives (used by both FileScanner and AdaptiveScanner) ────
|
|
119
|
+
# AdaptiveScanner was forked from FileScanner and carried byte-identical copies of
|
|
120
|
+
# these three; they live here as the single source of truth, each class method a
|
|
121
|
+
# thin delegate.
|
|
122
|
+
|
|
123
|
+
def find_manifest_paths(root: Path, excludes: frozenset[str]) -> list[str]:
|
|
124
|
+
"""Manifest files at depth 0–1 (SCAN-04): root manifests plus depth-1
|
|
125
|
+
sub-package manifests, with hidden/excluded dirs and symlinks skipped."""
|
|
126
|
+
manifests: list[str] = []
|
|
127
|
+
# Depth 0: root
|
|
128
|
+
for name in MANIFEST_NAMES:
|
|
129
|
+
candidate = root / name
|
|
130
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
131
|
+
manifests.append(str(candidate))
|
|
132
|
+
# Depth 1: first level (hidden dirs excluded — tooling, not project)
|
|
133
|
+
try:
|
|
134
|
+
for child in root.iterdir():
|
|
135
|
+
if (
|
|
136
|
+
child.is_dir()
|
|
137
|
+
and not child.is_symlink()
|
|
138
|
+
and child.name not in excludes
|
|
139
|
+
and not child.name.startswith(".")
|
|
140
|
+
):
|
|
141
|
+
for name in MANIFEST_NAMES:
|
|
142
|
+
candidate = child / name
|
|
143
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
144
|
+
manifests.append(str(candidate))
|
|
145
|
+
except PermissionError:
|
|
146
|
+
pass
|
|
147
|
+
return manifests
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_or_create_tree_node(
|
|
151
|
+
tree: dict[str, Any], parts: tuple[str, ...]
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Navigate/create the nested-dict tree node for a path's parts."""
|
|
154
|
+
node = tree
|
|
155
|
+
for part in parts:
|
|
156
|
+
if part not in node or node[part] is None:
|
|
157
|
+
node[part] = {}
|
|
158
|
+
node = cast(dict[str, Any], node[part])
|
|
159
|
+
return node
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def match_gitignore(spec: GitIgnoreSpec, rel_path: str, is_dir: bool) -> bool:
|
|
163
|
+
"""True if a root-relative path is excluded by ``spec``. GitIgnoreSpec expects
|
|
164
|
+
a trailing '/' for directories."""
|
|
165
|
+
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
166
|
+
return spec.match_file(path_to_match)
|
|
167
|
+
|
|
168
|
+
|
|
118
169
|
class FileScanner:
|
|
119
170
|
"""Escanea un directorio de proyecto y produce un arbol de ficheros filtrado.
|
|
120
171
|
|
|
@@ -148,10 +199,7 @@ class FileScanner:
|
|
|
148
199
|
|
|
149
200
|
def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
|
|
150
201
|
"""Comprueba si una ruta relativa (a self.root) esta excluida por .gitignore."""
|
|
151
|
-
|
|
152
|
-
# GitIgnoreSpec espera rutas con / al final para directorios
|
|
153
|
-
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
154
|
-
return spec.match_file(path_to_match)
|
|
202
|
+
return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
|
|
155
203
|
|
|
156
204
|
def scan_tree(self) -> dict[str, Any]:
|
|
157
205
|
"""Construye el arbol JSON anidado del proyecto.
|
|
@@ -223,38 +271,8 @@ class FileScanner:
|
|
|
223
271
|
self, tree: dict[str, Any], parts: tuple[str, ...]
|
|
224
272
|
) -> dict[str, Any]:
|
|
225
273
|
"""Navega/crea el nodo del arbol para la ruta indicada."""
|
|
226
|
-
|
|
227
|
-
for part in parts:
|
|
228
|
-
if part not in node or node[part] is None:
|
|
229
|
-
node[part] = {}
|
|
230
|
-
node = cast(dict[str, Any], node[part])
|
|
231
|
-
return node
|
|
274
|
+
return get_or_create_tree_node(tree, parts)
|
|
232
275
|
|
|
233
276
|
def find_manifests(self) -> list[str]:
|
|
234
|
-
"""Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04).
|
|
235
|
-
|
|
236
|
-
Retorna:
|
|
237
|
-
Lista de paths absolutos de manifiestos encontrados.
|
|
238
|
-
"""
|
|
239
|
-
manifests: list[str] = []
|
|
240
|
-
# Profundidad 0: raiz
|
|
241
|
-
for name in MANIFEST_NAMES:
|
|
242
|
-
candidate = self.root / name
|
|
243
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
244
|
-
manifests.append(str(candidate))
|
|
245
|
-
# Profundidad 1: primer nivel (excluir directorios ocultos — son tooling, no proyecto)
|
|
246
|
-
try:
|
|
247
|
-
for child in self.root.iterdir():
|
|
248
|
-
if (
|
|
249
|
-
child.is_dir()
|
|
250
|
-
and not child.is_symlink()
|
|
251
|
-
and child.name not in self._excludes
|
|
252
|
-
and not child.name.startswith(".")
|
|
253
|
-
):
|
|
254
|
-
for name in MANIFEST_NAMES:
|
|
255
|
-
candidate = child / name
|
|
256
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
257
|
-
manifests.append(str(candidate))
|
|
258
|
-
except PermissionError:
|
|
259
|
-
pass
|
|
260
|
-
return manifests
|
|
277
|
+
"""Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04)."""
|
|
278
|
+
return find_manifest_paths(self.root, self._excludes)
|
sourcecode/serializer.py
CHANGED
|
@@ -913,67 +913,6 @@ def _jndi_datasources(sm: "SourceMap") -> "Optional[list[dict[str, Any]]]":
|
|
|
913
913
|
return datasources
|
|
914
914
|
|
|
915
915
|
|
|
916
|
-
def _tiered_display_score(
|
|
917
|
-
pre_bonus_combined: float,
|
|
918
|
-
file_class: Any,
|
|
919
|
-
path: str,
|
|
920
|
-
entry_paths: set,
|
|
921
|
-
has_structural_signals: bool = False,
|
|
922
|
-
) -> float:
|
|
923
|
-
"""Evidence-tiered display score [0.0, 1.0].
|
|
924
|
-
|
|
925
|
-
Tiers enforce: strong evidence > medium evidence > filesystem/path only.
|
|
926
|
-
M3 sort bonuses must NOT be included in pre_bonus_combined — they are for
|
|
927
|
-
ordering only and must not inflate the displayed score.
|
|
928
|
-
|
|
929
|
-
Tier ceilings:
|
|
930
|
-
T1 confirmed production entrypoint 0.92–1.00
|
|
931
|
-
T2 entrypoint (weaker category) 0.80–0.91
|
|
932
|
-
T3 annotation-confirmed stereotype 0.40–0.90 (table-calibrated)
|
|
933
|
-
T4 framework import evidence 0.55–0.79
|
|
934
|
-
T5 code definitions + imports 0.38–0.54
|
|
935
|
-
T6 build manifest / tooling / test 0.25–0.45
|
|
936
|
-
T7 path/filesystem signal only 0.10–0.39
|
|
937
|
-
"""
|
|
938
|
-
from sourcecode.file_classifier import JAVA_STEREOTYPE_CATEGORIES
|
|
939
|
-
|
|
940
|
-
cat = file_class.category if file_class else None
|
|
941
|
-
base_rel = file_class.relevance if file_class else 0.0
|
|
942
|
-
|
|
943
|
-
# T1: confirmed production entrypoint
|
|
944
|
-
if path in entry_paths and cat in ("runtime_core", "cli_entrypoint"):
|
|
945
|
-
return round(min(1.0, max(0.92, base_rel)), 3)
|
|
946
|
-
|
|
947
|
-
# T2: in entry_paths but weaker evidence category
|
|
948
|
-
if path in entry_paths:
|
|
949
|
-
return round(min(0.91, max(0.80, pre_bonus_combined / 2.0)), 3)
|
|
950
|
-
|
|
951
|
-
# T3: annotation-confirmed stereotype — table values are already calibrated
|
|
952
|
-
if file_class and cat in JAVA_STEREOTYPE_CATEGORIES:
|
|
953
|
-
return round(base_rel, 3)
|
|
954
|
-
|
|
955
|
-
# T4: framework import evidence (medium strength)
|
|
956
|
-
if cat in ("api_layer", "database_layer", "infrastructure"):
|
|
957
|
-
return round(min(0.79, max(0.55, pre_bonus_combined / 2.0)), 3)
|
|
958
|
-
|
|
959
|
-
# T5: code definitions with imports (medium-low)
|
|
960
|
-
if cat in ("application_logic", "domain_model"):
|
|
961
|
-
return round(min(0.54, max(0.38, pre_bonus_combined / 2.0)), 3)
|
|
962
|
-
|
|
963
|
-
# T6: build manifest / tooling / test
|
|
964
|
-
if cat == "build_system":
|
|
965
|
-
return round(min(0.45, base_rel), 3)
|
|
966
|
-
if cat in ("tests", "tooling"):
|
|
967
|
-
return round(min(0.35, base_rel), 3)
|
|
968
|
-
|
|
969
|
-
# T7: no content classification — filesystem/structural signals only
|
|
970
|
-
# has_structural_signals: fan_in, churn, export — allows up to 0.54
|
|
971
|
-
# pure path/filename only — hard cap 0.39
|
|
972
|
-
if has_structural_signals:
|
|
973
|
-
return round(min(0.54, max(0.10, pre_bonus_combined / 2.0)), 3)
|
|
974
|
-
return round(min(0.39, max(0.10, pre_bonus_combined / 2.0)), 3)
|
|
975
|
-
|
|
976
|
-
|
|
977
916
|
def _build_file_signals(
|
|
978
917
|
file_class: Any,
|
|
979
918
|
path: str,
|
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -112,30 +112,6 @@ def _caught_types(catch_header_inner: str) -> list[str]:
|
|
|
112
112
|
return types
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def _extract_method_body(source: str, method_name: str) -> str:
|
|
116
|
-
"""Extract the first method body matching method_name using brace counting.
|
|
117
|
-
|
|
118
|
-
Returns the text from '{' to the matching '}', or empty string if not found.
|
|
119
|
-
Needed to scope TX-005 regex to the specific method instead of the whole file.
|
|
120
|
-
"""
|
|
121
|
-
pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
|
|
122
|
-
for m in pattern.finditer(source):
|
|
123
|
-
brace_pos = source.find('{', m.end())
|
|
124
|
-
if brace_pos < 0:
|
|
125
|
-
continue
|
|
126
|
-
depth = 1
|
|
127
|
-
i = brace_pos + 1
|
|
128
|
-
while i < len(source) and depth > 0:
|
|
129
|
-
c = source[i]
|
|
130
|
-
if c == '{':
|
|
131
|
-
depth += 1
|
|
132
|
-
elif c == '}':
|
|
133
|
-
depth -= 1
|
|
134
|
-
i += 1
|
|
135
|
-
return source[brace_pos:i]
|
|
136
|
-
return ""
|
|
137
|
-
|
|
138
|
-
|
|
139
115
|
# ---------------------------------------------------------------------------
|
|
140
116
|
# Pattern protocol
|
|
141
117
|
# ---------------------------------------------------------------------------
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
2
|
-
sourcecode/adaptive_scanner.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=Ao5tzW0DQ-NV-o4Yno0H_QEOlx1oyQh_ih-rdREfXDs,309
|
|
2
|
+
sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
|
|
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=ZwHN3Y0iRp2gpv3t5Nm5uvX7Xw1KtULCJPZqKpZQRAE,51255
|
|
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
10
|
sourcecode/canonical_ir.py,sha256=LdP_Ri3rl0M5p-MY0ypVvQwhq1WuUF1FmaHid7zyzEg,29807
|
|
11
11
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
12
12
|
sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
|
|
13
|
-
sourcecode/cli.py,sha256=
|
|
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
|
|
21
21
|
sourcecode/contract_pipeline.py,sha256=KfSgNkebcL5bYx3TDzEnvPLcTgm_JifJOAqqR8-0zb4,29774
|
|
22
|
-
sourcecode/coverage_parser.py,sha256=
|
|
22
|
+
sourcecode/coverage_parser.py,sha256=X9scuUWxacahYTrTBGea2Ku-sf-WCVcnc9OEB35_3IU,19340
|
|
23
23
|
sourcecode/dependency_analyzer.py,sha256=fQCaWQ7_BNcVgZv8gweJCwJ1CGlXsz8nw_tqN3aHJNE,63952
|
|
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
27
|
sourcecode/endpoint_metrics.py,sha256=sLSLUIgiIyvNdOynaxVgHd9SjRF_DbN3QxevaquykaU,2840
|
|
28
28
|
sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
|
|
29
|
-
sourcecode/env_analyzer.py,sha256=
|
|
29
|
+
sourcecode/env_analyzer.py,sha256=oLz4gDUE3BHlRRn6Qj4rnbjwyYjIZ0nlqO_SBQwL_H8,21999
|
|
30
30
|
sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
|
|
31
31
|
sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpXxo,4277
|
|
32
32
|
sourcecode/explain.py,sha256=HnHWVTNNf9fzeR3FP-A-eXKeKZvBcUEuODgIO3rJQkQ,22312
|
|
@@ -46,6 +46,7 @@ sourcecode/migrate_check.py,sha256=HJhvHuvXDGTJSRQU9_AMx0L94h2TImC1DCGs3m3RTu0,1
|
|
|
46
46
|
sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
|
|
47
47
|
sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
|
|
48
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,10 +57,10 @@ 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=Q2lMpm5x8ZAIY4MVKWEcLZ-BP26kUoP1dRfdER6gSbo,312316
|
|
60
61
|
sourcecode/ris.py,sha256=Hw8TakTQ6hku-Abf2k8954NwkrH_sP73_8wVH8x5khc,22079
|
|
61
62
|
sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
|
|
62
|
-
sourcecode/scanner.py,sha256=
|
|
63
|
+
sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
|
|
63
64
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
64
65
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
65
66
|
sourcecode/security_posture.py,sha256=CAJw4oJD0aAW2yRewvd_fonkzi9_HpUjBZlIDZle00s,26034
|
|
@@ -67,14 +68,14 @@ 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=zmN3pOcfxaQdxCE-jKoKudFjof9Sc_q7jpcBzNIA0EY,129493
|
|
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
|
|
74
75
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
75
76
|
sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
|
|
76
77
|
sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
|
|
77
|
-
sourcecode/spring_tx_analyzer.py,sha256=
|
|
78
|
+
sourcecode/spring_tx_analyzer.py,sha256=7qXwQR9QbpVxbxJC-mawBMXM9xzydtZeq5L18cP_6GE,33884
|
|
78
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
|
|
@@ -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.17.dist-info/METADATA,sha256=Pp6k3FfuZdHiohzyuajk3p9MPq7gTCOecNy-m3iBI98,10852
|
|
142
|
+
sourcecode-2.5.17.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
143
|
+
sourcecode-2.5.17.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
144
|
+
sourcecode-2.5.17.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
145
|
+
sourcecode-2.5.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|