sourcecode 5.5.0__py3-none-any.whl → 5.5.2__py3-none-any.whl

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

Potentially problematic release.


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

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__ = "5.5.0"
7
+ __version__ = "5.5.2"
@@ -136,7 +136,7 @@ pipx install sourcecode # isolated install, no venv needed
136
136
 
137
137
  # Verify
138
138
  ask version
139
- # ask 5.5.0
139
+ # ask 5.5.2
140
140
  ```
141
141
 
142
142
  Requires Python 3.9+.
sourcecode/cli.py CHANGED
@@ -3125,6 +3125,7 @@ def main(
3125
3125
  raise typer.Exit()
3126
3126
 
3127
3127
  _t0 = time.monotonic()
3128
+ _root_timing: dict[str, float] = {}
3128
3129
  no_tree: bool = False # set True by --agent; --no-tree flag removed
3129
3130
 
3130
3131
  # Validate new flag choices
@@ -3457,6 +3458,9 @@ def main(
3457
3458
  _core_hash = ""
3458
3459
  _core_flags_str = ""
3459
3460
  _view_flags_str = ""
3461
+ # Reuse the freshness decision for cache metadata; otherwise the root
3462
+ # command repeats a full Git status walk after the analysis completes.
3463
+ _tree_is_dirty: Optional[bool] = None
3460
3464
 
3461
3465
  if not no_cache:
3462
3466
  try:
@@ -3506,6 +3510,8 @@ def main(
3506
3510
  Path(_git_root_str) if _git_root_str else target,
3507
3511
  scope=target,
3508
3512
  )
3513
+ if _tree_sig and _git_sha:
3514
+ _tree_is_dirty = _tree_sig != _git_sha
3509
3515
  if _tree_sig:
3510
3516
  _core_key = f"{_tree_sig}-{_core_h}"
3511
3517
  else:
@@ -3821,7 +3827,11 @@ def main(
3821
3827
  if format == "json":
3822
3828
  try:
3823
3829
  from sourcecode.ris import _has_uncommitted_changes as _huc
3824
- _uncommitted = _huc(target)
3830
+ _uncommitted = (
3831
+ _tree_is_dirty
3832
+ if _tree_is_dirty is not None
3833
+ else _huc(target)
3834
+ )
3825
3835
  except Exception:
3826
3836
  _uncommitted = False
3827
3837
  _data_scope = "COMPACT" if compact else ("AGENT" if agent else "FULL")
@@ -3938,6 +3948,7 @@ def main(
3938
3948
  if _advisory:
3939
3949
  typer.echo(_advisory, err=True)
3940
3950
 
3951
+ _manifest_t0 = time.perf_counter()
3941
3952
  _perf_detection = perf.start()
3942
3953
  detector = ProjectDetector(build_default_detectors())
3943
3954
  workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
@@ -3998,6 +4009,7 @@ def main(
3998
4009
  root_dependencies, root_summary = dependency_analyzer.analyze(target)
3999
4010
  dependency_records.extend(root_dependencies)
4000
4011
  dependency_summaries.append(root_summary)
4012
+ _root_timing["manifests_ms"] = round((time.perf_counter() - _manifest_t0) * 1000, 2)
4001
4013
  module_graphs = []
4002
4014
  if graph_analyzer is not None:
4003
4015
  root_graph_tree = (
@@ -4643,6 +4655,13 @@ def main(
4643
4655
  )
4644
4656
 
4645
4657
  perf.stop("analysis", _perf_analysis)
4658
+ # Root timings are deliberately a small diagnostic envelope. They make cold
4659
+ # manifest cost visible even when the optional perf recorder is disabled.
4660
+ _root_timing.setdefault("context_ms", round((time.monotonic() - _t0) * 1000, 2))
4661
+ _root_timing.setdefault("parse_ms", 0.0)
4662
+ _root_timing.setdefault("cir_ms", 0.0)
4663
+ _root_timing.setdefault("ris_ms", 0.0)
4664
+ _root_timing.setdefault("snapshot_ms", 0.0)
4646
4665
  # 4. Serialize
4647
4666
  _progress.update("serializing output")
4648
4667
  _perf_serialize = perf.start()
@@ -4688,8 +4707,16 @@ def main(
4688
4707
  raw_dict = standard_view(sm, include_tree=tree and not no_tree)
4689
4708
  if not no_redact:
4690
4709
  raw_dict = redact_dict(raw_dict)
4710
+ data = raw_dict
4691
4711
  content = _serialize_dict(raw_dict, format)
4692
4712
 
4713
+ if isinstance(data, dict):
4714
+ data.setdefault("_meta", {})["timing_ms"] = {
4715
+ **_root_timing,
4716
+ "total_ms": round((time.monotonic() - _t0) * 1000, 2),
4717
+ }
4718
+ content = _serialize_dict(data, format)
4719
+
4693
4720
  perf.stop("serialize", _perf_serialize)
4694
4721
  perf.flush_recorder()
4695
4722
 
@@ -4717,7 +4744,11 @@ def main(
4717
4744
  if format == "json":
4718
4745
  try:
4719
4746
  from sourcecode.ris import _has_uncommitted_changes as _huc_fresh
4720
- _uncommitted_fresh = _huc_fresh(target)
4747
+ _uncommitted_fresh = (
4748
+ _tree_is_dirty
4749
+ if _tree_is_dirty is not None
4750
+ else _huc_fresh(target)
4751
+ )
4721
4752
  # _huc_fresh uses --untracked-files=no — it misses repos where all
4722
4753
  # files are untracked (no staged/unstaged changes to tracked files).
4723
4754
  # Promote to True when _allowed_changed_files contains untracked files.
@@ -6503,6 +6534,10 @@ def endpoints_cmd(
6503
6534
  "the endpoint count, and the endpoint total is carried across as one "
6504
6535
  "number rather than 3 500 rows.",
6505
6536
  ),
6537
+ include_tests: bool = typer.Option(
6538
+ False, "--include-tests",
6539
+ help="Include endpoints from test source and test-fixture modules; excluded by default.",
6540
+ ),
6506
6541
  client_usage: bool = typer.Option(
6507
6542
  False, "--client-usage",
6508
6543
  help="Join the routes against the HTTP calls written in the TypeScript/"
@@ -6563,7 +6598,7 @@ def endpoints_cmd(
6563
6598
  _prog = Progress()
6564
6599
  _prog.start("scanning endpoints")
6565
6600
  try:
6566
- data = _extract_java_endpoints(target)
6601
+ data = _extract_java_endpoints(target, include_tests=include_tests)
6567
6602
  finally:
6568
6603
  _prog.finish()
6569
6604
 
@@ -8989,9 +9024,26 @@ def spring_audit_cmd(
8989
9024
  # 9,53 s on a 1 303-file repository) and it is the same parse the other
8990
9025
  # knowledge commands share, so it is fetched rather than rebuilt — which
8991
9026
  # is what makes a warm worth anything here (cold == warm before C3-94).
8992
- cir = _ctxcache.shared_cir(
8993
- target, file_list, progress=_work_sink(_prog)
9027
+ _repo_root = _ctxcache.repo_root_of(target)
9028
+ cir, _cir_lookup = _ctxcache.get_or_build_cir(
9029
+ _repo_root, target, file_list, progress=_work_sink(_prog)
8994
9030
  )
9031
+ _cache_state = {
9032
+ "context": "warm" if _cir_lookup.hit else "cold",
9033
+ "parse": "warm" if _cir_lookup.hit else "cold",
9034
+ }
9035
+ try:
9036
+ from sourcecode import cache as _cache_state_mod
9037
+ _cache_status = _cache_state_mod.status(target)
9038
+ _cache_state["snapshot"] = (
9039
+ "warm" if _cache_status.get("cached_cores") else "cold"
9040
+ )
9041
+ _cache_state["ris"] = (
9042
+ "warm" if _cache_status.get("ris_exists") else "cold"
9043
+ )
9044
+ except Exception:
9045
+ _cache_state["snapshot"] = "unknown"
9046
+ _cache_state["ris"] = "unknown"
8995
9047
  # C3-85: the stretch between the last `linking` tick and the first rule
8996
9048
  # tick reports nothing, and what stays on the line is `n/n` — a counter
8997
9049
  # that finished, reading as a stage still running. Measured on keycloak
@@ -9142,7 +9194,18 @@ def spring_audit_cmd(
9142
9194
  "phase that never ran is not evidence there is none."
9143
9195
  )
9144
9196
 
9145
- data = combined.to_compact_dict() if compact else combined.to_dict()
9197
+ _bounded_default = (
9198
+ not compact and output_path is None and format == "json" and not table
9199
+ )
9200
+ data = (
9201
+ combined.to_compact_dict()
9202
+ if compact or _bounded_default
9203
+ else combined.to_dict()
9204
+ )
9205
+ if _bounded_default:
9206
+ data.pop("mode", None)
9207
+ data["default_output"] = "bounded"
9208
+ data.setdefault("metadata", {})["cache_layers"] = _cache_state
9146
9209
  if _stopped_early:
9147
9210
  # C3-87: the block is composed by `PhasedRun.status`, which is the
9148
9211
  # authority `risk` is now bound to as well — one vocabulary for
@@ -9176,6 +9239,7 @@ def spring_audit_cmd(
9176
9239
  elif format == "github-comment":
9177
9240
  output = _render_spring_audit_github_comment(combined, min_severity)
9178
9241
  else:
9242
+ data = combined.to_compact_dict() if compact else combined.to_dict()
9179
9243
  output = _serialize_dict(data, format)
9180
9244
 
9181
9245
  # F-AV: our own readers ask for the count under either spelling — the break
@@ -9763,6 +9827,9 @@ def risk_cmd(
9763
9827
  data, rule_filter=rule_filter, band_filter=band_filter, top_n=top_n
9764
9828
  )
9765
9829
  else:
9830
+ if output_path is None:
9831
+ from sourcecode.risk import compact_risk_output
9832
+ data = compact_risk_output(data)
9766
9833
  output = _serialize_dict(data, format)
9767
9834
  _emit_command_output(
9768
9835
  output,
@@ -162,6 +162,15 @@ class DependencyAnalyzer:
162
162
  *,
163
163
  workspace: str | None = None,
164
164
  ) -> tuple[list[DependencyRecord], DependencySummary]:
165
+ from sourcecode import manifest_cache
166
+
167
+ cached = manifest_cache.load(root)
168
+ if cached is not None:
169
+ records, summary = cached
170
+ if workspace is not None:
171
+ records = [replace(record, workspace=workspace) for record in records]
172
+ summary = replace(summary, dependencies=records)
173
+ return records, summary
165
174
  records: list[DependencyRecord] = []
166
175
  limitations: list[str] = []
167
176
 
@@ -185,7 +194,12 @@ class DependencyAnalyzer:
185
194
  replace(r, role=_infer_role(r.name, r.ecosystem, r.scope))
186
195
  for r in deduped
187
196
  ]
188
- return deduped, self._build_summary(deduped, limitations)
197
+ summary = self._build_summary(deduped, limitations)
198
+ manifest_cache.save(root, deduped, summary)
199
+ if workspace is not None:
200
+ deduped = [replace(record, workspace=workspace) for record in deduped]
201
+ summary = replace(summary, dependencies=deduped)
202
+ return deduped, summary
189
203
 
190
204
  def merge_summaries(self, summaries: Iterable[DependencySummary]) -> DependencySummary:
191
205
  result = DependencySummary(requested=True)
@@ -0,0 +1,62 @@
1
+ """Content-addressed cache for manifest dependency analysis."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from dataclasses import asdict
8
+ from pathlib import Path
9
+
10
+ from sourcecode.schema import DependencyRecord, DependencySummary
11
+
12
+
13
+ def _base() -> Path:
14
+ configured = os.environ.get("SOURCECODE_CACHE_DIR", "")
15
+ return Path(configured) if configured else Path.home() / ".sourcecode" / "cache"
16
+
17
+
18
+ def _key(root: Path) -> str:
19
+ digest = hashlib.sha256()
20
+ analyzer = Path(__file__).with_name("dependency_analyzer.py")
21
+ digest.update(analyzer.read_bytes())
22
+ names = {
23
+ "pom.xml", "build.gradle", "build.gradle.kts", "package.json",
24
+ "package-lock.json", "pyproject.toml", "Cargo.toml", "go.mod",
25
+ "composer.json", "Gemfile",
26
+ }
27
+ paths: set[Path] = set()
28
+ for name in names:
29
+ paths.update(root.rglob(name))
30
+ for path in sorted(paths):
31
+ if not path.is_file():
32
+ continue
33
+ try:
34
+ digest.update(path.relative_to(root).as_posix().encode())
35
+ digest.update(path.read_bytes())
36
+ except OSError:
37
+ continue
38
+ return digest.hexdigest()
39
+
40
+
41
+ def load(root: Path) -> tuple[list[DependencyRecord], DependencySummary] | None:
42
+ try:
43
+ path = _base() / "manifest-cache-v1" / f"{_key(root)}.json"
44
+ data = json.loads(path.read_text(encoding="utf-8"))
45
+ records = [DependencyRecord(**row) for row in data["records"]]
46
+ summary = DependencySummary(**data["summary"])
47
+ summary.dependencies = records
48
+ return records, summary
49
+ except (OSError, ValueError, KeyError, TypeError):
50
+ return None
51
+
52
+
53
+ def save(root: Path, records: list[DependencyRecord], summary: DependencySummary) -> None:
54
+ try:
55
+ path = _base() / "manifest-cache-v1" / f"{_key(root)}.json"
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ payload = {"records": [asdict(row) for row in records], "summary": asdict(summary)}
58
+ tmp = path.with_suffix(".tmp")
59
+ tmp.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8")
60
+ tmp.replace(path)
61
+ except OSError:
62
+ return
@@ -2375,6 +2375,26 @@ class MigrationReport:
2375
2375
  compact, "findings", top_n,
2376
2376
  key=lambda f: SEVERITY_ORDER.get(f.get("severity"), 3),
2377
2377
  )
2378
+ # Keep invariant rule prose once; each finding carries only location and
2379
+ # rule identity. Full prose remains available in the non-compact report.
2380
+ _rule_catalog: dict[str, dict] = {}
2381
+ _lean_findings: list[dict] = []
2382
+ for _finding in compact.get("findings", []):
2383
+ _rule_id = _finding.get("rule_id", "unknown")
2384
+ _rule_catalog.setdefault(_rule_id, {
2385
+ "title": _finding.get("title", ""),
2386
+ "explanation": _finding.get("explanation", ""),
2387
+ "fix_hint": _finding.get("fix_hint", ""),
2388
+ })
2389
+ _lean_findings.append({
2390
+ key: _finding[key]
2391
+ for key in (
2392
+ "id", "rule_id", "severity", "source_file", "first_line",
2393
+ "migration_target", "imports_found", "code_context",
2394
+ ) if key in _finding
2395
+ })
2396
+ compact["rule_catalog"] = _rule_catalog
2397
+ compact["findings"] = _lean_findings
2378
2398
  hib = full.get("hibernate")
2379
2399
  if isinstance(hib, dict):
2380
2400
  h = dict(hib)
@@ -223,6 +223,18 @@ def _scan_repository(
223
223
  for fqn in by_qualified[hit]:
224
224
  if fqn not in nested_hits and nested[fqn][1] != rel:
225
225
  nested_hits[fqn] = rel
226
+ # Nested types are often used unqualified by their enclosing
227
+ # class (`private Operation operation` or `Map<String, DATATYPE>`).
228
+ # The declaration is not a reference; a second occurrence is.
229
+ for fqn, (qualified, owner_file) in nested.items():
230
+ if owner_file != rel or fqn in nested_hits:
231
+ continue
232
+ nested_name = qualified.rsplit(".", 1)[-1]
233
+ unqualified = re.findall(
234
+ rf"(?<!\.)\b{re.escape(nested_name)}\b", text
235
+ )
236
+ if len(unqualified) > 1:
237
+ nested_hits[fqn] = rel
226
238
  scan_java = len(nested_hits) < len(nested)
227
239
  if not scan_config and not scan_java:
228
240
  return config_hits, nested_hits, literal_hits
@@ -311,7 +323,7 @@ def analyze_type_references(nodes: "list[dict]", root: Path) -> ReferenceFacts:
311
323
  entries.append(TypeReference(
312
324
  status=REFERENCED,
313
325
  basis=(
314
- f"named as a qualified nested type in {nested_hits[fqn]}"
326
+ f"referenced by its enclosing source in {nested_hits[fqn]}"
315
327
  if fqn in nested_hits
316
328
  else f"referenced by a class literal in {literal_refs[fqn]}"
317
329
  ),
@@ -27,7 +27,7 @@ from typing import Any, Optional
27
27
  #: The date `__version__` was released, from that version's CHANGELOG entry.
28
28
  #: `tests/test_release_info.py` fails the build if the two disagree, so this is
29
29
  #: a copy of a fact rather than a second authority for it.
30
- RELEASE_DATE = "2026-08-12"
30
+ RELEASE_DATE = "2026-08-13"
31
31
 
32
32
  #: Releases per day, measured over the 30 releases before this one (the same
33
33
  #: computation the test re-runs against the CHANGELOG). Stable across window
@@ -6362,10 +6362,13 @@ def is_test_source_file(rel_path: str) -> bool:
6362
6362
  # Convenience: find Java files in a repo
6363
6363
  # ---------------------------------------------------------------------------
6364
6364
 
6365
- def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6365
+ def extract_java_endpoints(
6366
+ root: Path, *, include_tests: bool = False
6367
+ ) -> "dict[str, Any]":
6366
6368
  """Extract REST endpoint surface from Java source files.
6367
6369
 
6368
6370
  Canonical endpoint extractor — uses IR symbol extraction + _build_route_surface().
6371
+ Test-fixture modules remain auditable by default and can be included explicitly.
6369
6372
  Security extraction delegated to _route_security_from_sym (single source of truth);
6370
6373
  results stored in route["security_annotations"] by _build_route_surface.
6371
6374
 
@@ -6408,7 +6411,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6408
6411
 
6409
6412
  java_files = sorted(
6410
6413
  p for p in root.rglob("*.java")
6411
- if not is_test_source_file(_rel(p))
6414
+ if (include_tests or not is_test_source_file(_rel(p)))
6412
6415
  and "target/" not in str(p).replace("\\", "/")
6413
6416
  and not any(f in str(p).replace("\\", "/") for f in _CLIENT_PATH_FRAGMENTS)
6414
6417
  )
@@ -6674,7 +6677,8 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6674
6677
  # counted with, the real API surface — but they are preserved in their own bucket
6675
6678
  # so nothing is silently dropped and the exclusion is auditable.
6676
6679
  _test_util_endpoints = [e for e in endpoints if e.get("scope") == "test_util"]
6677
- endpoints = [e for e in endpoints if e.get("scope") != "test_util"]
6680
+ if not include_tests:
6681
+ endpoints = [e for e in endpoints if e.get("scope") != "test_util"]
6678
6682
 
6679
6683
  # "no_security_signal" = no recognized security annotation at method OR class level.
6680
6684
  # Note: repos may use framework-level security (e.g. Keycloak itself) with no
@@ -6898,7 +6902,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6898
6902
  pass
6899
6903
  # Surface test-fixture endpoints excluded from the production surface above.
6900
6904
  # Reported separately (not hidden) so the exclusion is transparent and auditable.
6901
- if _test_util_endpoints:
6905
+ if _test_util_endpoints and not include_tests:
6902
6906
  result["test_util_endpoints"] = _test_util_endpoints
6903
6907
  result["test_util_excluded"] = len(_test_util_endpoints)
6904
6908
  result.setdefault("notes", []).append(
@@ -6906,6 +6910,8 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6906
6910
  f"module(s) under src/main were excluded from the production surface "
6907
6911
  f"(see test_util_endpoints)."
6908
6912
  )
6913
+ elif _test_util_endpoints:
6914
+ result["test_util_included"] = len(_test_util_endpoints)
6909
6915
  # Surface incomplete-endpoint warnings (interface-defined controllers) only when
6910
6916
  # present, to keep output backward-compatible for the common case.
6911
6917
  if endpoint_warnings:
@@ -6964,6 +6970,17 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
6964
6970
  if any(r.get("framework") == "jax_rs" for r in routes):
6965
6971
  _nonspring.pop("jax_rs", None)
6966
6972
 
6973
+ # Servlet mappings are a URL declaration population, not a count of
6974
+ # HttpServlet classes. Reuse the servlet extractor so `endpoints` and
6975
+ # `endpoints --servlets` publish the same authoritative census.
6976
+ try:
6977
+ from sourcecode.servlet_surface import build_servlet_surface
6978
+
6979
+ _nonspring["servlets"] = int(
6980
+ build_servlet_surface(root).get("declarations", 0)
6981
+ )
6982
+ except Exception:
6983
+ pass
6967
6984
  _nonspring_total = sum(_nonspring.values())
6968
6985
  if _nonspring_total:
6969
6986
  _frameworks = [k for k, v in _nonspring.items() if v]
@@ -7264,6 +7281,23 @@ def compute_blast_radius(
7264
7281
  # class-level node traversable at all (C1-16), and rebuilding it per node
7265
7282
  # would make the walk quadratic in the graph.
7266
7283
  _rg_index = _class_method_index(reverse_graph)
7284
+
7285
+ def _walk_callers(_node: str) -> list[str]:
7286
+ """Expand method frontiers through their owning class as well.
7287
+
7288
+ Service-locator accessors enter the frontier as ``Class#accessor`` while
7289
+ callers of the class are indexed under its method keys. Keeping the
7290
+ exact lookup preserves method-level reach; adding the enclosing class
7291
+ lookup prevents that frontier from becoming a false terminal node.
7292
+ """
7293
+ candidates = list(_all_callers_from_rg(
7294
+ _node, reverse_graph, _rg_index, _impl_graph
7295
+ ))
7296
+ if "#" in _node:
7297
+ candidates.extend(_all_callers_from_rg(
7298
+ _enclosing_class(_node), reverse_graph, _rg_index, _impl_graph
7299
+ ))
7300
+ return list(dict.fromkeys(candidates))
7267
7301
  # Callers inject the interface, not the implementation, so a walk that does not
7268
7302
  # know the implements relation stops one hop short of every DI consumer. Without
7269
7303
  # it this traversal reached 200 classes and **no controller at all** for
@@ -7298,14 +7332,14 @@ def compute_blast_radius(
7298
7332
  _expanded.add(_sym)
7299
7333
  matched_fqns = _expanded
7300
7334
  for seed in matched_fqns:
7301
- _seed_callers = _all_callers_from_rg(seed, reverse_graph, _rg_index, _impl_graph)
7335
+ _seed_callers = _walk_callers(seed)
7302
7336
  if len(_seed_callers) > _HUB_CALLER_THRESHOLD and max_depth > 1:
7303
7337
  _effective_depth = 1
7304
7338
  _hub_class_guard = True
7305
7339
  break
7306
7340
 
7307
7341
  for seed in matched_fqns:
7308
- callers = _all_callers_from_rg(seed, reverse_graph, _rg_index, _impl_graph)
7342
+ callers = _walk_callers(seed)
7309
7343
  for c in callers:
7310
7344
  if c not in all_affected:
7311
7345
  all_affected[c] = 1
@@ -7385,16 +7419,22 @@ def compute_blast_radius(
7385
7419
 
7386
7420
  # BFS for indirect callers
7387
7421
  indirect_callers: list[str] = []
7422
+ _indirect_parent: dict[str, str] = {}
7423
+ _hub_frontier_nodes: set[str] = set()
7388
7424
  visited: set[str] = set(matched_fqns) | set(direct_callers)
7389
7425
 
7390
7426
  while queue:
7391
7427
  node, depth = queue.pop(0)
7392
7428
  if depth >= _effective_depth:
7393
7429
  continue
7394
- for caller in _all_callers_from_rg(node, reverse_graph, _rg_index, _impl_graph):
7430
+ _node_callers = _walk_callers(node)
7431
+ if len(_node_callers) > _HUB_CALLER_THRESHOLD:
7432
+ _hub_frontier_nodes.add(node)
7433
+ for caller in _node_callers:
7395
7434
  if caller not in visited:
7396
7435
  visited.add(caller)
7397
7436
  all_affected[caller] = depth + 1
7437
+ _indirect_parent[caller] = node
7398
7438
  indirect_callers.append(caller)
7399
7439
  queue.append((caller, depth + 1))
7400
7440
 
@@ -7414,7 +7454,7 @@ def compute_blast_radius(
7414
7454
  _snode, _sdepth = _sample_queue.pop(0)
7415
7455
  if _sdepth >= _HUB_SAMPLE_DEPTH:
7416
7456
  continue
7417
- for _scaller in _all_callers_from_rg(_snode, reverse_graph, _rg_index, _impl_graph):
7457
+ for _scaller in _walk_callers(_snode):
7418
7458
  if _scaller not in _sample_visited:
7419
7459
  _sample_visited.add(_scaller)
7420
7460
  all_affected[_scaller] = _sdepth + 1
@@ -7615,10 +7655,38 @@ def compute_blast_radius(
7615
7655
  _direct_caller_classes = sorted(
7616
7656
  caller_classes(direct_callers, exclude=matched_fqns)
7617
7657
  )
7618
- n_direct_symbols = len(direct_callers)
7658
+ _direct_class_set = set(_direct_caller_classes)
7659
+ _direct_caller_symbols = sorted(
7660
+ symbol for symbol in direct_callers
7661
+ if _enclosing_class(symbol) in _direct_class_set
7662
+ )
7663
+ n_direct_symbols = len(_direct_caller_symbols)
7619
7664
  n_direct_classes = len(_direct_caller_classes)
7620
7665
  n_direct = n_direct_symbols
7621
7666
  n_indirect = len(indirect_callers)
7667
+
7668
+ # The complete reverse walk is useful as a measured upper reach, but a
7669
+ # service-locator hub can make it collapse to "the whole repository". Publish
7670
+ # a second, conservative axis that stops at hub frontiers instead of forcing
7671
+ # consumers to guess from a flat list of symbols.
7672
+ _without_hubs: list[str] = []
7673
+ _without_hubs_seen = set(matched_fqns) | set(direct_callers)
7674
+ _without_hubs_queue: list[tuple[str, int]] = [
7675
+ (c, 1) for c in direct_callers
7676
+ ]
7677
+ while _without_hubs_queue:
7678
+ _node, _depth = _without_hubs_queue.pop(0)
7679
+ if _depth >= _effective_depth:
7680
+ continue
7681
+ _next = _walk_callers(_node)
7682
+ if len(_next) > _HUB_CALLER_THRESHOLD:
7683
+ continue
7684
+ for _caller in _next:
7685
+ if _caller not in _without_hubs_seen:
7686
+ _without_hubs_seen.add(_caller)
7687
+ _without_hubs.append(_caller)
7688
+ _without_hubs_queue.append((_caller, _depth + 1))
7689
+
7622
7690
  n_ep = len(endpoints_affected)
7623
7691
  n_txn = len(txn_nodes)
7624
7692
  n_mappers = len(mappers_affected)
@@ -7785,6 +7853,20 @@ def compute_blast_radius(
7785
7853
  _indirect_summary = (
7786
7854
  _indirect_ordered if _indirect_cap is None else _indirect_ordered[:_indirect_cap]
7787
7855
  )
7856
+ def _via_hub(_node: str) -> bool:
7857
+ _seen_path: set[str] = set()
7858
+ _cursor = _node
7859
+ while _cursor in _indirect_parent and _cursor not in _seen_path:
7860
+ _seen_path.add(_cursor)
7861
+ _cursor = _indirect_parent[_cursor]
7862
+ if _cursor in _hub_frontier_nodes:
7863
+ return True
7864
+ return False
7865
+
7866
+ _indirect_details = [
7867
+ {"fqn": _node, "depth": all_affected.get(_node), "via_hub": _via_hub(_node)}
7868
+ for _node in _indirect_summary
7869
+ ]
7788
7870
 
7789
7871
  out: dict = {
7790
7872
  "target": target,
@@ -7827,10 +7909,11 @@ def compute_blast_radius(
7827
7909
  else _direct_caller_classes[:_direct_cap]
7828
7910
  ),
7829
7911
  "direct_caller_symbols": (
7830
- sorted(direct_callers) if _direct_cap is None
7831
- else sorted(direct_callers)[:_direct_cap]
7912
+ _direct_caller_symbols if _direct_cap is None
7913
+ else _direct_caller_symbols[:_direct_cap]
7832
7914
  ),
7833
7915
  "indirect_callers": _indirect_summary,
7916
+ "indirect_caller_details": _indirect_details,
7834
7917
  "container_wired": [w.to_dict() for w in _container_wired],
7835
7918
  "endpoints_affected": endpoints_affected,
7836
7919
  "mappers_affected": mappers_affected,
@@ -7850,6 +7933,10 @@ def compute_blast_radius(
7850
7933
  "indirect_caller_count": n_indirect,
7851
7934
  "indirect_callers_computed": not _bfs_truncated or _indirect_sampled,
7852
7935
  "indirect_callers_sampled": _indirect_sampled,
7936
+ "indirect_caller_count_unit": "reference symbols (methods or classes)",
7937
+ "indirect_callers_without_hubs_count": len(_without_hubs),
7938
+ "indirect_callers_without_hubs_unit": "reference symbols (hub frontiers excluded)",
7939
+ "hub_frontier_count": len(_hub_frontier_nodes),
7853
7940
  "endpoints_affected_count": n_ep,
7854
7941
  # C1-37: four figures called "endpoints" at one commit (3 364 /
7855
7942
  # 3 382 / 3 382 / 3 574). Each one now says which population it
sourcecode/risk.py CHANGED
@@ -47,6 +47,31 @@ if TYPE_CHECKING:
47
47
 
48
48
  RISK_SCHEMA = "risk-v1"
49
49
 
50
+
51
+ def compact_risk_output(data: dict) -> dict:
52
+ """Project stdout to stable rule prose plus small per-defect evidence."""
53
+ compact = dict(data)
54
+ rows = data.get("risks") or []
55
+ rules: dict[str, dict] = {}
56
+ lean: list[dict] = []
57
+ for row in rows:
58
+ rule_ids = row.get("rule_ids") or [row.get("rule_id", "unknown")]
59
+ for rule_id in rule_ids:
60
+ rules.setdefault(rule_id, {
61
+ "title": row.get("title", ""),
62
+ "statement": row.get("statement", ""),
63
+ })
64
+ lean.append({
65
+ key: row[key] for key in (
66
+ "defect_id", "symbol", "source_file", "first_line", "band",
67
+ "score", "rule_ids", "factors",
68
+ ) if key in row
69
+ })
70
+ compact["rules"] = rules
71
+ compact["risks"] = lean
72
+ compact["default_output"] = "bounded"
73
+ return compact
74
+
50
75
  #: The composition, in one place. The payload publishes it, `--help` renders it and
51
76
  #: the battery holds both to it: a hand-copied model line in the help screen went on
52
77
  #: advertising four factors for as long as it took someone to read it, which is the
@@ -168,9 +168,16 @@ def build_servlet_surface(root: Path, *, limit: Optional[int] = None) -> dict:
168
168
  elif path.suffix == ".java":
169
169
  mappings.extend(_from_registration_beans(path, relative))
170
170
 
171
- # One URL pattern declared twice is two declarations of one reachable path; the
172
- # census counts patterns, the rows keep every declaration.
173
- patterns = sorted({m.url_pattern for m in mappings})
171
+ # A URL pattern is the observable HTTP surface. Multiple descriptors can
172
+ # repeat it, but publishing every duplicate made `declarations` disagree with
173
+ # the pattern census and with endpoint-side reconciliation. Keep the first
174
+ # deterministic declaration as provenance and expose one row per reachable
175
+ # pattern.
176
+ _unique: dict[str, ServletMapping] = {}
177
+ for mapping in sorted(mappings, key=lambda m: (m.url_pattern, m.source_file, m.source)):
178
+ _unique.setdefault(mapping.url_pattern, mapping)
179
+ mappings = list(_unique.values())
180
+ patterns = sorted(_unique)
174
181
  by_source: "dict[str, int]" = {}
175
182
  for mapping in mappings:
176
183
  by_source[mapping.source] = by_source.get(mapping.source, 0) + 1
@@ -484,18 +484,15 @@ def _programmatic_validation_signals(graph: "ContextGraph") -> "list[str]":
484
484
  if _is_excluded(t.source_file):
485
485
  continue
486
486
  sig = (t.signature or "")
487
- if "org.springframework.validation.Validator" not in sig:
487
+ if (
488
+ "org.springframework.validation.Validator" not in sig
489
+ and not re.search(r"\bimplements\s+Validator\b", sig)
490
+ ):
488
491
  continue
489
492
  if "ConstraintValidator" in sig:
490
493
  continue
491
494
  evidence.append(f"{t.fqn} implements org.springframework.validation.Validator")
492
495
 
493
- for m in graph.symbols(kind="method"):
494
- if _is_excluded(m.source_file):
495
- continue
496
- if m.has_annotation("InitBinder"):
497
- evidence.append(f"{m.fqn} is annotated with @InitBinder")
498
-
499
496
  if evidence:
500
497
  return sorted(dict.fromkeys(evidence))
501
498
 
@@ -627,6 +624,7 @@ def build_validation_surface(
627
624
  "detected": True,
628
625
  "evidence": programmatic_evidence,
629
626
  }
627
+ result["programmatic_validation_gaps"] = []
630
628
  spec_path = endpoints_data.get("openapi_spec")
631
629
  if spec_path:
632
630
  result["openapi_spec"] = spec_path
@@ -717,7 +715,7 @@ def build_validation_surface(
717
715
  "outside the declared-constraint surface this command models."
718
716
  ).strip()
719
717
  if not result.get("gaps"):
720
- result["gaps"] = [
718
+ result["programmatic_validation_gaps"] = [
721
719
  {
722
720
  "method": "",
723
721
  "path": "",
@@ -727,7 +725,6 @@ def build_validation_surface(
727
725
  "confidence": "Likely",
728
726
  }
729
727
  ]
730
- result["summary"]["gaps"] = 1
731
728
 
732
729
  _reconcile_with_code_axis(result, graph, root)
733
730
  if programmatic_evidence:
@@ -740,7 +737,7 @@ def build_validation_surface(
740
737
  if programmatic_note.strip() not in note:
741
738
  result["note"] = (note + programmatic_note).strip()
742
739
  if not result.get("gaps"):
743
- result["gaps"] = [
740
+ result["programmatic_validation_gaps"] = [
744
741
  {
745
742
  "method": "",
746
743
  "path": "",
@@ -750,7 +747,6 @@ def build_validation_surface(
750
747
  "confidence": "Likely",
751
748
  }
752
749
  ]
753
- result["summary"]["gaps"] = 1
754
750
  return result
755
751
 
756
752
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.5.0
3
+ Version: 5.5.2
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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-5.5.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-5.5.2-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -126,7 +126,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
126
126
  # pip / pipx
127
127
  pipx install sourcecode # or: pip install sourcecode
128
128
 
129
- ask version # ask 5.5.0 — and, on a build that has aged,
129
+ ask version # ask 5.5.2 — and, on a build that has aged,
130
130
  # how many releases have probably shipped since
131
131
  ```
