sourcecode 5.5.2__py3-none-any.whl → 5.5.5__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.2"
7
+ __version__ = "5.5.5"
@@ -16,6 +16,21 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
16
16
 
17
17
  ---
18
18
 
19
+ ## Audit synchronization
20
+
21
+ | ID | Defect | Found | Severity | Status |
22
+ |---|---|---|---|---|
23
+ | AS-01 | `risk`/`enrich` could omit `sql_taint.bounded_interpolations`, making `sinks_total` impossible to audit against its bounded evidence. | 5.4.2 re-audit | Medium | **closed 5.4.2** — both projections preserve the bounded rows; the paired fixture asserts the sink total and the retained bounded interpolation. |
24
+ | AS-02 | Route projection emitted declarations from abstract Spring controllers, creating phantom routes and inflating the unprotected census. | 5.4.2 re-audit | Low | **closed 5.4.2** — abstract declarers are excluded while concrete subtype projection remains admitted; the route-surface regression covers both sides. |
25
+ | AS-03 | The distributed ledger could leave `C3-123` open after the 5.4.1 build already excluded static assets. | 5.4.2 re-audit | Informational | **closed 5.4.2** — the stale state was synchronized to the wheel it documents. |
26
+ | AS-04 | An unconstrained `@RequestMapping` was described by consumers as GET although Spring admits every verb. | 5.5.1 audit | Medium | **closed 5.5.1** — primary `method` is `ANY`; 5.5.4 adds the concrete `methods` list so consumers do not need to infer its expansion. |
27
+ | AS-05 | Test-source endpoint exclusion was neither visible nor reversible for modules whose test code lives below `src/main`. | 5.5.1 audit | High | **closed 5.5.3** — `--include-tests` changes admission and the production answer counts exclusions; 5.5.4 recovery adds the matched path basis. |
28
+ | AS-06 | `@InitBinder` alone was treated as evidence of programmatic validation, contaminating gaps and confidence. | 5.5.1 audit | High | **closed 5.5.1** — only a Spring `Validator` implementation or `setValidator` wiring declares the limit; 5.5.4 recovery publishes that population separately from annotation validators. |
29
+ | AS-07 | Nested types used as field types were reported statically unreferenced by modernization. | 5.5.1 audit | Medium | **closed 5.5.1** — field-type references contribute an incoming static edge. |
30
+ | AS-08 | `spring-audit` severity census could sum to zero beside nonzero findings. | 5.5.4 re-audit | High | **closed 5.5.4** — severity partitions reconcile to `total_findings`; this is asserted in the audit battery. |
31
+
32
+ ---
33
+
19
34
  ## Class C1 — One fact, two authorities
20
35
 
21
36
  | ID | Defect | Found | Severity | Status |
