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 CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.15"
7
+ __version__ = "2.5.17"
@@ -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, cast
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 DEFAULT_EXCLUDES, EXCLUDED_FILE_SUFFIXES, MANIFEST_NAMES
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
- spec = self._load_gitignore_spec()
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
- node = tree
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)
@@ -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 = [s for s in graph.symbols() if s.source_file == rel_path]
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 r in graph.relations():
981
- if r.source not in type_fqns:
982
- continue
983
- if r.kind == "extends":
984
- extends_of.setdefault(r.source, []).append(r.target)
985
- elif r.kind == "implements":
986
- implements_of.setdefault(r.source, []).append(r.target)
987
- elif r.kind == "imports":
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
@@ -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(
@@ -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
  # ---------------------------------------------------------------------------
@@ -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
+ }
@@ -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
- spec = self._load_gitignore_spec()
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
- node = tree
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,
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.15
3
+ Version: 2.5.17
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -1,32 +1,32 @@
1
- sourcecode/__init__.py,sha256=TA67wvVQzG1GVLowfrLngK0bKQZu-eJyvWrAm5PiAdg,309
2
- sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
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=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
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=LKK4czUeENVlcxHpSd5xao2HnAgtbRSYo8OoSwcZeZg,324334
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=WFtUvfxoE8XtQxtRJLvaF4SJuS00ICjNoTk0sH2_V5U,44454
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=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
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=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
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=3hpxzOrudflSteYDLMQHK7KWdXg6mhhVwG94JSn7FHY,312825
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=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
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=UCGwxPZw9jS_amhU4om9a4Z6VOGOpttNuPpLuhGnuuU,132052
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=QMXkxa_hrqDVN3D8WJsd0nT7vfdZCT1E8ZWNoFFbvgU,34692
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.15.dist-info/METADATA,sha256=lClKdlUiQRzl134vAQzx_Nvdk0pfm5mGK0pfIT0FIxs,10852
141
- sourcecode-2.5.15.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
142
- sourcecode-2.5.15.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
143
- sourcecode-2.5.15.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
144
- sourcecode-2.5.15.dist-info/RECORD,,
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,,