132
132
 
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=kIpmlxwOoXC5aa6kJcz5s9dCm4R4SN2RiXVFk3uxLJQ,308
1
+ sourcecode/__init__.py,sha256=N8caEJv8V-c5M8fb_R3zr9u-Fd7axm-yB4yw-d_rbf0,308
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/archetype.py,sha256=CZvRLpkHot_D8D3JFQVorr-EHDJyh0BbS7RnSTqBigM,40499
4
4
  sourcecode/architectural_baseline.py,sha256=4GiMVBLJVHvRKSWQGf2ZVurI1-R0qk5pmaevgvGSgVA,26325
@@ -19,7 +19,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
19
19
  sourcecode/change_plan.py,sha256=aX1mp2XnlDN-8R2BcTiu8HlkDwQ_4fN1ZLckj0VMXHM,9945
20
20
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
21
21
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
22
- sourcecode/cli.py,sha256=vV53BEtmpl97Kz2a00Sk88_SKgdbd17B5mA52tMLcas,645928
22
+ sourcecode/cli.py,sha256=hJukwL8ZTc1ZHVkkZWau9J0LFYtAfyHvVdp4HKIRWIY,648972
23
23
  sourcecode/client_calls.py,sha256=5_sBfFVXi6RgGiyWrlJKH4x4EF21do6gKlAOuFCKAoA,17709