@@ -35,7 +50,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
35
50
  | C1-13 | **Hub-guard publishes `endpoints_total: 0` instead of `unknown`.** A service with 780 callers is capped at `depth=1` for cost — defensible — but reports 0 endpoints reached, contradicting the epistemic standard the tool imposes on itself everywhere else. Hubs are precisely where blast radius is asked for | 3.2.1 (eval #4) | Medium | **closed 3.6.0** — no battery repository crosses the guard (openmrs-core's largest hub, `Context`, has 215 caller classes against a threshold of 500), so it was reproduced on a generated fixture: `bfs_truncated: true`, `depth_reached: 1`, `endpoints_affected_count: 0` beside four more zeros nothing looked for. A reach-derived count of 0 under the cap is now `null`, named in `stats.unknown_under_cap`. The distinction is the point and is asserted both ways: a **non-zero** count under the cap stays a number (it is a floor the effect block already describes), and an **uncapped** run keeps its measured zero, because converting that would be the mirror-image defect. The hub threshold moved to module level so the fixture crosses the real guard instead of a copy of its value |
36
51
  | C1-14 | **File-count drift across commands**: 3 334 / 3 337 / 3 342 / 3 374 files for one repository in one session. No command is wrong in isolation; together they cost the one thing a deterministic tool cannot lose — that its numbers agree with themselves | 3.2.1 (eval #4) | Medium | **within-document half closed 3.6.0 · cross-command half does not reproduce.** Of six commands measured on openmrs-core (`--compact`, `endpoints`, `spring-audit`, `migrate-check`, `modernize`, `validation`) only `migrate-check` publishes a scanned-file count at all, so there are no two figures left to disagree. What does reproduce is the same defect *inside one document*: `affected_files` named two populations — 265 under `summary` (every finding, any scope) against 264 under `effort_breakdown` (blocking findings only). Both correct; the shared name was the contradiction. Same remedy as C1-5: the narrower becomes `blocking_affected_files`, both carry a `_basis`, the old key stays an alias, and the battery asserts the two diverge exactly when a finding is non-blocking |
37
52
  | C1-15 | **Analysis assumes a single destination.** Asked about a JDK move, `migrate-check` answers `headline_blocker: boot3_migration` and emits fix hints that send a developer to migrate namespaces they never planned to touch (481 `javax.persistence` under MIG-001). There is no way to say "JDK 11, keep Boot 2.7" | 3.2.1 (eval #4) | **High (product)** | **closed 3.6.0** — `--target-jdk N` and `--keep-boot` say which migration is being planned, filtered over the `migration_target` vocabulary every rule already carried rather than a parallel one. Measured on openmrs-core with `--target-jdk 11 --keep-boot`, the row's own acceptance: **MIG-001 85 → 0**, blocking 114 → 0, `headline_blocker` `hibernate_rewrite` → `none_applicable`, and the Hibernate 5→6 rewrite drops with it because Hibernate 6 arrives with Boot 3. Narrowing is not silencing: `metadata.route` publishes the target, the flag and every excluded axis, a `limitations` entry states how many findings were left out and that they are unresolved rather than fixed, and the route's own blockers stay (a JDK 11 route still reports MIG-021, asserted). The default is untouched, verified identical on three repositories |
38
- | C1-16 | **Four fan-in figures for one class, and the note that promises the relationship is itself false.** Field: `modernize.in_degree` 1 026 / `impact-chain.direct_callers` 611 / `explain.incoming_callers` 519 / `impact.stats.direct_caller_count` 484 for one service, plus 3 222 vs 3 458 endpoints affected. **Re-measured on the battery, 3.4.0, `Money` @ BroadleafCommerce: 673 / 1 868 / 151 / 418** — `impact-chain` is absent from the published relationship and is **2,8× larger than the figure the note declares "always the largest of the three"**, while `explain` vs `impact` differ by **177 %**, not the *"small margin"* the same note promises. In the same runs `impact.stats.endpoints_affected_count: 0` against a populated `endpoints_affected` from `impact-chain` — a **false zero** on the blast-radius axis | 3.2.1 (eval #5) | **High** — this is the number a risk decision is taken with: *"¿el blast radius son 484 o 611 clases?"* | **closed 4.10.0** — the caller-axis unit fix closed in 3.5.0 (`caller_metrics.caller_classes`, unit-named figures, generated note, no `len(display_list)` in `compare`). The endpoint reach half then bound `impact`, `impact-chain` and `migrate-check --blast-radius` to `caller_reach`, crossing class→method keys and DI interface boundaries without admitting structural `implements`/`extends` edges as callers. The remaining asymmetry is now closed too: `impact` expands class/interface seeds to in-repo subtypes before BFS, using `ImplementationGraph` and a class-aware seed resolver so a base-class query reaches callers of subclass members the same way `impact-chain` does. Tests cover DI parity, endpoint count/list parity, caller-unit stability and the base-class subtype regression fixture |
53
+ | C1-16 | **Four fan-in figures for one class, and the note that promises the relationship is itself false.** Field: `modernize.in_degree` 1 026 / `impact-chain.direct_callers` 611 / `explain.incoming_callers` 519 / `impact.stats.direct_caller_count` 484 for one service, plus 3 222 vs 3 458 endpoints affected. **Re-measured on the battery, 3.4.0, `Money` @ BroadleafCommerce: 673 / 1 868 / 151 / 418** — `impact-chain` is absent from the published relationship and is **2,8× larger than the figure the note declares "always the largest of the three"**, while `explain` vs `impact` differ by **177 %**, not the *"small margin"* the same note promises. In the same runs `impact.stats.endpoints_affected_count: 0` against a populated `endpoints_affected` from `impact-chain` — a **false zero** on the blast-radius axis | 3.2.1 (eval #5), reopened 5.5.4 | **High** — this is the number a risk decision is taken with: *"¿el blast radius son 484 o 611 clases?"* | **reopened 5.5.4.** The DI and subtype fixtures remain closed, but the claimed one-authority implementation was false for service locators: `impact` kept a local BFS that elevated `Locator#accessor` to every caller of `Locator`, while `impact-chain` used `caller_reach` and did not. 5.5.4 recovery removes that elevation and adds a two-accessor regression fixture; authority unification and cross-command parity remain open as Q-03. |
39
54
  | C1-17 | **A fact with an authority still contradicts itself across two commands**: `--compact.analysis_gaps` says *"1 test file for 3 336 Java files"* while `review-pr.test_coverage_risk.repository_has_test_sources` says `false`, in the same repository. C1-2 was closed in 3.2.1 with five consumers rebound, and this evaluation ran **on 3.2.1** | 3.2.1 (eval #5) | Medium | **does not reproduce (measured 3.6.0)** — re-measured as the row instructed. On openmrs-core, keycloak and spring-petclinic neither statement is emitted, and both consumers reach one authority: `--compact` through `analyze_test_sources`, `review-pr` through `is_test_path`. What was missing is now asserted: the authority has **two entry points** and nothing held them to each other, which is exactly the shape a sixth consumer would take. Zero mismatches across a case table including the C1-2 discriminator (a `test` package inside a *main* source root is production code), plus the literal contradiction — "N test files" and "no test sources" — asserted unreachable in both directions |
40
55
  | C1-18 | **The profile population differs per command, and the narrower one is what an agent reads.** Field: `--agent` reports a two-name profile set while `prepare-context fix-bug` reports four for the same repository in the same session — and the two names missing from the agent payload are the ones the *build* names for its deployable artefacts. Every finding about conditional security is keyed on that set, so the default agent surface answers the profile question with the population that lost the environments | 3.5.0 (eval #6) | **High** — it is the input to the one capability nothing else in the market has | **closed (unreleased)** — **fixture first, as C1-10 required**, and it reproduced worse than reported: on `tests/fixtures/spring_profile_population` the same repository in one session answered **`["qa","dev"]`** (default view — filenames + overlay directories), **`["dev","prod","staging"]`** (`posture` — config documents + `@Profile`) and **`["dev","prod","qa","staging"]`** (`--agent` / `fix-bug`, the union). Three populations under one name; every conditional-security finding is keyed on it. Authority `spring_profiles.profile_population`: four named signals (`file_naming`, `overlay_directory`, `config_documents`, `annotations`, plus `caller_supplied`), the union, and `by_signal` published so a name's provenance is auditable. The stack detector, `posture` and the serializer section all read it — asserted per surface, not claimed. Closing it exposed two false positives in the signals it inherited, both fixed structurally (never by name — VAI): an `application-{x}` file counts only **beside its base config** (alfresco's `alfresco/messages/application-model_de.properties` + 19 siblings invented twenty environments; now 0) and an overlay directory counts only when it **holds a config file** (spring-petclinic's `db/ messages/ static/ templates/` reported four; now `mysql, postgres`, which are its real profiles) |
41
56
  | C1-19 | **File-count drift across commands, with witnesses this time.** 3 337 (`compact.project_summary`) / 3 336 (`analysis_gaps.testing`) / 3 342 (`migrate-check.metadata.java_files_scanned`) for one repository in one session | 3.8.0 (eval #7) | Medium | **closed 3.9.0** — measured, not equalised: the three figures count code-extension files in any language, the non-test denominator of a test ratio, and the .java files handed to the migration rules. Each names its population now, and `migrate-check.files_scanned_basis` states the two it is not. Original note: open — **C1-14's cross-command half, reopened.** 3.6.0 recorded that half as *not reproducing* because on openmrs-core only `migrate-check` published a scanned-file count at all; eval #7 shows three commands publishing one on a repository we do not have. Three populations are plausible and each may be right in isolation (all Java files / files admitted to the graph / files the migration rules scanned), which is exactly the C1-5 remedy: name the unit per figure, or bind them to one authority. Do not fix by making the numbers equal — measure which population each counts first |
@@ -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.2
139
+ # ask 5.5.5
140
140
  ```
141
141
 
142
142
  Requires Python 3.9+.
@@ -261,7 +261,13 @@ ask endpoints /path/to/repo
261
261
  ask endpoints . --format yaml
262
262
  ```
263
263
 
264
- Output: list of `{method, path, controller, handler}`. Covers `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`, `@RequestMapping`, `@GET`, `@POST`, `@PUT`, `@DELETE`, `@PATCH` + `@Path`.
264
+ Output: list of `{method, methods, path, controller, handler}`. `method` remains
265
+ the backwards-compatible primary verb; `methods` is the complete declared set.
266
+ An unconstrained `@RequestMapping` reports `method: "ANY"` and
267
+ `methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]`, because it accepts all
268
+ of those verbs rather than defaulting to GET. Covers `@GetMapping`, `@PostMapping`,
269
+ `@PutMapping`, `@DeleteMapping`, `@PatchMapping`, `@RequestMapping`, `@GET`,
270
+ `@POST`, `@PUT`, `@DELETE`, `@PATCH` + `@Path`.
265
271
 
266
272
  Each endpoint carries a `security` policy. Standard annotations (`@PreAuthorize`, `@Secured`, `@RolesAllowed`, …) are recognized automatically; a custom authorization annotation reports `policy: "none_detected"` until declared in a `sourcecode.config.json` at the repo root:
267
273
 
sourcecode/cli.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import contextlib
4
+ import errno
4
5
  import hashlib
5
6
  import json
6
7
  import difflib
@@ -1196,6 +1197,15 @@ def _safe_write_file(path: "Path", content: str) -> None:
1196
1197
  fh.flush()
1197
1198
  os.fsync(fh.fileno())
1198
1199
  tmp.replace(path)
1200
+ # Atomic replacement alone does not prove that the destination is the
1201
+ # requested artifact. Verify the postcondition before reporting a run
1202
+ # output path, especially for non-empty JSON reports.
1203
+ if not path.is_file() or (content and path.stat().st_size == 0):
1204
+ raise OSError(
1205
+ errno.EIO,
1206
+ "atomic output replacement did not produce the requested file",
1207
+ str(path),
1208
+ )
1199
1209
  try:
1200
1210
  dir_fd = os.open(str(path.parent), os.O_RDONLY)
1201
1211
  try:
@@ -2071,6 +2081,14 @@ def _resolve_files_option(
2071
2081
 
2072
2082
  raw = str(files).replace(_text_input.BOM_CHAR, "").strip()
2073
2083
  if raw == "-":
2084
+ if sys.stdin.isatty():
2085
+ _emit_error_json(
2086
+ INVALID_INPUT_CODE,
2087
+ "--files - reads the file list from stdin, but stdin is a terminal.",
2088
+ hint=f"Pipe the list: git diff --name-only main | ask {command} . --files -",
2089
+ expected="Piped stdin, a list file, or a comma-separated file list.",
2090
+ )
2091
+ raise typer.Exit(code=1)
2074
2092
  return _text_input.parse_path_lines(_text_input.read_stdin_text()), "stdin"
2075
2093
  as_path = Path(raw)
2076
2094
  if as_path.is_file():
@@ -8857,7 +8875,8 @@ def spring_audit_cmd(
8857
8875
  help=(
8858
8876
  "Bounded summary: caps `findings` and the per-endpoint security projection "
8859
8877
  "to their top 5, most-severe first. Every count is still measured over the "
8860
- "whole population. The full report can exceed 1 MB on a monolith."
8878
+ "whole population. Stdout also switches to this form only when the full "
8879
+ "JSON exceeds its 50 KB budget; use --output for the complete report."
8861
8880
  ),
8862
8881
  ),
8863
8882
  table: bool = typer.Option(
@@ -9194,17 +9213,19 @@ def spring_audit_cmd(
9194
9213
  "phase that never ran is not evidence there is none."
9195
9214
  )
9196
9215
 
9216
+ _full_data = combined.to_dict()
9197
9217
  _bounded_default = (
9198
9218
  not compact and output_path is None and format == "json" and not table
9219
+ and len(_json.dumps(_full_data, ensure_ascii=False).encode("utf-8")) > 50_000
9199
9220
  )
9200
- data = (
9201
- combined.to_compact_dict()
9202
- if compact or _bounded_default
9203
- else combined.to_dict()
9204
- )
9221
+ data = combined.to_compact_dict() if compact or _bounded_default else _full_data
9205
9222
  if _bounded_default:
9206
9223
  data.pop("mode", None)
9207
9224
  data["default_output"] = "bounded"
9225
+ data.pop("compact_note", None)
9226
+ data["stdout_budget_note"] = (
9227
+ "Capped by the stdout payload budget; use --output <file> for the complete report."
9228
+ )
9208
9229
  data.setdefault("metadata", {})["cache_layers"] = _cache_state
9209
9230
  if _stopped_early:
9210
9231
  # C3-87: the block is composed by `PhasedRun.status`, which is the
@@ -9239,7 +9260,19 @@ def spring_audit_cmd(
9239
9260
  elif format == "github-comment":
9240
9261
  output = _render_spring_audit_github_comment(combined, min_severity)
9241
9262
  else:
9242
- data = combined.to_compact_dict() if compact else combined.to_dict()
9263
+ _full_data = combined.to_dict()
9264
+ _bounded_default = (
9265
+ not compact and output_path is None and format == "json" and not table
9266
+ and len(_json.dumps(_full_data, ensure_ascii=False).encode("utf-8")) > 50_000
9267
+ )
9268
+ data = combined.to_compact_dict() if compact or _bounded_default else _full_data
9269
+ if _bounded_default:
9270
+ data.pop("mode", None)
9271
+ data["default_output"] = "bounded"
9272
+ data.pop("compact_note", None)
9273
+ data["stdout_budget_note"] = (
9274
+ "Capped by the stdout payload budget; use --output <file> for the complete report."
9275
+ )
9243
9276
  output = _serialize_dict(data, format)
9244
9277
 
9245
9278
  # F-AV: our own readers ask for the count under either spelling — the break
@@ -12850,7 +12883,8 @@ def config_cmd(
12850
12883
  _answer.say(f"Declaration: none at {_declaration_path}")
12851
12884
  _answer.say(
12852
12885
  " Nothing is declared, so `data-exposure` answers "
12853
- "`answered: false` and no custom security annotation is recognised."
12886
+ "`answered: false`. Custom security annotations may still be inferred "
12887
+ "from usage with Likely confidence; declare them to make that policy Declared."
12854
12888
  )
12855
12889
  else:
12856
12890
  _answer.say(f"Declaration: {_declaration_path}")
@@ -23,7 +23,7 @@ from __future__ import annotations
23
23
  import re
24
24
  from dataclasses import dataclass, field
25
25
  from pathlib import Path
26
- from typing import Any, Optional
26
+ from typing import Any, Iterable, Optional
27
27
 
28
28
  # Published Spring Boot property keys (DATA — never a predicate on a repo name).
29
29
  _CONTEXT_PATH_KEYS: tuple[str, ...] = (
@@ -163,25 +163,31 @@ def _profile_of_document(flat: "dict[str, str]", file_profile: Optional[str]) ->
163
163
  return file_profile
164
164
 
165
165
 
166
- def _config_files(root: Path) -> "list[Path]":
166
+ def _config_files(root: Path, files: Optional[Iterable[Path]] = None) -> "list[Path]":
167
167
  """Spring config files under conventional resource directories.
168
168
 
169
169
  Bounded and sorted for determinism; test resources are excluded because a
170
170
  prefix declared for tests is not the deployed one.
171
171
  """
172
+ candidates = files if files is not None else root.rglob("application*.*")
172
173
  found: list[Path] = []
173
- for pattern in ("**/src/main/resources/**/application*.*", "**/config/application*.*"):
174
- for path in root.glob(pattern):
175
- name = path.name
176
- if name in _CONFIG_BASENAMES or _PROFILE_FILE_RE.match(name):
177
- text = str(path).replace("\\", "/")
178
- if "/src/test/" in text or "/target/" in text:
179
- continue
180
- found.append(path)
174
+ for path in candidates:
175
+ path = Path(path)
176
+ name = path.name
177
+ if name not in _CONFIG_BASENAMES and not _PROFILE_FILE_RE.match(name):
178
+ continue
179
+ text = str(path).replace("\\", "/")
180
+ in_resources = "/src/main/resources/" in text
181
+ in_config = "/config/" in text
182
+ if not (in_resources or in_config) or "/src/test/" in text or "/target/" in text:
183
+ continue
184
+ found.append(path)
181
185
  return sorted(set(found))[:60]
182
186
 
183
187
 
184
- def detect_deployment_prefix(root: Path) -> DeploymentPrefix:
188
+ def detect_deployment_prefix(
189
+ root: Path, *, files: Optional[Iterable[Path]] = None
190
+ ) -> DeploymentPrefix:
185
191
  """Read the deployment prefix from the repo's Spring configuration.
186
192
 
187
193
  Best effort and never raises: an unreadable or malformed config yields an
@@ -190,7 +196,7 @@ def detect_deployment_prefix(root: Path) -> DeploymentPrefix:
190
196
  """
191
197
  result = DeploymentPrefix()
192
198
  try:
193
- candidates = _config_files(Path(root))
199
+ candidates = _config_files(Path(root), files)
194
200
  except Exception:
195
201
  return result
196
202
 
@@ -3772,17 +3772,40 @@ def _route_path_expression(args_str: str, constants: "Optional[dict[str, str]]")
3772
3772
  return args_str.strip()
3773
3773
 
3774
3774
 
3775
- def _parse_route_http_method(ann_name: str, args_str: str) -> str:
3776
- """Derive HTTP method from annotation name or explicit method= arg."""
3775
+ _ANY_HTTP_METHODS = ("GET", "POST", "PUT", "DELETE", "PATCH")
3776
+
3777
+
3778
+ def _parse_route_http_methods(ann_name: str, args_str: str) -> tuple[str, ...]:
3779
+ """Derive every declared HTTP method, preserving annotation order."""
3777
3780
  explicit = _HTTP_METHOD_MAP.get(ann_name)
3778
3781
  if explicit:
3779
- return explicit
3780
- m = re.search(r'method\s*=\s*(?:RequestMethod\.)?(\w+)', args_str or "")
3782
+ return (explicit,)
3783
+ # Spring accepts both `method=RequestMethod.GET` and the array spelling
3784
+ # `method={RequestMethod.GET, RequestMethod.POST}`. The old expression
3785
+ # expected the token immediately after `=`, so the braces form fell through
3786
+ # to ANY and overstated the reachable verb surface.
3787
+ m = re.search(
3788
+ r'\bmethod\s*=\s*\{?\s*((?:RequestMethod\.)?\w+(?:\s*,\s*(?:RequestMethod\.)?\w+)*)',
3789
+ args_str or "",
3790
+ )
3781
3791
  if m:
3782
- return m.group(1).upper()
3792
+ methods = re.findall(r'(?:RequestMethod\.)?(\w+)', m.group(1))
3793
+ if methods:
3794
+ return tuple(dict.fromkeys(method.upper() for method in methods))
3783
3795
  if ann_name == "@RequestMapping":
3784
- return "ANY"
3785
- return ""
3796
+ return ("ANY",)
3797
+ return ()
3798
+
3799
+
3800
+ def _parse_route_http_method(ann_name: str, args_str: str) -> str:
3801
+ """Backward-compatible primary HTTP method for a route."""
3802
+ methods = _parse_route_http_methods(ann_name, args_str)
3803
+ return methods[0] if methods else ""
3804
+
3805
+
3806
+ def _published_http_methods(method: str) -> list[str]:
3807
+ """Concrete verbs a consumer must consider for a route's method label."""
3808
+ return list(_ANY_HTTP_METHODS if method == "ANY" else (method,))
3786
3809
 
3787
3810
 
3788
3811
  def _parse_route_extras(args_str: str) -> dict:
@@ -5014,7 +5037,8 @@ def _build_route_surface(
5014
5037
  _suffix_expr = _route_path_expression(args, constants)
5015
5038
  effective_prefixes = class_info.get(cls_simple, {}).get("prefixes", [""])
5016
5039
 
5017
- method = _parse_route_http_method(ann_name, args) or "GET"
5040
+ route_methods = _parse_route_http_methods(ann_name, args) or ("GET",)
5041
+ method = route_methods[0]
5018
5042
 
5019
5043
  if cls_simple in class_info:
5020
5044
  class_info[cls_simple]["own_endpoints"].append(
@@ -5070,6 +5094,7 @@ def _build_route_surface(
5070
5094
  "effective_class": cls_fqn,
5071
5095
  "path": full_path,
5072
5096
  "method": method,
5097
+ "methods": _published_http_methods(method) if method == "ANY" else list(route_methods),
5073
5098
  "return_type": (sym.return_type.strip() if sym.return_type else "void"),
5074
5099
  "stable_id": sym.stable_id,
5075
5100
  "inheritance_depth": 0,
@@ -5175,6 +5200,7 @@ def _build_route_surface(
5175
5200
  "effective_class": data["fqn"],
5176
5201
  "path": full_path,
5177
5202
  "method": verb,
5203
+ "methods": _published_http_methods(verb),
5178
5204
  "return_type": _parent_rt_by_sym.get(declaring_sym, "void"),
5179
5205
  "stable_id": stable_id,
5180
5206
  "inheritance_depth": depth,
@@ -6415,6 +6441,37 @@ def extract_java_endpoints(
6415
6441
  and "target/" not in str(p).replace("\\", "/")
6416
6442
  and not any(f in str(p).replace("\\", "/") for f in _CLIENT_PATH_FRAGMENTS)
6417
6443
  )
6444
+ # `endpoints` needs XML both for Spring Security and WebScript detection.
6445
+ # One inventory avoids seven name-specific walks plus a later desc.xml walk.
6446
+ _xml_files = sorted(root.rglob("*.xml"))
6447
+ # Deployment-prefix resolution formerly ran two independent `root.glob()`
6448
+ # calls. One candidate list keeps that work bounded and reusable below.
6449
+ _config_files = sorted(root.rglob("application*.*"))
6450
+ _excluded_test_source_routes = 0
6451
+ _excluded_test_patterns: set[str] = set()
6452
+ if not include_tests:
6453
+ _route_marker = _re.compile(
6454
+ r"@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping|GET|POST|PUT|DELETE|PATCH|Path)\b"
6455
+ )
6456
+ for _candidate in root.rglob("*.java"):
6457
+ _candidate_rel = _rel(_candidate)
6458
+ if not is_test_source_file(_candidate_rel):
6459
+ continue
6460
+ _candidate_parts = _candidate_rel.lower().split("/")
6461
+ if "test-suite" in _candidate_parts:
6462
+ _excluded_test_patterns.add("test-suite/")
6463
+ elif "src/test/" in _candidate_rel.lower():
6464
+ _excluded_test_patterns.add("src/test/")
6465
+ elif "src/it/" in _candidate_rel.lower():
6466
+ _excluded_test_patterns.add("src/it/")
6467
+ else:
6468
+ _excluded_test_patterns.add("test-source classification")
6469
+ try:
6470
+ _excluded_test_source_routes += len(
6471
+ _route_marker.findall(_candidate.read_text(encoding="utf-8", errors="replace"))
6472
+ )
6473
+ except OSError:
6474
+ continue
6418
6475
 
6419
6476
  all_symbols: list[SymbolRecord] = []
6420
6477
  extends_map: dict[str, str] = {}
@@ -6620,6 +6677,7 @@ def extract_java_endpoints(
6620
6677
 
6621
6678
  entry: dict = {
6622
6679
  "method": route["method"],
6680
+ "methods": route.get("methods", _published_http_methods(route["method"])),
6623
6681
  "path": route["path"],
6624
6682
  "controller": controller,
6625
6683
  # Two controllers can share a simple name across packages: keycloak
@@ -6776,23 +6834,23 @@ def extract_java_endpoints(
6776
6834
  re.IGNORECASE,
6777
6835
  )
6778
6836
  _xml_security_detected = False
6779
- _XML_GLOBS = (
6780
- "*security*.xml", "*Security*.xml",
6781
- "*applicationContext*.xml", "*-context.xml", "*Context.xml",
6782
- "*spring*.xml", "*Spring*.xml",
6783
- )
6784
- for _glob in _XML_GLOBS:
6785
- for _xf in root.rglob(_glob):
6786
- if "target/" in str(_xf).replace("\\", "/"):
6787
- continue
6788
- try:
6789
- _xt = _xf.read_text(encoding="utf-8", errors="replace")
6790
- except OSError:
6791
- continue
6792
- if _XML_SECURITY_RE.search(_xt):
6793
- _xml_security_detected = True
6794
- break
6795
- if _xml_security_detected:
6837
+ for _xf in _xml_files:
6838
+ _xml_name = _xf.name.lower()
6839
+ if not (
6840
+ "security" in _xml_name
6841
+ or "spring" in _xml_name
6842
+ or _xml_name.endswith("-context.xml")
6843
+ or _xml_name.endswith("context.xml")
6844
+ ):
6845
+ continue
6846
+ if "target/" in str(_xf).replace("\\", "/"):
6847
+ continue
6848
+ try:
6849
+ _xt = _xf.read_text(encoding="utf-8", errors="replace")
6850
+ except OSError:
6851
+ continue
6852
+ if _XML_SECURITY_RE.search(_xt):
6853
+ _xml_security_detected = True
6796
6854
  break
6797
6855
 
6798
6856
  if _xml_security_detected:
@@ -6892,7 +6950,7 @@ def extract_java_endpoints(
6892
6950
  try:
6893
6951
  from sourcecode.deployment_prefix import detect_deployment_prefix
6894
6952
 
6895
- _prefix = detect_deployment_prefix(root)
6953
+ _prefix = detect_deployment_prefix(root, files=_config_files)
6896
6954
  if _prefix.sources:
6897
6955
  result["deployment_prefix"] = _prefix.to_dict()
6898
6956
  if _prefix.prefix:
@@ -6912,6 +6970,17 @@ def extract_java_endpoints(
6912
6970
  )
6913
6971
  elif _test_util_endpoints:
6914
6972
  result["test_util_included"] = len(_test_util_endpoints)
6973
+ if _excluded_test_source_routes:
6974
+ result["test_source_excluded"] = _excluded_test_source_routes
6975
+ result["test_source_excluded_detail"] = {
6976
+ "count": _excluded_test_source_routes,
6977
+ "patterns_matched": sorted(_excluded_test_patterns),
6978
+ "basis": "path classification; use --include-tests to include these routes",
6979
+ }
6980
+ result.setdefault("notes", []).append(
6981
+ f"{_excluded_test_source_routes} route annotation(s) from test source trees "
6982
+ "were excluded; use --include-tests to include them."
6983
+ )
6915
6984
  # Surface incomplete-endpoint warnings (interface-defined controllers) only when
6916
6985
  # present, to keep output backward-compatible for the common case.
6917
6986
  if endpoint_warnings:
@@ -6949,7 +7018,9 @@ def extract_java_endpoints(
6949
7018
  # (*.desc.xml: <url>/<authentication>/<transaction>) in addition to the Java
6950
7019
  # signals gathered above.
6951
7020
  _webscript_descriptors = 0
6952
- for _df in root.rglob("*.desc.xml"):
7021
+ for _df in _xml_files:
7022
+ if not _df.name.endswith(".desc.xml"):
7023
+ continue
6953
7024
  if "target/" in str(_df).replace("\\", "/"):
6954
7025
  continue
6955
7026
  try:
@@ -6977,7 +7048,7 @@ def extract_java_endpoints(
6977
7048
  from sourcecode.servlet_surface import build_servlet_surface
6978
7049
 
6979
7050
  _nonspring["servlets"] = int(
6980
- build_servlet_surface(root).get("declarations", 0)
7051
+ build_servlet_surface(root, files=[*java_files, *_xml_files]).get("declarations", 0)
6981
7052
  )
6982
7053
  except Exception:
6983
7054
  pass
@@ -7131,6 +7202,10 @@ _DISPLAY_INDIRECT_CALLERS = 50
7131
7202
  HUB_CALLER_THRESHOLD = 500
7132
7203
  HUB_SAMPLE_SIZE = 20
7133
7204
  HUB_SAMPLE_DEPTH = 2
7205
+ # A frontier is a hub for this query when it materially exceeds the target's
7206
+ # own fan-in, even if it does not reach the global safety cap above.
7207
+ HUB_RELATIVE_FANIN_MULTIPLIER = 3
7208
+ HUB_RELATIVE_FANIN_FLOOR = 20
7134
7209
 
7135
7210
 
7136
7211
  def _symbol_listing_advice(root: "Optional[Path]") -> str:
@@ -7283,21 +7358,15 @@ def compute_blast_radius(
7283
7358
  _rg_index = _class_method_index(reverse_graph)
7284
7359
 
7285
7360
  def _walk_callers(_node: str) -> list[str]:
7286
- """Expand method frontiers through their owning class as well.
7361
+ """Return the one-authority caller set for this exact frontier node.
7287
7362
 
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.
7363
+ `caller_reach` expands class nodes through their method keys, which is
7364
+ required for DI-normalized class frontiers. It deliberately does not
7365
+ invert that containment for a method: callers of `Locator#getA` are a
7366
+ subset of callers of `Locator`, and adding the latter leaks users of
7367
+ every other locator accessor into the blast cone.
7292
7368
  """
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))
7369
+ return _all_callers_from_rg(_node, reverse_graph, _rg_index, _impl_graph)
7301
7370
  # Callers inject the interface, not the implementation, so a walk that does not
7302
7371
  # know the implements relation stops one hop short of every DI consumer. Without
7303
7372
  # it this traversal reached 200 classes and **no controller at all** for
@@ -7417,6 +7486,26 @@ def compute_blast_radius(
7417
7486
  if _effective_depth > 1:
7418
7487
  queue.append((c, 1))
7419
7488
 
7489
+ # A locator accessor can be a narrow method frontier on a broad class hub.
7490
+ # Measure the owner only to classify that frontier, never to expand it: the
7491
+ # latter would merge callers of unrelated accessors (Q-01).
7492
+ _seed_fan_in = max(1, len(set(direct_callers)))
7493
+ _hub_frontier_cutoff = max(
7494
+ HUB_RELATIVE_FANIN_FLOOR,
7495
+ _seed_fan_in * HUB_RELATIVE_FANIN_MULTIPLIER,
7496
+ )
7497
+
7498
+ def _is_hub_frontier(_node: str) -> bool:
7499
+ _fan_in = len(_walk_callers(_node))
7500
+ if "#" in _node:
7501
+ _fan_in = max(
7502
+ _fan_in,
7503
+ len(_all_callers_from_rg(
7504
+ _enclosing_class(_node), reverse_graph, _rg_index, _impl_graph
7505
+ )),
7506
+ )
7507
+ return _fan_in > _hub_frontier_cutoff
7508
+
7420
7509
  # BFS for indirect callers
7421
7510
  indirect_callers: list[str] = []
7422
7511
  _indirect_parent: dict[str, str] = {}
@@ -7428,7 +7517,7 @@ def compute_blast_radius(
7428
7517
  if depth >= _effective_depth:
7429
7518
  continue
7430
7519
  _node_callers = _walk_callers(node)
7431
- if len(_node_callers) > _HUB_CALLER_THRESHOLD:
7520
+ if _is_hub_frontier(node):
7432
7521
  _hub_frontier_nodes.add(node)
7433
7522
  for caller in _node_callers:
7434
7523
  if caller not in visited:
@@ -7679,7 +7768,7 @@ def compute_blast_radius(
7679
7768
  if _depth >= _effective_depth:
7680
7769
  continue
7681
7770
  _next = _walk_callers(_node)
7682
- if len(_next) > _HUB_CALLER_THRESHOLD:
7771
+ if _is_hub_frontier(_node):
7683
7772
  continue
7684
7773
  for _caller in _next:
7685
7774
  if _caller not in _without_hubs_seen:
@@ -7853,6 +7942,10 @@ def compute_blast_radius(
7853
7942
  _indirect_summary = (
7854
7943
  _indirect_ordered if _indirect_cap is None else _indirect_ordered[:_indirect_cap]
7855
7944
  )
7945
+ _indirect_by_depth: dict[str, int] = {}
7946
+ for _node in indirect_callers:
7947
+ _depth_key = str(all_affected.get(_node, 0))
7948
+ _indirect_by_depth[_depth_key] = _indirect_by_depth.get(_depth_key, 0) + 1
7856
7949
  def _via_hub(_node: str) -> bool:
7857
7950
  _seen_path: set[str] = set()
7858
7951
  _cursor = _node
@@ -7914,6 +8007,9 @@ def compute_blast_radius(
7914
8007
  ),
7915
8008
  "indirect_callers": _indirect_summary,
7916
8009
  "indirect_caller_details": _indirect_details,
8010
+ "indirect_callers_unit": "reference symbols (methods or classes)",
8011
+ "indirect_callers_by_depth": _indirect_by_depth,
8012
+ "indirect_callers_without_hubs": _without_hubs,
7917
8013
  "container_wired": [w.to_dict() for w in _container_wired],
7918
8014
  "endpoints_affected": endpoints_affected,
7919
8015
  "mappers_affected": mappers_affected,
@@ -276,7 +276,13 @@ def _scan_java(source: str, rel: str) -> "list[SecurityConfigObservation]":
276
276
  ))
277
277
  break # one observation per algorithm per file
278
278
  for encoder in _DEPRECATED_ENCODERS:
279
- m = re.search(re.escape(encoder), source)
279
+ # An import only makes the type available; it is not evidence that
280
+ # a password encoder is used. Require construction or the legacy
281
+ # factory call so the witness opens on a remediation site.
282
+ m = re.search(
283
+ rf"(?:new\s+{re.escape(encoder)}\b|{re.escape(encoder)}\s*\.\s*getInstance\s*\()",
284
+ source,
285
+ )
280
286
  if m:
281
287
  out.append(SecurityConfigObservation(
282
288
  rule="SEC-004",
@@ -20,10 +20,11 @@ So the answer is a second, named population beside the first, with its own total
20
20
 
21
21
  from __future__ import annotations
22
22
 
23
+ import os
23
24
  import re
24
25
  from dataclasses import dataclass
25
26
  from pathlib import Path
26
- from typing import Optional
27
+ from typing import Iterable, Optional
27
28
  from xml.etree import ElementTree as ET
28
29
 
29
30
  POPULATION = "servlet"
@@ -82,11 +83,12 @@ def _child_text(elem: "ET.Element", name: str) -> str:
82
83
  return ""
83
84
 
84
85
 
85
- def _from_web_xml(xml_path: Path, relative: str) -> "list[ServletMapping]":
86
+ def _from_web_xml(xml_path: Path, relative: str) -> "tuple[list[ServletMapping], int]":
86
87
  try:
87
- root = ET.parse(str(xml_path)).getroot()
88
+ text = xml_path.read_text(encoding="utf-8", errors="replace")
89
+ root = ET.fromstring(text)
88
90
  except Exception:
89
- return []
91
+ return [], 0
90
92
  classes: "dict[str, str]" = {}
91
93
  for elem in root.iter():
92
94
  if _local(elem.tag) != "servlet":
@@ -96,10 +98,17 @@ def _from_web_xml(xml_path: Path, relative: str) -> "list[ServletMapping]":
96
98
  if name:
97
99
  classes[name] = klass
98
100
  out: "list[ServletMapping]" = []
101
+ undeclared_servlet_mappings = 0
99
102
  for elem in root.iter():
100
103
  if _local(elem.tag) != "servlet-mapping":
101
104
  continue
102
105
  name = _child_text(elem, "servlet-name")
106
+ if not name or name not in classes:
107
+ undeclared_servlet_mappings += sum(
108
+ 1 for child in elem.iter()
109
+ if _local(child.tag) == "url-pattern" and (child.text or "").strip()
110
+ )
111
+ continue
103
112
  for child in elem.iter():
104
113
  if _local(child.tag) != "url-pattern" or not (child.text or "").strip():
105
114
  continue
@@ -110,7 +119,31 @@ def _from_web_xml(xml_path: Path, relative: str) -> "list[ServletMapping]":
110
119
  source_file=relative,
111
120
  source="web.xml",
112
121
  ))
113
- return out
122
+ # Keep a tolerant lexical fallback for descriptors containing legacy XML
123
+ # constructs that ElementTree rejects or partially normalizes. Comments are
124
+ # removed first so commented deployment examples never become live routes.
125
+ try:
126
+ active = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
127
+ for block in re.findall(r"<servlet-mapping\b[^>]*>(.*?)</servlet-mapping>", active, re.DOTALL | re.IGNORECASE):
128
+ name_match = re.search(r"<servlet-name\b[^>]*>\s*([^<]+?)\s*</servlet-name>", block, re.DOTALL | re.IGNORECASE)
129
+ pattern_match = re.search(r"<url-pattern\b[^>]*>\s*([^<]+?)\s*</url-pattern>", block, re.DOTALL | re.IGNORECASE)
130
+ if not pattern_match:
131
+ continue
132
+ servlet_name = name_match.group(1).strip() if name_match else ""
133
+ if not servlet_name or servlet_name not in classes:
134
+ continue
135
+ candidate = ServletMapping(
136
+ url_pattern=pattern_match.group(1).strip(),
137
+ servlet_name=servlet_name,
138
+ servlet_class=classes[servlet_name],
139
+ source_file=relative,
140
+ source="web.xml",
141
+ )
142
+ if candidate not in out:
143
+ out.append(candidate)
144
+ except Exception:
145
+ pass
146
+ return out, undeclared_servlet_mappings
114
147
 
115
148
 
116
149
  def _from_registration_beans(java_path: Path, relative: str) -> "list[ServletMapping]":
@@ -144,14 +177,37 @@ def _from_registration_beans(java_path: Path, relative: str) -> "list[ServletMap
144
177
  return out
145
178
 
146
179
 
147
- def build_servlet_surface(root: Path, *, limit: Optional[int] = None) -> dict:
180
+ def build_servlet_surface(
181
+ root: Path,
182
+ *,
183
+ limit: Optional[int] = None,
184
+ files: Optional[Iterable[Path]] = None,
185
+ ) -> dict:
148
186
  """Every servlet-mapped URL pattern under `root`, as a population of its own."""
149
187
  from sourcecode import non_coverage
150
188
 
151
189
  root = Path(root)
152
190
  mappings: "list[ServletMapping]" = []
153
- for path in sorted(root.rglob("*")):
154
- if any(part in _SKIP_DIRS for part in path.parts) or not path.is_file():
191
+ undeclared_servlet_mappings = 0
192
+ if files is None:
193
+ candidates: list[Path] = []
194
+ for directory, dirs, names in os.walk(root):
195
+ dirs[:] = sorted(name for name in dirs if name not in _SKIP_DIRS)
196
+ candidates.extend(
197
+ Path(directory) / name
198
+ for name in sorted(names)
199
+ if name == "web.xml" or name.endswith(".java")
200
+ )
201
+ else:
202
+ candidates = sorted(
203
+ {
204
+ Path(path)
205
+ for path in files
206
+ if Path(path).name == "web.xml" or Path(path).suffix == ".java"
207
+ }
208
+ )
209
+ for path in candidates:
210
+ if not path.is_file():
155
211
  continue
156
212
  try:
157
213
  # POSIX, like every other path this CLI publishes. C2-18 fixed this
@@ -164,7 +220,9 @@ def build_servlet_surface(root: Path, *, limit: Optional[int] = None) -> dict:
164
220
  except ValueError:
165
221
  relative = Path(path).as_posix()
166
222
  if path.name == "web.xml":
167
- mappings.extend(_from_web_xml(path, relative))
223
+ parsed, omitted = _from_web_xml(path, relative)
224
+ mappings.extend(parsed)
225
+ undeclared_servlet_mappings += omitted
168
226
  elif path.suffix == ".java":
169
227
  mappings.extend(_from_registration_beans(path, relative))
170
228
 
@@ -176,6 +234,7 @@ def build_servlet_surface(root: Path, *, limit: Optional[int] = None) -> dict:
176
234
  _unique: dict[str, ServletMapping] = {}
177
235
  for mapping in sorted(mappings, key=lambda m: (m.url_pattern, m.source_file, m.source)):
178
236
  _unique.setdefault(mapping.url_pattern, mapping)
237
+ declared_mappings = list(mappings)
179
238
  mappings = list(_unique.values())
180
239
  patterns = sorted(_unique)
181
240
  by_source: "dict[str, int]" = {}
@@ -187,15 +246,25 @@ def build_servlet_surface(root: Path, *, limit: Optional[int] = None) -> dict:
187
246
  )]
188
247
  shown = rows if limit is None else rows[:limit]
189
248
 
249
+ display_omitted = len(rows) - len(shown)
250
+ omitted_total = undeclared_servlet_mappings + display_omitted
251
+ omitted_by_reason: dict[str, int] = {}
252
+ if undeclared_servlet_mappings:
253
+ omitted_by_reason["servlet_not_declared"] = undeclared_servlet_mappings
254
+ if display_omitted:
255
+ omitted_by_reason["display_limit"] = display_omitted
256
+
190
257
  return {
191
258
  "schema_version": SCHEMA_VERSION,
192
259
  "population_key": POPULATION,
193
260
  "total_url_patterns": len(patterns),
194
- "declarations": len(mappings),
261
+ "declarations": len(declared_mappings),
262
+ "declarations_unit": "live servlet-mapping declarations before URL-pattern deduplication",
195
263
  "by_source": dict(sorted(by_source.items())),
196
264
  "url_patterns": patterns,
197
265
  "mappings": shown,
198
- "mappings_omitted": len(rows) - len(shown),
266
+ "mappings_omitted": omitted_total,
267
+ "mappings_omitted_by_reason": omitted_by_reason,
199
268
  "population": (
200
269
  "servlet mappings declared in `web.xml` or registered as a "
201
270
  "`ServletRegistrationBean` — HTTP surface that is NOT a Spring handler "
@@ -614,7 +614,16 @@ def build_validation_surface(
614
614
  "summary": {
615
615
  "endpoints_with_body": len(out_endpoints),
616
616
  "validated_fields": total_validated_fields,
617
+ # This catalog contains annotation-based ConstraintValidator types.
618
+ # Spring Validator wiring is detected separately below and must not
619
+ # be read as zero just because it belongs to another population.
617
620
  "custom_validators_declared": len(custom_list),
621
+ "custom_validators_declared_unit": "annotation-based ConstraintValidator declarations",
622
+ "programmatic_validators_declared": len(programmatic_evidence),
623
+ "programmatic_validators_declared_unit": (
624
+ "detected org.springframework.validation.Validator implementations "
625
+ "or setValidator wiring; rules are not modeled per endpoint"
626
+ ),
618
627
  "custom_validators_linked": len(custom_used),
619
628
  "gaps": len(gaps),
620
629
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.5.2
3
+ Version: 5.5.5
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.2-blue)
45
+ ![Version](https://img.shields.io/badge/version-5.5.5-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.2 — and, on a build that has aged,
129
+ ask version # ask 5.5.5 — 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=N8caEJv8V-c5M8fb_R3zr9u-Fd7axm-yB4yw-d_rbf0,308
1
+ sourcecode/__init__.py,sha256=LWoy6uySGMfIcuMSAGcCF4LHEF2d_DbNo4PrVl8kous,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=hJukwL8ZTc1ZHVkkZWau9J0LFYtAfyHvVdp4HKIRWIY,648972
22
+ sourcecode/cli.py,sha256=Lq6Kbunzj01R5pbcDbrF8lMslaTpc4JbrLyVozCsstA,650907
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
@@ -42,7 +42,7 @@ sourcecode/declarations.py,sha256=Jr7oJDg004sFw0TvJySNVggXFT2jB6hQgbeQK2qreVo,70
42
42
  sourcecode/defect_identity.py,sha256=XS2VFDImg9NLaRImybKTeyk-CoDSsXmchldg0dwvM7c,7028
43
43
  sourcecode/degradation.py,sha256=BtuGUKFgkzI3Z6fwmUdxEUEaLFAcPQ_bHhU2w9ItDxw,18376
44
44
  sourcecode/dependency_analyzer.py,sha256=MRv6eh1TG5E9Y5Db-bNdZHDIJ3gWvvL3bBU1-9_QSZI,64593
45
- sourcecode/deployment_prefix.py,sha256=g0_4bE_m9svjhnapRpOp8BJkj6Ul94Ij_iVezygMZ0I,11956
45
+ sourcecode/deployment_prefix.py,sha256=kPHPT6ZLg6CnMqCp9QwhbNNjDyS0sXAfKlPtSHUMggs,12180
46
46
  sourcecode/detach.py,sha256=PvkCcbIuTaDKKt5TGqtQLahmsmCkflui33jLnMEWkvg,8006
47
47
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
48
48
  sourcecode/dynamic_argument_surface.py,sha256=u6zJoY-zChYVw1-SrgiyAq-rUwqMZ5ZqBwLfbPJ_8ng,2960
@@ -106,7 +106,7 @@ sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcr
106
106
  sourcecode/remedies.py,sha256=V2_cD4pLq8bKRkzF-3RGFoiu1xWYZS384WDVp1MrlZw,10225
107
107
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
108
108
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
109
- sourcecode/repository_ir.py,sha256=tV0Uu7qWw_gDUG9WldYxmbNLZnbQljxHYZznWP4cdlA,384071
109
+ sourcecode/repository_ir.py,sha256=nSStGh9z8uHSTj92IJp85Y1J8NDGqlY59KtEsgHThqY,388812
110
110
  sourcecode/ris.py,sha256=Xbx34dkJI_uXil9xsgw-fA_Apcm4nbnTjQgFm66dP1g,27489
111
111
  sourcecode/risk.py,sha256=u2V7nG5yGW8VV4sFDSgFkZqdI6ltmilWhlQwlIH7d2o,82660
112
112
  sourcecode/rule_catalog.py,sha256=pTkgkQZ1u7atB3fkQJ9BPWo8lNiamvEuRN7MtRgle0c,5263
@@ -118,14 +118,14 @@ sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
118
118
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
119
119
  sourcecode/security_chain.py,sha256=1YZVeFLwy65kntMBhMNvz1cUpOoKCmvEqYICgxs66Gw,6823
120
120
  sourcecode/security_config.py,sha256=LUppRDYQK0oKM4qdEOLObaeU3frFTS-paEZo7dMgtY8,5467
121
- sourcecode/security_config_scan.py,sha256=CttiIYVhBlPCuLNYMWTEtgbZmwXGYm2GYiFo9ehD1Go,32487
121
+ sourcecode/security_config_scan.py,sha256=iXDeulgdbAwUFR01p9PgrJED2vd4HE2FCZgqrxX5D0E,32824
122
122
  sourcecode/security_posture.py,sha256=FiEGood01y5gKhuYyL6NK4VT-WtCUMJwHOSR5xTfnPI,54063
123
123
  sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
124
124
  sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
125
125
  sourcecode/semantic_integration_engine.py,sha256=7a0WqAInOv39f0Yr_94TYo_JP_8QpeI9KGaknpzAyQU,18899
126
126
  sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
127
127
  sourcecode/serializer.py,sha256=cK4I3WzX0H-oBPbqzBsUBRux5GcYmH_yPDQwxvkf8yQ,141598
128
- sourcecode/servlet_surface.py,sha256=QY3QPw6pZJ4CQK4yS8q7uDd3f2PDyUUsQ8TS_BkbGs8,9506
128
+ sourcecode/servlet_surface.py,sha256=1ZB76G5PrIGqmRAO8rEd20SMYoexZ5t01v82HtHLSXo,12481
129
129
  sourcecode/source_text.py,sha256=R52gmSljz7L6YVPbixv5W-LOZ5eKUVvSUP3S-QH_IWc,9047
130
130
  sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
131
131
  sourcecode/spring_findings.py,sha256=cW6NiQAFUM9mDYPitJr6MrQg--YBI41LxrSEMI9aeIQ,25272
@@ -147,7 +147,7 @@ sourcecode/token_estimate.py,sha256=G-ivzIJfVq5TLQN04dC0WK4LZJjWVYL54Ebcxe4fJ7M,
147
147
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
148
148
  sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
149
149
  sourcecode/validation_inference.py,sha256=-oWJqE6PqkqcZbCFDJeWCQHgI9Dbuv5ggJWD3LE_2IU,20817
150
- sourcecode/validation_surface.py,sha256=06muCMxymQEQSqmNgTMdsguC7XQ22tqcZmYJOmvWFHY,34745
150
+ sourcecode/validation_surface.py,sha256=diiAyPiLdvmtTYrx9EywDXHG8odg9gITmN4od8Tj3JQ,35390
151
151
  sourcecode/verify_edit.py,sha256=SUaJeH5Icc54mutQx3-gh7i8U6d50cb_oxYc9SDUnHs,42757
152
152
  sourcecode/verify_repo.py,sha256=aHlNIUfoVVUxTXTNtSFryP4dOFBOvCfdyC31fFRXU6s,15346
153
153
  sourcecode/verify_rules.py,sha256=R3l6nLT6WQzUNBzxtmnxyGZcKj9qMDW38Y0c0gNdCGk,19973
@@ -213,10 +213,10 @@ sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8
213
213
  sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
214
214
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
215
215
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
216
- sourcecode/_docs/DEFECT-LEDGER.md,sha256=sTFPEV2xx-L9n4mrG8UO4LxeETMJpGKHXyKQBZlqNog,497112
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,,
216
+ sourcecode/_docs/DEFECT-LEDGER.md,sha256=5SxWnKt6p5ZRmx-WsFhIlMfe7-e5fLF0QRa9lAmzYug,499071
217
+ sourcecode/_docs/USER_GUIDE.md,sha256=ykjudytfe1VLgGJQAcs36wkXsEO3N-QJY2js1cJc7y0,79837
218
+ sourcecode-5.5.5.dist-info/METADATA,sha256=bMkQLLoD1zYBemJ5ZJpHyy9xIurRVdVOTCP20jSLN6I,47104
219
+ sourcecode-5.5.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
220
+ sourcecode-5.5.5.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
221
+ sourcecode-5.5.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
222
+ sourcecode-5.5.5.dist-info/RECORD,,