24
24
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
25
25
  sourcecode/compare.py,sha256=2GdDy0qkDSgmkkpgvoCHitoLj3mt30EeFgJ8PGEXJng,14712
@@ -41,7 +41,7 @@ sourcecode/data_labels.py,sha256=c_hmgudTE68cVRdywfbfonr7nUWIL-JH2HOXB8Pplv0,845
41
41
  sourcecode/declarations.py,sha256=Jr7oJDg004sFw0TvJySNVggXFT2jB6hQgbeQK2qreVo,7000
42
42
  sourcecode/defect_identity.py,sha256=XS2VFDImg9NLaRImybKTeyk-CoDSsXmchldg0dwvM7c,7028
43
43
  sourcecode/degradation.py,sha256=BtuGUKFgkzI3Z6fwmUdxEUEaLFAcPQ_bHhU2w9ItDxw,18376
44
- sourcecode/dependency_analyzer.py,sha256=fQCaWQ7_BNcVgZv8gweJCwJ1CGlXsz8nw_tqN3aHJNE,63952
44
+ sourcecode/dependency_analyzer.py,sha256=MRv6eh1TG5E9Y5Db-bNdZHDIJ3gWvvL3bBU1-9_QSZI,64593
45
45
  sourcecode/deployment_prefix.py,sha256=g0_4bE_m9svjhnapRpOp8BJkj6Ul94Ij_iVezygMZ0I,11956
46
46
  sourcecode/detach.py,sha256=PvkCcbIuTaDKKt5TGqtQLahmsmCkflui33jLnMEWkvg,8006
47
47
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
@@ -71,9 +71,10 @@ sourcecode/identity_fallback.py,sha256=uRC_j_80uaI4Ue4tf-OhKX4h472dmufYcjEFGSxY2
71
71
  sourcecode/integration_coordinates.py,sha256=7vGFxN4tCn_KTisYFJuixa8XkyHBoasEFB3fqM62OBI,7407
72
72
  sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
73
73
  sourcecode/license.py,sha256=vvgE1vBUWh8ozWI7USYuteh6noxntnjuzDQIPF-l4rA,27795
74
+ sourcecode/manifest_cache.py,sha256=NkVtg0T5YfHdIiM63jStvcLcsznM6_bnTHib4t8_FW8,2158
74
75
  sourcecode/mcp_nudge.py,sha256=lKemOqK_wny2u7Ymcr2Idi5Kx8pXY02jCi-_nJYLGMg,2992
75
76
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
76
- sourcecode/migrate_check.py,sha256=8KKgsiiWu21XwCXgFV1bB8AVsB-pl6KNBTmMW6NYGB8,149028
77
+ sourcecode/migrate_check.py,sha256=vdJ_2hOR9XOIQsz4czzamQMKKzHnNErg8vPdzzYqfMY,150019
77
78
  sourcecode/migration_blast.py,sha256=OPryWkM6-PbvcG3eJFdcWUnlpXBje41nu2BkrZnE2hg,9359
78
79
  sourcecode/non_coverage.py,sha256=sKp0J4_hKzKb_HMoXtdP91D0lMfEAPrOx44sqdQc18Y,23520
79
80
  sourcecode/openapi_surface.py,sha256=BTt0K-woZbkbWTN77IkqeBm_Okag9owR0848fmot8sk,16207
@@ -99,15 +100,15 @@ sourcecode/readiness_timeline.py,sha256=KjghC1MWH7Tzto97TBR3N0F8FkR87w5z7lZ1QsWv
99
100
  sourcecode/readonly.py,sha256=dqGiNVvpfi4Ww8VzwUs6E3SdbqcHL1LFHItIK7IjrM0,5830
100
101
  sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
101
102
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
102
- sourcecode/reference_facts.py,sha256=3r8EnbKewOAls3FOggDWnTr84eogJoQ8fVVdvyXSpfE,14801
103
- sourcecode/release_info.py,sha256=IQdMsFrQM4IVHNmwXWS7TbtRu80_oWugvNrYnpQ0bao,5895
103
+ sourcecode/reference_facts.py,sha256=mx7H-Ax5y8sKAQHrnJCXlgjkUzqZtQTByv9Zy2lxKQQ,15500
104
+ sourcecode/release_info.py,sha256=lCzjRkPWDmOZQxPujpY5FwPU8a87G358ectkhNU8cX8,5895
104
105
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
105
106
  sourcecode/remedies.py,sha256=V2_cD4pLq8bKRkzF-3RGFoiu1xWYZS384WDVp1MrlZw,10225
106
107
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
107
108
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
108
- sourcecode/repository_ir.py,sha256=AAX2OCLNIBcTicuHH-WydIJStBVPjbdkBbYCwOhFYWs,380428
109
+ sourcecode/repository_ir.py,sha256=tV0Uu7qWw_gDUG9WldYxmbNLZnbQljxHYZznWP4cdlA,384071
109
110
  sourcecode/ris.py,sha256=Xbx34dkJI_uXil9xsgw-fA_Apcm4nbnTjQgFm66dP1g,27489
110
- sourcecode/risk.py,sha256=432XEiL9xp8JBaxidwQInZBN4F98QJLAsqgZrh3CKow,81790
111
+ sourcecode/risk.py,sha256=u2V7nG5yGW8VV4sFDSgFkZqdI6ltmilWhlQwlIH7d2o,82660
111
112
  sourcecode/rule_catalog.py,sha256=pTkgkQZ1u7atB3fkQJ9BPWo8lNiamvEuRN7MtRgle0c,5263
112
113
  sourcecode/rule_pass.py,sha256=DTNxD-nAecMJhIzI1LCd9FyRumInQSZjitacNh4Zoc8,14671
113
114
  sourcecode/runs.py,sha256=o4oXkB3WFJGScbK_C10OAWzsE18B9Zc55u_IE8_yl4M,6291
@@ -124,7 +125,7 @@ sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XE
124
125
  sourcecode/semantic_integration_engine.py,sha256=7a0WqAInOv39f0Yr_94TYo_JP_8QpeI9KGaknpzAyQU,18899
125
126
  sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
126
127
  sourcecode/serializer.py,sha256=cK4I3WzX0H-oBPbqzBsUBRux5GcYmH_yPDQwxvkf8yQ,141598
127
- sourcecode/servlet_surface.py,sha256=aOeRne07man8DwHsbog4FTOyF3sYV-oP_N1e4gt3Uvk,9115
128
+ sourcecode/servlet_surface.py,sha256=QY3QPw6pZJ4CQK4yS8q7uDd3f2PDyUUsQ8TS_BkbGs8,9506
128
129
  sourcecode/source_text.py,sha256=R52gmSljz7L6YVPbixv5W-LOZ5eKUVvSUP3S-QH_IWc,9047
129
130
  sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
130
131
  sourcecode/spring_findings.py,sha256=cW6NiQAFUM9mDYPitJr6MrQg--YBI41LxrSEMI9aeIQ,25272
@@ -146,7 +147,7 @@ sourcecode/token_estimate.py,sha256=G-ivzIJfVq5TLQN04dC0WK4LZJjWVYL54Ebcxe4fJ7M,
146
147
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
147
148
  sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
148
149
  sourcecode/validation_inference.py,sha256=-oWJqE6PqkqcZbCFDJeWCQHgI9Dbuv5ggJWD3LE_2IU,20817
149
- sourcecode/validation_surface.py,sha256=2wQAEfzWKAymsptGLaIlN-xw2C7ozE4zO-WR8a7qr20,34857
150
+ sourcecode/validation_surface.py,sha256=06muCMxymQEQSqmNgTMdsguC7XQ22tqcZmYJOmvWFHY,34745
150
151
  sourcecode/verify_edit.py,sha256=SUaJeH5Icc54mutQx3-gh7i8U6d50cb_oxYc9SDUnHs,42757
151
152
  sourcecode/verify_repo.py,sha256=aHlNIUfoVVUxTXTNtSFryP4dOFBOvCfdyC31fFRXU6s,15346
152
153
  sourcecode/verify_rules.py,sha256=R3l6nLT6WQzUNBzxtmnxyGZcKj9qMDW38Y0c0gNdCGk,19973
@@ -213,9 +214,9 @@ sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB
213
214
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
214
215
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
215
216
  sourcecode/_docs/DEFECT-LEDGER.md,sha256=sTFPEV2xx-L9n4mrG8UO4LxeETMJpGKHXyKQBZlqNog,497112
216
- sourcecode/_docs/USER_GUIDE.md,sha256=yRFIp4sx38X4KSxIf1RjwCiWsvKSmJ-IoPkzHOOsMI4,79546
217
- sourcecode-5.5.0.dist-info/METADATA,sha256=jg3OolyQqoitfNoZCkPqc8nQm20P8TZogBj2pyC8ljA,47104
218
- sourcecode-5.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
219
- sourcecode-5.5.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
220
- sourcecode-5.5.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
221
- sourcecode-5.5.0.dist-info/RECORD,,
217
+ sourcecode/_docs/USER_GUIDE.md,sha256=IOYL9rI9tLViNMICXma_uRSiztjMaW92V5sDAYlPkfo,79546
218
+ sourcecode-5.5.2.dist-info/METADATA,sha256=VjdCWsZA1Bm_mhWsXA-yr6JT1zTSKcLVJA8lejltoug,47104
219
+ sourcecode-5.5.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
220
+ sourcecode-5.5.2.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
221
+ sourcecode-5.5.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
222
+ sourcecode-5.5.2.dist-info/RECORD,,