sourcecode 4.10.3__py3-none-any.whl → 4.10.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 +1 -1
- sourcecode/architectural_baseline.py +34 -5
- sourcecode/cache_model.py +41 -1
- sourcecode/cli.py +121 -18
- sourcecode/pr_impact.py +38 -4
- sourcecode/progress.py +98 -5
- sourcecode/provenance.py +18 -10
- sourcecode/release_info.py +1 -1
- sourcecode/repository_ir.py +38 -1
- sourcecode/security_config_scan.py +30 -22
- sourcecode/spring_security_audit.py +10 -0
- sourcecode/text_input.py +93 -0
- sourcecode/verify_edit.py +31 -0
- {sourcecode-4.10.3.dist-info → sourcecode-4.10.5.dist-info}/METADATA +6 -6
- {sourcecode-4.10.3.dist-info → sourcecode-4.10.5.dist-info}/RECORD +18 -17
- {sourcecode-4.10.3.dist-info → sourcecode-4.10.5.dist-info}/WHEEL +0 -0
- {sourcecode-4.10.3.dist-info → sourcecode-4.10.5.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.10.3.dist-info → sourcecode-4.10.5.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -33,7 +33,7 @@ import json
|
|
|
33
33
|
import subprocess
|
|
34
34
|
from datetime import datetime, timezone
|
|
35
35
|
from pathlib import Path
|
|
36
|
-
from typing import TYPE_CHECKING
|
|
36
|
+
from typing import TYPE_CHECKING, Any
|
|
37
37
|
|
|
38
38
|
from sourcecode import __version__ as TOOL_VERSION
|
|
39
39
|
from sourcecode.architectural_delta import ArchMetrics, diff_metrics, extract_metrics
|
|
@@ -107,6 +107,35 @@ def _hotspots(fan_in: dict[str, int], top_n: int) -> list[dict]:
|
|
|
107
107
|
return [{"symbol": s, "fan_in": c} for s, c in ranked[:top_n]]
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def _fan_in_to_list(fan_in: dict[str, int]) -> list[dict]:
|
|
111
|
+
"""Serialise the full fan-in map as an array of ``{symbol, fan_in}`` objects,
|
|
112
|
+
sorted by symbol — never a JSON object keyed by symbol.
|
|
113
|
+
|
|
114
|
+
C3-62: PowerShell's `ConvertFrom-Json` folds object keys case-insensitively, so
|
|
115
|
+
two distinct Java symbols differing only by case (legal — Java is case-sensitive)
|
|
116
|
+
collapse into one key and abort parsing the *whole* baseline document. `hotspots`
|
|
117
|
+
already used this array shape (`_hotspots`); the full map stored beside it did
|
|
118
|
+
not. An array of objects has no such ceiling on any consumer."""
|
|
119
|
+
return [{"symbol": s, "fan_in": c} for s, c in sorted(fan_in.items())]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _fan_in_from_stored(value: Any) -> dict[str, int]:
|
|
123
|
+
"""Read a stored ``fan_in`` field back into a symbol → count map.
|
|
124
|
+
|
|
125
|
+
Accepts both the array-of-objects shape (C3-62, current) and the legacy
|
|
126
|
+
object-keyed-by-symbol shape (baselines captured before this fix) so an older
|
|
127
|
+
baseline already committed to disk keeps diffing and trending correctly."""
|
|
128
|
+
if isinstance(value, list):
|
|
129
|
+
return {
|
|
130
|
+
str(item["symbol"]): int(item.get("fan_in", 0))
|
|
131
|
+
for item in value
|
|
132
|
+
if isinstance(item, dict) and "symbol" in item
|
|
133
|
+
}
|
|
134
|
+
if isinstance(value, dict):
|
|
135
|
+
return {str(k): int(v) for k, v in value.items()}
|
|
136
|
+
return {}
|
|
137
|
+
|
|
138
|
+
|
|
110
139
|
def build_baseline(
|
|
111
140
|
cir: "CanonicalRepositoryIR",
|
|
112
141
|
*,
|
|
@@ -126,7 +155,6 @@ def build_baseline(
|
|
|
126
155
|
that share them."""
|
|
127
156
|
m = extract_metrics(cir)
|
|
128
157
|
endpoints = sorted([meth, path] for (meth, path) in m.endpoints)
|
|
129
|
-
fan_in = dict(sorted(m.fan_in.items()))
|
|
130
158
|
cycles = sorted((sorted(c) for c in m.cycles), key=lambda mm: (len(mm), mm))
|
|
131
159
|
return {
|
|
132
160
|
"schema": ARCH_BASELINE_SCHEMA,
|
|
@@ -145,7 +173,7 @@ def build_baseline(
|
|
|
145
173
|
},
|
|
146
174
|
"hotspots": _hotspots(m.fan_in, hotspot_n),
|
|
147
175
|
"endpoint_surface": endpoints,
|
|
148
|
-
"fan_in": fan_in,
|
|
176
|
+
"fan_in": _fan_in_to_list(m.fan_in),
|
|
149
177
|
"import_cycles": cycles,
|
|
150
178
|
"env": env if env is not None else collect_env(),
|
|
151
179
|
"provenance": (
|
|
@@ -209,7 +237,7 @@ def baseline_to_metrics(baseline: dict) -> ArchMetrics:
|
|
|
209
237
|
endpoint_count=int(totals.get("endpoints", 0)),
|
|
210
238
|
dependency_edge_count=int(totals.get("dependency_edges", 0)),
|
|
211
239
|
endpoints=endpoints,
|
|
212
|
-
fan_in=
|
|
240
|
+
fan_in=_fan_in_from_stored(baseline.get("fan_in")),
|
|
213
241
|
cycles=frozenset(
|
|
214
242
|
frozenset(str(m) for m in members)
|
|
215
243
|
for members in baseline.get("import_cycles", [])
|
|
@@ -397,9 +425,10 @@ def build_trend(baselines: list[dict]) -> dict:
|
|
|
397
425
|
if isinstance(h, dict) and h.get("symbol"):
|
|
398
426
|
tracked.add(str(h["symbol"]))
|
|
399
427
|
|
|
428
|
+
fan_in_by_baseline = [_fan_in_from_stored(b.get("fan_in")) for b in baselines]
|
|
400
429
|
hotspot_trends: list[dict] = []
|
|
401
430
|
for sym in sorted(tracked):
|
|
402
|
-
series = [
|
|
431
|
+
series = [fmap.get(sym, 0) for fmap in fan_in_by_baseline]
|
|
403
432
|
first, last = series[0], series[-1]
|
|
404
433
|
hotspot_trends.append({
|
|
405
434
|
"symbol": sym,
|
sourcecode/cache_model.py
CHANGED
|
@@ -302,8 +302,48 @@ def render_markdown() -> str:
|
|
|
302
302
|
return "\n".join(out)
|
|
303
303
|
|
|
304
304
|
|
|
305
|
+
def render_summary() -> str:
|
|
306
|
+
"""The model as `ask cache model` prints it by default (C3-63).
|
|
307
|
+
|
|
308
|
+
One line per layer and one line per command — every command named (nothing
|
|
309
|
+
curated out, so the same completeness the battery checks against `render_text`
|
|
310
|
+
holds here too), but without the per-command prose (`note`) and timing
|
|
311
|
+
(`measured`) detail that made the default ~2 500 tokens of prose in a CLI whose
|
|
312
|
+
own selling argument is token economy. That detail is still one flag away.
|
|
313
|
+
"""
|
|
314
|
+
lines: list[str] = []
|
|
315
|
+
lines.append(
|
|
316
|
+
"Invalidation: every layer keys on the exact tree state — any change to the "
|
|
317
|
+
"analysed files invalidates it, committed or not."
|
|
318
|
+
)
|
|
319
|
+
lines.append("")
|
|
320
|
+
lines.append("Layers")
|
|
321
|
+
id_width = max(len(lyr.id) for lyr in LAYERS)
|
|
322
|
+
for lyr in LAYERS:
|
|
323
|
+
lines.append(f" {lyr.id.ljust(id_width)} {lyr.stores}")
|
|
324
|
+
lines.append("")
|
|
325
|
+
lines.append(f"What a warm gives each command ({REFERENCE_REPOSITORY})")
|
|
326
|
+
cmd_width = max(len(c.command) for c in COMMANDS)
|
|
327
|
+
warm_width = max(len(label) for label in _WARM_LABEL.values())
|
|
328
|
+
for cmd in COMMANDS:
|
|
329
|
+
repeat = "repeat cached" if cmd.repeat else "recomputes"
|
|
330
|
+
layers = ", ".join(cmd.reads) or "none"
|
|
331
|
+
lines.append(
|
|
332
|
+
f" {cmd.command.ljust(cmd_width)} {_WARM_LABEL[cmd.warm].ljust(warm_width)} "
|
|
333
|
+
f"({repeat}) [{layers}]"
|
|
334
|
+
)
|
|
335
|
+
lines.append("")
|
|
336
|
+
lines.append(
|
|
337
|
+
"Full detail (measured timings, per-command notes): ask cache model --full"
|
|
338
|
+
)
|
|
339
|
+
lines.append("Tables for docs: ask cache model --markdown · machine-readable: --json")
|
|
340
|
+
return "\n".join(lines)
|
|
341
|
+
|
|
342
|
+
|
|
305
343
|
def render_text() -> str:
|
|
306
|
-
"""The model
|
|
344
|
+
"""The model in full: every layer and command with its measured timing and note.
|
|
345
|
+
|
|
346
|
+
`ask cache model --full`. See `render_summary` for the default."""
|
|
307
347
|
lines: list[str] = []
|
|
308
348
|
lines.append("Invalidation — one rule for every layer")
|
|
309
349
|
lines.append(" Every layer keys on the exact tree state. Any change to the analysed files")
|
sourcecode/cli.py
CHANGED
|
@@ -15,9 +15,10 @@ from typing import Any, NoReturn, Optional, cast
|
|
|
15
15
|
import typer
|
|
16
16
|
|
|
17
17
|
from sourcecode import __version__
|
|
18
|
-
from sourcecode.error_schema import INVALID_INPUT_CODE, build_error_envelope
|
|
18
|
+
from sourcecode.error_schema import EXECUTION_FAILED_CODE, INVALID_INPUT_CODE, build_error_envelope
|
|
19
19
|
from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
|
|
20
20
|
from sourcecode.progress import Progress
|
|
21
|
+
from sourcecode.progress import output_emitted as _progress_output_emitted
|
|
21
22
|
from sourcecode import perf
|
|
22
23
|
from sourcecode.caller_metrics import (
|
|
23
24
|
CALLER_METRIC_RECONCILIATION,
|
|
@@ -888,8 +889,27 @@ def _safe_write_file(path: "Path", content: str) -> None:
|
|
|
888
889
|
path = Path(path)
|
|
889
890
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
890
891
|
tmp = path.with_name(f".{path.name}.tmp-{os.getpid()}-{time.time_ns()}")
|
|
891
|
-
|
|
892
|
+
# C3-54 part 2: the rename is atomic, the bytes were not durable. A
|
|
893
|
+
# process torn down between `write_text` and the OS flushing its page
|
|
894
|
+
# cache leaves a file that exists at full length and holds nothing —
|
|
895
|
+
# the same total loss the false `done` line reported as success, one
|
|
896
|
+
# layer down. fsync the data before the rename, then the directory
|
|
897
|
+
# entry, so a run that reports an output file has one.
|
|
898
|
+
with open(tmp, "w", encoding="utf-8") as fh:
|
|
899
|
+
fh.write(content)
|
|
900
|
+
fh.flush()
|
|
901
|
+
os.fsync(fh.fileno())
|
|
892
902
|
tmp.replace(path)
|
|
903
|
+
try:
|
|
904
|
+
dir_fd = os.open(str(path.parent), os.O_RDONLY)
|
|
905
|
+
try:
|
|
906
|
+
os.fsync(dir_fd)
|
|
907
|
+
finally:
|
|
908
|
+
os.close(dir_fd)
|
|
909
|
+
except (OSError, AttributeError):
|
|
910
|
+
# Directory fsync is POSIX-only; on Windows the rename is already
|
|
911
|
+
# ordered against the data write. Never fail a good run over it.
|
|
912
|
+
pass
|
|
893
913
|
try:
|
|
894
914
|
from sourcecode.runs import active_run
|
|
895
915
|
run = active_run()
|
|
@@ -1213,6 +1233,9 @@ def _emit_command_output(
|
|
|
1213
1233
|
# The payload is on stdout here, so this cannot join it; it is a
|
|
1214
1234
|
# notice and takes the terminal gate rather than dirtying a capture.
|
|
1215
1235
|
_notice("✓ copied to clipboard")
|
|
1236
|
+
# The answer has been handed over. Any `done (Xs)` a command deferred is now
|
|
1237
|
+
# a true claim and prints here — never before this line (C3-54).
|
|
1238
|
+
_progress_output_emitted()
|
|
1216
1239
|
|
|
1217
1240
|
|
|
1218
1241
|
def _emit_text_output(text: str, copy: bool) -> None:
|
|
@@ -1228,6 +1251,7 @@ def _emit_text_output(text: str, copy: bool) -> None:
|
|
|
1228
1251
|
typer.echo(text)
|
|
1229
1252
|
if copy and _copy_to_clipboard(text):
|
|
1230
1253
|
_notice("✓ copied to clipboard")
|
|
1254
|
+
_progress_output_emitted()
|
|
1231
1255
|
|
|
1232
1256
|
|
|
1233
1257
|
class _TextAnswer:
|
|
@@ -1257,6 +1281,7 @@ class _TextAnswer:
|
|
|
1257
1281
|
def done(self, copy: bool) -> None:
|
|
1258
1282
|
if copy and self._lines and _copy_to_clipboard(self.text):
|
|
1259
1283
|
_notice("✓ copied to clipboard")
|
|
1284
|
+
_progress_output_emitted()
|
|
1260
1285
|
|
|
1261
1286
|
|
|
1262
1287
|
# H-06: Intercept Click-level UsageError (unknown options, bad args) and emit JSON.
|
|
@@ -4825,8 +4850,15 @@ def repo_ir_cmd(
|
|
|
4825
4850
|
size_bytes = len(raw_bytes)
|
|
4826
4851
|
if gzip_output:
|
|
4827
4852
|
import gzip as _gzip
|
|
4853
|
+
# Same durability rule as `_safe_write_file`: the bytes are on disk
|
|
4854
|
+
# before anything says they are (C3-54).
|
|
4828
4855
|
with _gzip.open(output_path, "wb") as _gz:
|
|
4829
4856
|
_gz.write(raw_bytes)
|
|
4857
|
+
_gz.flush()
|
|
4858
|
+
try:
|
|
4859
|
+
os.fsync(_gz.fileno())
|
|
4860
|
+
except (OSError, AttributeError):
|
|
4861
|
+
pass
|
|
4830
4862
|
compressed_kb = output_path.stat().st_size // 1024
|
|
4831
4863
|
size_kb = size_bytes // 1024
|
|
4832
4864
|
# C3-26: when the payload went to a file, this confirmation IS the
|
|
@@ -4837,7 +4869,9 @@ def repo_ir_cmd(
|
|
|
4837
4869
|
f"IR written to {output_path} ({compressed_kb}KB gzip, {size_kb}KB uncompressed)"
|
|
4838
4870
|
)
|
|
4839
4871
|
else:
|
|
4840
|
-
|
|
4872
|
+
# Was a bare `write_bytes`: neither atomic nor durable, and the only
|
|
4873
|
+
# `-o` write in the CLI that skipped the shared seam (C3-54).
|
|
4874
|
+
_safe_write_file(output_path, output)
|
|
4841
4875
|
size_kb = size_bytes // 1024
|
|
4842
4876
|
if summary_only:
|
|
4843
4877
|
typer.echo(
|
|
@@ -4850,6 +4884,7 @@ def repo_ir_cmd(
|
|
|
4850
4884
|
f"IR written to {output_path} "
|
|
4851
4885
|
f"({size_kb}KB, {n_nodes} nodes, {n_edges} edges)"
|
|
4852
4886
|
)
|
|
4887
|
+
_progress_output_emitted()
|
|
4853
4888
|
else:
|
|
4854
4889
|
if gzip_output:
|
|
4855
4890
|
_emit_error_json(
|
|
@@ -7402,7 +7437,14 @@ def spring_audit_cmd(
|
|
|
7402
7437
|
except Exception:
|
|
7403
7438
|
pass
|
|
7404
7439
|
finally:
|
|
7405
|
-
|
|
7440
|
+
# C3-54: `done` must not claim the answer exists before it does. The
|
|
7441
|
+
# field log showed `[ask] done (6.8m)` printed, then the process torn
|
|
7442
|
+
# down before `-o` was written — a false success on 6.8 minutes of
|
|
7443
|
+
# CPU. `stop()` here only clears the spinner (satisfies the standing
|
|
7444
|
+
# rule that a raised error must never print under a live spinner
|
|
7445
|
+
# frame); it prints no completion line. `_prog.finish()` runs below,
|
|
7446
|
+
# after the render AND the write have actually happened.
|
|
7447
|
+
_prog.stop()
|
|
7406
7448
|
if table:
|
|
7407
7449
|
output = _render_spring_audit_table(
|
|
7408
7450
|
combined, rule_filter=rule_filter, top_n=top_n, min_severity=min_severity
|
|
@@ -7415,6 +7457,7 @@ def spring_audit_cmd(
|
|
|
7415
7457
|
_total = combined.summary.get("total_findings", 0)
|
|
7416
7458
|
_emit_command_output(output, output_path, copy,
|
|
7417
7459
|
success_msg=f"Spring audit written to {output_path} ({_total} findings)")
|
|
7460
|
+
_prog.finish()
|
|
7418
7461
|
|
|
7419
7462
|
if ci and combined.findings:
|
|
7420
7463
|
raise typer.Exit(code=1)
|
|
@@ -9453,18 +9496,21 @@ def pr_impact_cmd(
|
|
|
9453
9496
|
# --files accepts three shapes: a list file, '-' for stdin, or an inline
|
|
9454
9497
|
# comma-separated list. The list-file shape was the only documented one and
|
|
9455
9498
|
# the only one the error hint mentioned.
|
|
9456
|
-
|
|
9499
|
+
# Both byte-carrying shapes decode through `text_input`: stdin and list files
|
|
9500
|
+
# arrive as somebody else's shell wrote them, and the locale codepage is not
|
|
9501
|
+
# evidence of their encoding (C3-58).
|
|
9502
|
+
from sourcecode import text_input as _text_input
|
|
9503
|
+
|
|
9504
|
+
_files_raw = str(files).replace(_text_input.BOM_CHAR, "").strip()
|
|
9457
9505
|
if _files_raw == "-":
|
|
9458
|
-
changed_files =
|
|
9506
|
+
changed_files = _text_input.parse_path_lines(_text_input.read_stdin_text())
|
|
9459
9507
|
_files_desc = "stdin"
|
|
9460
9508
|
else:
|
|
9461
9509
|
_files_path = Path(_files_raw)
|
|
9462
9510
|
if _files_path.is_file():
|
|
9463
|
-
changed_files =
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
if line.strip()
|
|
9467
|
-
]
|
|
9511
|
+
changed_files = _text_input.parse_path_lines(
|
|
9512
|
+
_text_input.read_text_file(_files_path)
|
|
9513
|
+
)
|
|
9468
9514
|
_files_desc = str(_files_path)
|
|
9469
9515
|
elif all(
|
|
9470
9516
|
p.strip().lower().endswith(".java")
|
|
@@ -10301,6 +10347,9 @@ def modernize_cmd(
|
|
|
10301
10347
|
sys.stdout.buffer.flush()
|
|
10302
10348
|
except AttributeError:
|
|
10303
10349
|
sys.stdout.write(output + "\n")
|
|
10350
|
+
# This branch predates `_emit_command_output`, so it also has to release the
|
|
10351
|
+
# deferred completion line itself (C3-54).
|
|
10352
|
+
_progress_output_emitted()
|
|
10304
10353
|
|
|
10305
10354
|
if copy:
|
|
10306
10355
|
_copy_to_clipboard(output)
|
|
@@ -11569,12 +11618,33 @@ def mcp_status(copy: bool = _copy_option()) -> None:
|
|
|
11569
11618
|
_answer.say("")
|
|
11570
11619
|
|
|
11571
11620
|
# Stage 1: Dependencies
|
|
11621
|
+
# C3-57: check the symbol this CLI actually consumes (FastMCP), not just
|
|
11622
|
+
# whether the root `mcp` package imports — `mcp>=2.0` imports cleanly but
|
|
11623
|
+
# has no `mcp.server.fastmcp` module, so importing only the root package
|
|
11624
|
+
# reports a green check for a build that will fail on `mcp serve`/`mcp list-tools`.
|
|
11625
|
+
def _installed_mcp_version() -> str:
|
|
11626
|
+
# The `mcp` package does not set `__version__`; read installed
|
|
11627
|
+
# distribution metadata instead, and fall back gracefully off a
|
|
11628
|
+
# normal install (editable installs, vendored copies, etc.).
|
|
11629
|
+
try:
|
|
11630
|
+
from importlib.metadata import version as _dist_version
|
|
11631
|
+
return _dist_version("mcp")
|
|
11632
|
+
except Exception:
|
|
11633
|
+
return "unknown"
|
|
11634
|
+
|
|
11572
11635
|
try:
|
|
11573
|
-
|
|
11574
|
-
|
|
11636
|
+
from mcp.server.fastmcp import FastMCP # noqa: F401
|
|
11637
|
+
_mcp_ver = _installed_mcp_version()
|
|
11638
|
+
_answer.say(f"Dependencies ✓ installed (mcp {_mcp_ver})")
|
|
11575
11639
|
except ImportError:
|
|
11576
|
-
|
|
11577
|
-
|
|
11640
|
+
try:
|
|
11641
|
+
import mcp as _mcp_pkg # noqa: F401
|
|
11642
|
+
_mcp_ver = _installed_mcp_version()
|
|
11643
|
+
_answer.say(f"Dependencies ✗ mcp {_mcp_ver} incompatible (requires mcp>=1.2,<2)")
|
|
11644
|
+
_answer.say(" Fix: pip install 'sourcecode[mcp]' --upgrade")
|
|
11645
|
+
except ImportError:
|
|
11646
|
+
_answer.say("Dependencies ✗ missing")
|
|
11647
|
+
_answer.say(" Fix: pip install sourcecode[mcp]")
|
|
11578
11648
|
_answer.say("")
|
|
11579
11649
|
|
|
11580
11650
|
clients = detect_clients()
|
|
@@ -11789,9 +11859,32 @@ def mcp_list_tools(
|
|
|
11789
11859
|
import asyncio
|
|
11790
11860
|
import json as _json
|
|
11791
11861
|
|
|
11792
|
-
|
|
11862
|
+
# C3-57: this command previously let a raw Python traceback through when
|
|
11863
|
+
# the installed `mcp` package was incompatible (e.g. mcp>=2.0, which has
|
|
11864
|
+
# no mcp.server.fastmcp) — the only command in the catalogue to do so.
|
|
11865
|
+
# Route failures through the same structured error contract as every
|
|
11866
|
+
# other command instead.
|
|
11867
|
+
try:
|
|
11868
|
+
from sourcecode.mcp.server import mcp as _mcp
|
|
11869
|
+
tools = asyncio.run(_mcp.list_tools())
|
|
11870
|
+
except ImportError as exc:
|
|
11871
|
+
_emit_error_json(
|
|
11872
|
+
EXECUTION_FAILED_CODE,
|
|
11873
|
+
f"MCP server dependencies are not importable: {exc}",
|
|
11874
|
+
hint="Run 'ask mcp status' to check the installed mcp package version — "
|
|
11875
|
+
"this CLI requires mcp>=1.2,<2. Fix: pip install 'sourcecode[mcp]' --upgrade",
|
|
11876
|
+
expected="mcp>=1.2,<2 installed with mcp.server.fastmcp.FastMCP importable.",
|
|
11877
|
+
)
|
|
11878
|
+
raise typer.Exit(code=1)
|
|
11879
|
+
except Exception as exc:
|
|
11880
|
+
_emit_error_json(
|
|
11881
|
+
EXECUTION_FAILED_CODE,
|
|
11882
|
+
f"Failed to list MCP tools: {type(exc).__name__}: {exc}",
|
|
11883
|
+
hint="Run 'ask mcp status' to check MCP server health.",
|
|
11884
|
+
expected="A list of registered MCP tools.",
|
|
11885
|
+
)
|
|
11886
|
+
raise typer.Exit(code=1)
|
|
11793
11887
|
|
|
11794
|
-
tools = asyncio.run(_mcp.list_tools())
|
|
11795
11888
|
tools_sorted = sorted(tools, key=lambda t: t.name)
|
|
11796
11889
|
|
|
11797
11890
|
if json_output:
|
|
@@ -12057,6 +12150,11 @@ def cache_model_cmd(
|
|
|
12057
12150
|
path: Path = typer.Argument(Path("."), help="Repository path for CLI consistency (model is global)."),
|
|
12058
12151
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
|
|
12059
12152
|
markdown: bool = typer.Option(False, "--markdown", help="Output the tables published in the user guide."),
|
|
12153
|
+
full: bool = typer.Option(
|
|
12154
|
+
False, "--full",
|
|
12155
|
+
help="Full prose: every command's measured timing and note (~2.5k tokens). "
|
|
12156
|
+
"Default is a compact one-line-per-command summary.",
|
|
12157
|
+
),
|
|
12060
12158
|
copy: bool = _copy_option(),
|
|
12061
12159
|
) -> None:
|
|
12062
12160
|
"""What each cache layer stores, what invalidates it, and what a warm helps.
|
|
@@ -12070,6 +12168,10 @@ def cache_model_cmd(
|
|
|
12070
12168
|
|
|
12071
12169
|
One rule covers invalidation: every layer keys on the exact tree state, so any
|
|
12072
12170
|
change to the analysed files invalidates it, committed or not.
|
|
12171
|
+
|
|
12172
|
+
\b
|
|
12173
|
+
Default output is a compact summary (C3-63) — every command named, without the
|
|
12174
|
+
per-command measured timing and note. Pass --full for that detail.
|
|
12073
12175
|
"""
|
|
12074
12176
|
from sourcecode import cache_model as _cmodel
|
|
12075
12177
|
|
|
@@ -12089,7 +12191,8 @@ def cache_model_cmd(
|
|
|
12089
12191
|
f"Scope: {Path(path).resolve()} (model is global; path accepted for "
|
|
12090
12192
|
"cache command grammar consistency)\n\n"
|
|
12091
12193
|
)
|
|
12092
|
-
|
|
12194
|
+
_body = _cmodel.render_text() if full else _cmodel.render_summary()
|
|
12195
|
+
_emit_text_output(_prefix + _body, copy)
|
|
12093
12196
|
|
|
12094
12197
|
|
|
12095
12198
|
@cache_app.command("freshness", short_help="RIS freshness relative to the current git HEAD")
|
sourcecode/pr_impact.py
CHANGED
|
@@ -228,6 +228,43 @@ def _suffix_candidates(norm: str, file_class_index: dict[str, list[str]]) -> lis
|
|
|
228
228
|
return shorter_given or longer_given
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def _exists_on_disk(norm: str, root: Path) -> bool:
|
|
232
|
+
"""True when the given path names a file that is actually there."""
|
|
233
|
+
try:
|
|
234
|
+
candidate = Path(norm)
|
|
235
|
+
if candidate.is_absolute():
|
|
236
|
+
return candidate.is_file()
|
|
237
|
+
return (root / norm).is_file()
|
|
238
|
+
except OSError:
|
|
239
|
+
# A path the OS refuses to even stat (illegal characters after a bad
|
|
240
|
+
# decode, a name past MAX_PATH) is not on disk as far as we can tell.
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _unresolved_warning(path_str: str, norm: str, root: Path) -> str:
|
|
245
|
+
"""Name the right thing to check for a changed file that mapped to nothing.
|
|
246
|
+
|
|
247
|
+
The two causes need opposite investigations and the single old message sent
|
|
248
|
+
every operator to the wrong one. A path that is *not there* is a path
|
|
249
|
+
problem — the field case was a BOM and cp1252 mojibake reaching us through
|
|
250
|
+
stdin (C3-58), and the message said "not in CIR", so the operator went
|
|
251
|
+
looking at analysis coverage. A path that *is* there carries no class
|
|
252
|
+
symbols, which is a fact about the file.
|
|
253
|
+
"""
|
|
254
|
+
if _exists_on_disk(norm, root):
|
|
255
|
+
return (
|
|
256
|
+
f"No Spring classes found for '{path_str}' — the file exists but "
|
|
257
|
+
"contributes no class symbols to the CIR (interface-free package-info, "
|
|
258
|
+
"an empty file, or a file excluded from analysis)."
|
|
259
|
+
)
|
|
260
|
+
return (
|
|
261
|
+
f"No file at '{path_str}' under the analyzed repository — nothing was "
|
|
262
|
+
"matched, and this is a path problem, not an analysis gap. Check the path "
|
|
263
|
+
"form (relative to the repo root) and its encoding: a byte-order mark or a "
|
|
264
|
+
"non-UTF-8 pipe corrupts accented names."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
231
268
|
def _resolve_changed_files(
|
|
232
269
|
file_list: list[str],
|
|
233
270
|
file_class_index: dict[str, list[str]],
|
|
@@ -293,10 +330,7 @@ def _resolve_changed_files(
|
|
|
293
330
|
|
|
294
331
|
if not candidates:
|
|
295
332
|
unresolved_java.append(norm)
|
|
296
|
-
warnings.append(
|
|
297
|
-
f"No Spring classes found for '{path_str}' — "
|
|
298
|
-
"file not in CIR or has no class symbols."
|
|
299
|
-
)
|
|
333
|
+
warnings.append(_unresolved_warning(path_str, norm, root))
|
|
300
334
|
|
|
301
335
|
for cls in candidates:
|
|
302
336
|
if cls not in seen_classes:
|
sourcecode/progress.py
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
Writes only to stderr. Zero-cost when stderr is not a TTY or CI is detected,
|
|
4
4
|
unless ASK_PROGRESS=1 explicitly requests line heartbeats for supervised jobs.
|
|
5
5
|
Thread-safe; stop() is idempotent.
|
|
6
|
+
|
|
7
|
+
`done (Xs)` is a claim, and C3-54 is what happens when it is made early: the
|
|
8
|
+
field log printed `[ask] done (6.8m)` and the output directory held no
|
|
9
|
+
`f3-audit.json`, because the completion line was emitted when the *analysis*
|
|
10
|
+
finished — before the render and the write — and the process was torn down in
|
|
11
|
+
that window. 6.8 minutes of CPU, zero recoverable bytes, reported as success.
|
|
12
|
+
|
|
13
|
+
So the line is not printed where `finish()` is called. `finish()` claims the
|
|
14
|
+
work is done; the line is emitted when the **answer has actually been handed
|
|
15
|
+
over**, which the CLI signals through `output_emitted()` at its output seams. A
|
|
16
|
+
run that is killed between the two prints nothing, which is the honest outcome:
|
|
17
|
+
no answer, no completion line.
|
|
6
18
|
"""
|
|
7
19
|
from __future__ import annotations
|
|
8
20
|
|
|
@@ -14,6 +26,40 @@ from typing import Optional
|
|
|
14
26
|
|
|
15
27
|
_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
16
28
|
|
|
29
|
+
#: Consecutive failed heartbeat writes before the loop gives up on the stream.
|
|
30
|
+
_MAX_WRITE_FAILURES = 5
|
|
31
|
+
|
|
32
|
+
# --- Completion registry ----------------------------------------------------
|
|
33
|
+
# Module state, not per-instance: the emitter of the answer (the CLI output
|
|
34
|
+
# seam) and the owner of the completion line (a Progress somewhere up the call
|
|
35
|
+
# stack) never see each other.
|
|
36
|
+
_registry_lock = threading.Lock()
|
|
37
|
+
_output_events = 0
|
|
38
|
+
_pending_done: "list[Progress]" = []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def output_emitted() -> None:
|
|
42
|
+
"""The command's answer has been written. Release any deferred `done` line.
|
|
43
|
+
|
|
44
|
+
Called from every CLI output seam. Safe to call when no Progress is running
|
|
45
|
+
and safe to call more than once per run.
|
|
46
|
+
"""
|
|
47
|
+
global _output_events
|
|
48
|
+
with _registry_lock:
|
|
49
|
+
_output_events += 1
|
|
50
|
+
released = list(_pending_done)
|
|
51
|
+
_pending_done.clear()
|
|
52
|
+
for prog in released:
|
|
53
|
+
prog._print_done()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _reset_for_tests() -> None:
|
|
57
|
+
"""Drop registry state between in-process test runs."""
|
|
58
|
+
global _output_events
|
|
59
|
+
with _registry_lock:
|
|
60
|
+
_output_events = 0
|
|
61
|
+
_pending_done.clear()
|
|
62
|
+
|
|
17
63
|
|
|
18
64
|
def _terminal_capable() -> bool:
|
|
19
65
|
if os.environ.get("ASK_PROGRESS", "").lower() in ("1", "true", "yes", "on"):
|
|
@@ -57,6 +103,10 @@ class Progress:
|
|
|
57
103
|
self._stop_event = threading.Event()
|
|
58
104
|
self._thread: Optional[threading.Thread] = None
|
|
59
105
|
self._stopped = False
|
|
106
|
+
self._done_printed = False
|
|
107
|
+
self._outputs_at_start = 0
|
|
108
|
+
self._elapsed_at_finish = 0.0
|
|
109
|
+
self._write_failures = 0
|
|
60
110
|
|
|
61
111
|
@property
|
|
62
112
|
def elapsed(self) -> float:
|
|
@@ -67,6 +117,9 @@ class Progress:
|
|
|
67
117
|
with self._lock:
|
|
68
118
|
self._phase = phase
|
|
69
119
|
self._stopped = False
|
|
120
|
+
self._done_printed = False
|
|
121
|
+
with _registry_lock:
|
|
122
|
+
self._outputs_at_start = _output_events
|
|
70
123
|
if not self._enabled:
|
|
71
124
|
return self
|
|
72
125
|
self._stop_event.clear()
|
|
@@ -95,14 +148,41 @@ class Progress:
|
|
|
95
148
|
return elapsed
|
|
96
149
|
|
|
97
150
|
def finish(self) -> None:
|
|
98
|
-
"""Stop spinner and
|
|
99
|
-
|
|
151
|
+
"""Stop the spinner and claim completion — as soon as that is true.
|
|
152
|
+
|
|
153
|
+
If the command has already handed over its answer (the `spring-audit`
|
|
154
|
+
shape: `finish()` called after the write), the line prints here. If it
|
|
155
|
+
has not, the line waits for `output_emitted()`. It is never printed on a
|
|
156
|
+
run that dies in between, and never on a run that raised: `finally:
|
|
157
|
+
finish()` on an exception path leaves a claim pending that nothing
|
|
158
|
+
releases (C3-54).
|
|
159
|
+
"""
|
|
160
|
+
self._elapsed_at_finish = self.stop()
|
|
100
161
|
if not self._enabled:
|
|
101
162
|
return
|
|
163
|
+
with _registry_lock:
|
|
164
|
+
if _output_events > self._outputs_at_start:
|
|
165
|
+
already_emitted = True
|
|
166
|
+
else:
|
|
167
|
+
already_emitted = False
|
|
168
|
+
if self not in _pending_done:
|
|
169
|
+
_pending_done.append(self)
|
|
170
|
+
if already_emitted:
|
|
171
|
+
self._print_done()
|
|
172
|
+
|
|
173
|
+
def _print_done(self) -> None:
|
|
174
|
+
"""Write the completion line once, for an answer that now exists."""
|
|
175
|
+
if self._done_printed or not self._enabled:
|
|
176
|
+
return
|
|
177
|
+
self._done_printed = True
|
|
178
|
+
elapsed = getattr(self, "_elapsed_at_finish", self.elapsed)
|
|
102
179
|
t = f"{elapsed:.1f}s" if elapsed < 60 else f"{elapsed / 60:.1f}m"
|
|
103
180
|
prefix = "[ask] " if self._line_mode else "✓ "
|
|
104
|
-
|
|
105
|
-
|
|
181
|
+
try:
|
|
182
|
+
sys.stderr.write(f"{prefix}done ({t})\n")
|
|
183
|
+
sys.stderr.flush()
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
106
186
|
|
|
107
187
|
def _loop(self) -> None:
|
|
108
188
|
idx = 0
|
|
@@ -120,6 +200,19 @@ class Progress:
|
|
|
120
200
|
try:
|
|
121
201
|
sys.stderr.write(line)
|
|
122
202
|
sys.stderr.flush()
|
|
123
|
-
|
|
203
|
+
self._write_failures = 0
|
|
204
|
+
except ValueError:
|
|
205
|
+
# The stream is closed — the run is ending. Nothing to retry.
|
|
124
206
|
break
|
|
207
|
+
except Exception:
|
|
208
|
+
# C3-56: a single failed write used to end the heartbeat for the
|
|
209
|
+
# rest of the run, and the operator's only remaining way to tell
|
|
210
|
+
# work from a hang was reading the worker's CPU time. One bad
|
|
211
|
+
# write is not evidence that the next one fails: on Windows a
|
|
212
|
+
# redirected stderr can reject a write transiently. Give up only
|
|
213
|
+
# when the stream refuses repeatedly.
|
|
214
|
+
self._write_failures += 1
|
|
215
|
+
if self._write_failures >= _MAX_WRITE_FAILURES:
|
|
216
|
+
break
|
|
217
|
+
continue
|
|
125
218
|
idx += 1
|
sourcecode/provenance.py
CHANGED
|
@@ -105,8 +105,6 @@ def _claims_from_rows(command: str, payload: dict[str, Any]) -> list[dict[str, A
|
|
|
105
105
|
if row.get("rule_ids"):
|
|
106
106
|
claim["rule_ids"] = row["rule_ids"]
|
|
107
107
|
claims.append(claim)
|
|
108
|
-
if len(claims) >= _MAX_CLAIMS:
|
|
109
|
-
return claims
|
|
110
108
|
return claims
|
|
111
109
|
|
|
112
110
|
|
|
@@ -166,9 +164,16 @@ def build_evidence_manifest(
|
|
|
166
164
|
) -> dict[str, Any]:
|
|
167
165
|
"""Build a machine-readable provenance index for an existing payload."""
|
|
168
166
|
root = Path(root).resolve()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
# I-2: measure the whole population, THEN cut it. `_claims_from_rows` used to
|
|
168
|
+
# return the moment it reached the cap, so the number of claims the payload
|
|
169
|
+
# actually carries was never computed and could not be published — the manifest
|
|
170
|
+
# reported the cap itself as `total_indexed`, which is the exact shape ("the
|
|
171
|
+
# published count is the size of the rendering") this module exists to make
|
|
172
|
+
# auditable. Building every claim is arithmetic over rows already in memory.
|
|
173
|
+
all_claims = _claims_from_rows(command, payload)
|
|
174
|
+
all_claims.extend(_claims_from_java_lts(command, payload))
|
|
175
|
+
total_claims = len(all_claims)
|
|
176
|
+
claims = all_claims[:_MAX_CLAIMS]
|
|
172
177
|
sources = sorted({
|
|
173
178
|
str((claim.get("source") or {}).get("path"))
|
|
174
179
|
for claim in claims
|
|
@@ -189,11 +194,14 @@ def build_evidence_manifest(
|
|
|
189
194
|
"run": run,
|
|
190
195
|
"claims": claims,
|
|
191
196
|
"sources": [{"path": path} for path in sources],
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
# `total` is the measurement; `total_indexed` is the size of this manifest.
|
|
198
|
+
# `capped` is strictly `total > cap`: a payload with exactly `_MAX_CLAIMS`
|
|
199
|
+
# claims had nothing removed and must not report that it did.
|
|
200
|
+
"claims_cap": {
|
|
201
|
+
"total": total_claims,
|
|
202
|
+
"total_indexed": min(total_claims, _MAX_CLAIMS),
|
|
203
|
+
"capped": total_claims > _MAX_CLAIMS,
|
|
204
|
+
},
|
|
197
205
|
"embedded_manifests": _embedded_manifests(payload),
|
|
198
206
|
"limits": {
|
|
199
207
|
"statement": (
|
sourcecode/release_info.py
CHANGED
|
@@ -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-
|
|
30
|
+
RELEASE_DATE = "2026-08-07"
|
|
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
|
sourcecode/repository_ir.py
CHANGED
|
@@ -21,7 +21,7 @@ import subprocess
|
|
|
21
21
|
from collections import deque
|
|
22
22
|
from dataclasses import dataclass, field
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Any, Iterable, Optional
|
|
24
|
+
from typing import Any, Iterable, Iterator, Optional
|
|
25
25
|
|
|
26
26
|
from sourcecode.caller_metrics import CALLER_METRIC_RECONCILIATION, caller_classes
|
|
27
27
|
from sourcecode.degradation import cap_effect as _cap_effect
|
|
@@ -5634,6 +5634,43 @@ _VENDOR_DIRS: frozenset[str] = frozenset({
|
|
|
5634
5634
|
"generated-resources",
|
|
5635
5635
|
})
|
|
5636
5636
|
|
|
5637
|
+
#: Metadata trees that hold no repository source at all. Kept apart from
|
|
5638
|
+
#: `_VENDOR_DIRS` because that set answers "is this file part of the product";
|
|
5639
|
+
#: this one answers "is there anything here worth opening". `.git` alone holds
|
|
5640
|
+
#: 5 542 loose objects in this repository, and every unpruned `rglob` visits
|
|
5641
|
+
#: every one of them.
|
|
5642
|
+
_METADATA_DIRS: frozenset[str] = frozenset({
|
|
5643
|
+
".git", ".hg", ".svn", ".idea", ".vscode", "__pycache__",
|
|
5644
|
+
})
|
|
5645
|
+
|
|
5646
|
+
#: One ignore set for every tree walk that is looking for source (C3-64). A
|
|
5647
|
+
#: second, hand-kept list beside this one is how two surfaces come to disagree
|
|
5648
|
+
#: about what counts as source.
|
|
5649
|
+
PRUNED_TREE_DIRS: frozenset[str] = _VENDOR_DIRS | _METADATA_DIRS
|
|
5650
|
+
|
|
5651
|
+
|
|
5652
|
+
def walk_source_tree(
|
|
5653
|
+
root: Path, *, prune: frozenset[str] = PRUNED_TREE_DIRS,
|
|
5654
|
+
) -> "Iterator[Path]":
|
|
5655
|
+
"""Yield every file under `root`, never descending into a pruned directory.
|
|
5656
|
+
|
|
5657
|
+
`Path.rglob` has no way to skip a subtree: it walks the build output and the
|
|
5658
|
+
VCS metadata and then filters by name, so the cost of a scan is set by what
|
|
5659
|
+
the build wrote rather than by what the developer did. Measured on
|
|
5660
|
+
openmrs-core, 47 % of all tree entries live under directories in `prune`
|
|
5661
|
+
(C3-64).
|
|
5662
|
+
|
|
5663
|
+
Yields absolute paths, and never raises: an unreadable directory is skipped
|
|
5664
|
+
the same way the previous `rglob` calls swallowed `OSError`.
|
|
5665
|
+
"""
|
|
5666
|
+
import os
|
|
5667
|
+
|
|
5668
|
+
for dirpath, dirnames, filenames in os.walk(root, onerror=lambda _e: None):
|
|
5669
|
+
dirnames[:] = [d for d in dirnames if d not in prune]
|
|
5670
|
+
base = Path(dirpath)
|
|
5671
|
+
for name in filenames:
|
|
5672
|
+
yield base / name
|
|
5673
|
+
|
|
5637
5674
|
|
|
5638
5675
|
def apply_ir_size_limits(
|
|
5639
5676
|
ir: dict,
|
|
@@ -609,13 +609,36 @@ def scan_security_configuration(
|
|
|
609
609
|
out.extend(_scan_commented_controls(source, rel, xml=False))
|
|
610
610
|
out.extend(_scan_java(blank_java_comments(source), rel))
|
|
611
611
|
|
|
612
|
+
# C3-64 — one pruned walk for all three populations. This used to be five
|
|
613
|
+
# `rglob` calls (one per config suffix, one for descriptors, one for
|
|
614
|
+
# mappers), none of which could skip a subtree: the cost was set by the size
|
|
615
|
+
# of `target/`, `node_modules/` and `.git` rather than by the source. The
|
|
616
|
+
# ignore set is the walker's own (`repository_ir.PRUNED_TREE_DIRS`), so this
|
|
617
|
+
# surface and Java discovery cannot disagree about what is source.
|
|
618
|
+
from sourcecode.repository_ir import walk_source_tree
|
|
619
|
+
|
|
620
|
+
_walked_config: "list[str]" = []
|
|
621
|
+
descriptors: "list[str]" = []
|
|
622
|
+
mappers: "list[str]" = []
|
|
623
|
+
for path in walk_source_tree(root):
|
|
624
|
+
name = path.name
|
|
625
|
+
lower = name.lower()
|
|
626
|
+
try:
|
|
627
|
+
rel_posix = path.relative_to(root).as_posix()
|
|
628
|
+
except ValueError:
|
|
629
|
+
continue
|
|
630
|
+
if name.startswith("application") and name.endswith(_CONFIG_SUFFIXES):
|
|
631
|
+
_walked_config.append(rel_posix)
|
|
632
|
+
elif lower.endswith(".xml"):
|
|
633
|
+
# A descriptor that also ends in `Mapper.xml` stays a descriptor —
|
|
634
|
+
# the old code took the same side by subtracting one set from the other.
|
|
635
|
+
if lower in _DESCRIPTOR_NAMES or lower.endswith(_DESCRIPTOR_SUFFIXES):
|
|
636
|
+
descriptors.append(rel_posix)
|
|
637
|
+
elif name.endswith("Mapper.xml"):
|
|
638
|
+
mappers.append(rel_posix)
|
|
639
|
+
|
|
612
640
|
if config_files is None:
|
|
613
|
-
config_files =
|
|
614
|
-
for suffix in _CONFIG_SUFFIXES:
|
|
615
|
-
config_files += [
|
|
616
|
-
p.relative_to(root).as_posix()
|
|
617
|
-
for p in root.rglob(f"application*{suffix}")
|
|
618
|
-
]
|
|
641
|
+
config_files = _walked_config
|
|
619
642
|
for rel in sorted(set(config_files)):
|
|
620
643
|
try:
|
|
621
644
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
@@ -624,18 +647,10 @@ def scan_security_configuration(
|
|
|
624
647
|
out.extend(_scan_commented_config_controls(text, rel))
|
|
625
648
|
out.extend(_scan_config(blank_hash_comments(text), rel))
|
|
626
649
|
|
|
627
|
-
# CL-10 — the descriptors.
|
|
650
|
+
# CL-10 — the descriptors. Classified the same way `environment_resolution`
|
|
628
651
|
# classifies them, so the two surfaces cannot disagree about which files are
|
|
629
652
|
# deployment descriptors, and a per-environment overlay tree is covered
|
|
630
653
|
# because the walk finds the file wherever the build put it.
|
|
631
|
-
descriptors: "list[str]" = []
|
|
632
|
-
try:
|
|
633
|
-
for path in root.rglob("*.xml"):
|
|
634
|
-
name = path.name.lower()
|
|
635
|
-
if name in _DESCRIPTOR_NAMES or name.endswith(_DESCRIPTOR_SUFFIXES):
|
|
636
|
-
descriptors.append(path.relative_to(root).as_posix())
|
|
637
|
-
except OSError:
|
|
638
|
-
pass
|
|
639
654
|
for rel in sorted(set(descriptors)):
|
|
640
655
|
try:
|
|
641
656
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
@@ -644,13 +659,6 @@ def scan_security_configuration(
|
|
|
644
659
|
out.extend(_scan_commented_controls(text, rel, xml=True))
|
|
645
660
|
out.extend(_scan_descriptor(blank_xml_comments(text), rel))
|
|
646
661
|
|
|
647
|
-
mappers: "list[str]" = []
|
|
648
|
-
try:
|
|
649
|
-
for path in root.rglob("*.xml"):
|
|
650
|
-
if path.name.endswith("Mapper.xml"):
|
|
651
|
-
mappers.append(path.relative_to(root).as_posix())
|
|
652
|
-
except OSError:
|
|
653
|
-
pass
|
|
654
662
|
for rel in sorted(set(mappers) - set(descriptors)):
|
|
655
663
|
try:
|
|
656
664
|
text = (root / rel).read_text(encoding="utf-8", errors="replace")
|
|
@@ -621,6 +621,14 @@ class _GATE00XCustomGateAudit:
|
|
|
621
621
|
findings: list[SpringFinding] = []
|
|
622
622
|
ann_re = _annotation_call_re(token)
|
|
623
623
|
for rel, source in sources:
|
|
624
|
+
# C3-61: `ann_re` is `@<token>\b…`, so it cannot match a source that does
|
|
625
|
+
# not contain the literal token, and neither can any prefix slice of it.
|
|
626
|
+
# Without this line every source pays a full method `finditer` plus one
|
|
627
|
+
# prefix regex per method before proving it had nothing to find — 98,9 %
|
|
628
|
+
# of (source, token) pairs on the measured repository. Same guard
|
|
629
|
+
# `_advice_body_rules` already carries.
|
|
630
|
+
if token not in source:
|
|
631
|
+
continue
|
|
624
632
|
class_fqn = _java_fqn(source)
|
|
625
633
|
annotated_methods: dict[str, tuple[int, str]] = {}
|
|
626
634
|
for method in _JAVA_METHOD_RE.finditer(source):
|
|
@@ -712,6 +720,8 @@ class _GATE00XCustomGateAudit:
|
|
|
712
720
|
findings: list[SpringFinding] = []
|
|
713
721
|
ann_re = _annotation_call_re(token)
|
|
714
722
|
for rel, source in sources:
|
|
723
|
+
if token not in source: # C3-61, same equivalence as `_proxy_bypass`
|
|
724
|
+
continue
|
|
715
725
|
class_fqn = _java_fqn(source)
|
|
716
726
|
for ann in ann_re.finditer(source):
|
|
717
727
|
args = (ann.group("args") or "").strip()
|
sourcecode/text_input.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""text_input.py — one authority for decoding text that arrives from outside.
|
|
2
|
+
|
|
3
|
+
Every path list this CLI accepts (stdin, a list file) is bytes written by
|
|
4
|
+
somebody else's shell. Python decodes stdin with the *locale* encoding, which on
|
|
5
|
+
Windows is the ANSI codepage (cp1252), not UTF-8 — so a path piped from
|
|
6
|
+
PowerShell came back mojibake and resolved to nothing:
|
|
7
|
+
|
|
8
|
+
Año…java -> Año…java (UTF-8 bytes read as cp1252)
|
|
9
|
+
src/… -> src/… (the UTF-8 BOM read as three cp1252 chars)
|
|
10
|
+
|
|
11
|
+
Both forms silently produced an empty blast radius on `pr-impact --files -`,
|
|
12
|
+
which is the gating command: `CRITICAL` became a green pipeline (C3-58).
|
|
13
|
+
|
|
14
|
+
The rule here is that the *bytes* decide the encoding, never the platform
|
|
15
|
+
locale: a BOM is authoritative when present, UTF-8 otherwise. A byte sequence
|
|
16
|
+
that is not valid UTF-8 (a genuinely cp1252-encoded list) decodes with
|
|
17
|
+
replacement characters rather than raising — the path then fails to resolve and
|
|
18
|
+
the consumer's diagnostic names encoding as a cause, which beats a traceback.
|
|
19
|
+
|
|
20
|
+
Pure module: no I/O policy, no CLI imports.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
#: Byte-order marks, longest first — UTF-32-LE starts with the UTF-16-LE mark.
|
|
28
|
+
#: The codec named here is the one that *consumes* the mark: `utf-16-le` leaves a
|
|
29
|
+
#: U+FEFF at the head of the text, `utf-16` reads the mark and drops it.
|
|
30
|
+
_BOMS: tuple[tuple[bytes, str], ...] = (
|
|
31
|
+
(b"\x00\x00\xfe\xff", "utf-32"),
|
|
32
|
+
(b"\xff\xfe\x00\x00", "utf-32"),
|
|
33
|
+
(b"\xef\xbb\xbf", "utf-8-sig"),
|
|
34
|
+
(b"\xfe\xff", "utf-16"),
|
|
35
|
+
(b"\xff\xfe", "utf-16"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
#: The BOM as a character. `utf-16`/`utf-32` decoding consumes the mark, but a
|
|
39
|
+
#: BOM in the *middle* of a stream (PowerShell writing one per redirection) and
|
|
40
|
+
#: a stream already handed to us as `str` both leave it in the text.
|
|
41
|
+
BOM_CHAR = ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sniff_encoding(data: bytes) -> str:
|
|
45
|
+
"""Return the encoding named by `data`'s byte-order mark, else `utf-8`."""
|
|
46
|
+
for mark, encoding in _BOMS:
|
|
47
|
+
if data.startswith(mark):
|
|
48
|
+
return encoding
|
|
49
|
+
return "utf-8"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decode_bytes(data: bytes) -> str:
|
|
53
|
+
"""Decode externally supplied bytes. BOM decides; UTF-8 is the default.
|
|
54
|
+
|
|
55
|
+
Never raises: undecodable bytes become U+FFFD so the caller can report an
|
|
56
|
+
unusable path instead of dying on it.
|
|
57
|
+
"""
|
|
58
|
+
return data.decode(sniff_encoding(data), errors="replace")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def read_stdin_text() -> str:
|
|
62
|
+
"""Read all of stdin as text, decoded by the bytes rather than the locale.
|
|
63
|
+
|
|
64
|
+
Falls back to `sys.stdin.read()` when the stream has no binary buffer —
|
|
65
|
+
Click's `CliRunner` and test doubles replace `sys.stdin` with a `StringIO`.
|
|
66
|
+
"""
|
|
67
|
+
buffer = getattr(sys.stdin, "buffer", None)
|
|
68
|
+
if buffer is None:
|
|
69
|
+
return sys.stdin.read()
|
|
70
|
+
return decode_bytes(buffer.read())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_text_file(path: Path) -> str:
|
|
74
|
+
"""Read a file supplied by the user as text, BOM-aware.
|
|
75
|
+
|
|
76
|
+
`Path.read_text(encoding="utf-8")` raised on the file PowerShell 5.1 writes
|
|
77
|
+
by default: `git diff --name-only > changed.txt` there is UTF-16-LE.
|
|
78
|
+
"""
|
|
79
|
+
return decode_bytes(path.read_bytes())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_path_lines(text: str) -> list[str]:
|
|
83
|
+
"""Split a path listing into clean entries.
|
|
84
|
+
|
|
85
|
+
Strips the BOM character wherever it survived decoding, normalises CRLF, and
|
|
86
|
+
drops blank lines. Order is preserved; duplicates are the caller's business.
|
|
87
|
+
"""
|
|
88
|
+
entries: list[str] = []
|
|
89
|
+
for line in text.splitlines():
|
|
90
|
+
cleaned = line.replace(BOM_CHAR, "").strip()
|
|
91
|
+
if cleaned:
|
|
92
|
+
entries.append(cleaned)
|
|
93
|
+
return entries
|
sourcecode/verify_edit.py
CHANGED
|
@@ -685,6 +685,37 @@ def verdict(hvw: HeadVsWorking, root: Path) -> VerifyVerdict:
|
|
|
685
685
|
exit code: 0=pass, 1=break (a blocking axis proved a regression), 2=unverified
|
|
686
686
|
(a blocking axis could not be computed — honest, never a silent pass)."""
|
|
687
687
|
root = _resolve_repo_root(Path(root))
|
|
688
|
+
|
|
689
|
+
# C3-60: an empty change set has one correct answer and it needs none of the
|
|
690
|
+
# six full-repository analyzers below. `working_cir_reused` is only True when
|
|
691
|
+
# `head_vs_working` positively confirmed nothing changed (C3-32's
|
|
692
|
+
# `git_answered and not all_changed`) — never a guess — in which case
|
|
693
|
+
# `cir_working IS cir_head` (the same object) and `changed_files` /
|
|
694
|
+
# `changed_build_files` are both empty by construction. Every axis diffs
|
|
695
|
+
# cir_head against cir_working, so diffing an object against itself can only
|
|
696
|
+
# ever prove "no change"; running the tx/security audits (and everything
|
|
697
|
+
# else) twice over the whole tree to demonstrate that took 8+ minutes on a
|
|
698
|
+
# clean 3 342-file monolith while `cache freshness` already had the answer
|
|
699
|
+
# in under a second. Short-circuit to the answer every axis would compute.
|
|
700
|
+
if hvw.working_cir_reused:
|
|
701
|
+
clean_axes = {
|
|
702
|
+
name: AxisResult(False)
|
|
703
|
+
for name in (
|
|
704
|
+
"contract_broken", "tx_boundary_changed", "security_delta",
|
|
705
|
+
"new_orphan_bean", "custom_rule_violated", "build_config_changed",
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
return VerifyVerdict(
|
|
709
|
+
verdict="pass",
|
|
710
|
+
axes=clean_axes,
|
|
711
|
+
changed_files=hvw.changed_files,
|
|
712
|
+
changed_build_files=hvw.changed_build_files,
|
|
713
|
+
blast_radius={"affected_entry_points": 0, "confidence": "high", "note": "no changed files"},
|
|
714
|
+
reasons=(),
|
|
715
|
+
exit_code=0,
|
|
716
|
+
head_sha=hvw.head_sha,
|
|
717
|
+
)
|
|
718
|
+
|
|
688
719
|
axes: dict[str, AxisResult] = {
|
|
689
720
|
"contract_broken": _contract_axis(hvw.cir_head, hvw.cir_working),
|
|
690
721
|
"tx_boundary_changed": _tx_axis(hvw.cir_head, hvw.cir_working, root),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 4.10.
|
|
3
|
+
Version: 4.10.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
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
18
18
|
Classifier: Topic :: Utilities
|
|
19
19
|
Requires-Python: >=3.9
|
|
20
20
|
Requires-Dist: defusedxml>=0.7
|
|
21
|
-
Requires-Dist: mcp
|
|
21
|
+
Requires-Dist: mcp<2,>=1.2
|
|
22
22
|
Requires-Dist: pathspec>=1.0
|
|
23
23
|
Requires-Dist: ruamel-yaml>=0.18
|
|
24
24
|
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
@@ -28,12 +28,12 @@ Requires-Dist: tree-sitter-javascript>=0.21; extra == 'ast'
|
|
|
28
28
|
Requires-Dist: tree-sitter-typescript>=0.21; extra == 'ast'
|
|
29
29
|
Requires-Dist: tree-sitter>=0.21; extra == 'ast'
|
|
30
30
|
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: mcp
|
|
31
|
+
Requires-Dist: mcp<2,>=1.2; extra == 'dev'
|
|
32
32
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
33
33
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
34
34
|
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
35
35
|
Provides-Extra: mcp
|
|
36
|
-
Requires-Dist: mcp
|
|
36
|
+
Requires-Dist: mcp<2,>=1.2; extra == 'mcp'
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
39
|
# ASK Engine
|
|
@@ -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
|
-

|
|
46
46
|

|
|
47
47
|
|
|
48
48
|
> **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
|
|
@@ -106,7 +106,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
|
|
|
106
106
|
# pip / pipx
|
|
107
107
|
pipx install sourcecode # or: pip install sourcecode
|
|
108
108
|
|
|
109
|
-
ask version # ask 4.10.
|
|
109
|
+
ask version # ask 4.10.5 — and, on a build that has aged,
|
|
110
110
|
# how many releases have probably shipped since
|
|
111
111
|
```
|
|
112
112
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=N-KHrUXD6ch30-2odxw2VCquK5xEfMXbG2ZoDVLuWQU,309
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
|
|
3
3
|
sourcecode/archetype.py,sha256=CZvRLpkHot_D8D3JFQVorr-EHDJyh0BbS7RnSTqBigM,40499
|
|
4
|
-
sourcecode/architectural_baseline.py,sha256=
|
|
4
|
+
sourcecode/architectural_baseline.py,sha256=ihbYyVgR9MJ2RBuEfayY3rWfubS27MT7m0JPc2NpSWU,20087
|
|
5
5
|
sourcecode/architectural_delta.py,sha256=E6MkyjWl-1ZgR4KSKnI785gUAfc8nq-lK-jd_rupQBU,12496
|
|
6
6
|
sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
|
|
7
7
|
sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
|
|
@@ -9,7 +9,7 @@ sourcecode/ast_extractor.py,sha256=aXJjZ7XjAdxa99hWNm4SyzPqx4gNWiTbUYjNWGNl-Vc,5
|
|
|
9
9
|
sourcecode/audit_report.py,sha256=SwFTJAk76Rew9YWpE3s8sHpi6VY-TN2nlLTCdjpUzDk,6031
|
|
10
10
|
sourcecode/baseline_autocapture.py,sha256=tmMLexQSeaQRRbqxAd7walvGQqnF0mdpxQmkL40rlhI,15623
|
|
11
11
|
sourcecode/cache.py,sha256=9XJUNSGzqM6DNWy4erUO-OcZY9qkrTCCcOJ-OANVrw8,41409
|
|
12
|
-
sourcecode/cache_model.py,sha256=
|
|
12
|
+
sourcecode/cache_model.py,sha256=fFxq5bmDkqmQ-Bcb5TEyCWDb5VO8UGpd_FV3CsYDlvs,19635
|
|
13
13
|
sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
|
|
14
14
|
sourcecode/caller_metrics.py,sha256=--sFGDnIog_YGu9xZHMcB91F5xZakfaAKvy15xU54hg,7904
|
|
15
15
|
sourcecode/caller_reach.py,sha256=RRF49tv4-QswraJ2vsZ89VAMOj7BmYXlAdXr-tLP_CA,9483
|
|
@@ -18,7 +18,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
|
|
|
18
18
|
sourcecode/change_plan.py,sha256=kFjjp16XYbupgkv1CPkfqo39_SiZPMRQ8OOfC-Vy9eg,7929
|
|
19
19
|
sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
|
|
20
20
|
sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
|
|
21
|
-
sourcecode/cli.py,sha256=
|
|
21
|
+
sourcecode/cli.py,sha256=yUCVp8xLvSIieX417jnIPJq-Tc_W5ThDle6V3El_Gxk,530346
|
|
22
22
|
sourcecode/client_calls.py,sha256=daRTgbXNUOfkzGXJpVb6A737R_Thhka8vw1_YgxCaLg,13548
|
|
23
23
|
sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
|
|
24
24
|
sourcecode/compare.py,sha256=xq3zsqAOAw4AWkoD9khb9xDP_O3KvwqO9k-pf8sbi3g,10951
|
|
@@ -79,21 +79,21 @@ sourcecode/perf.py,sha256=2YlJTXjyur1S7hecOMz_joUHAXE5bkcZ85a_LXBEcHQ,13931
|
|
|
79
79
|
sourcecode/pipe_contract.py,sha256=PML0Er5d8uDyec4OrdUXuyqHbGPUYndjeaKUjkFz2u4,8369
|
|
80
80
|
sourcecode/posture.py,sha256=VWnUjqmWp4Koa43dzhonaSvAgMqbOwqaMKCg4ITbgIc,76194
|
|
81
81
|
sourcecode/pr_comment_renderer.py,sha256=239PmJdf95av_ZW236C7_tvq_ahECeuF9AZxrheJ4OQ,15573
|
|
82
|
-
sourcecode/pr_impact.py,sha256=
|
|
82
|
+
sourcecode/pr_impact.py,sha256=b315e2ZCRihuw23EXxHIJll1hDs7M2CZTczrZwAxGFQ,24044
|
|
83
83
|
sourcecode/prepare_context.py,sha256=B8TiBQbazxUOz6D37Dt9zqbOtD5b2S_fR3MB15P9DrI,237206
|
|
84
|
-
sourcecode/progress.py,sha256=
|
|
85
|
-
sourcecode/provenance.py,sha256=
|
|
84
|
+
sourcecode/progress.py,sha256=In9yXx-WOXLVkj_jxu_ae2_4bABHnhj4nWg4KvetfHM,7921
|
|
85
|
+
sourcecode/provenance.py,sha256=gLbsLv0gze7k7ChM4i5M3MwKfFPzs8wzM72rUd2C5sM,7342
|
|
86
86
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
87
87
|
sourcecode/readiness_timeline.py,sha256=qqc14vL9nj9E4qw4bVZpGhfMLaVaYr9s6Pak_VUIwt0,8008
|
|
88
88
|
sourcecode/reconciliation.py,sha256=GU-1PTcVr8zcbtC7BASfpHcZndP9AdboXPNQiBc0fzo,34251
|
|
89
89
|
sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
|
|
90
90
|
sourcecode/reference_facts.py,sha256=Ns495c6eTmq2SrqPkcUrJUJru4_wj_yRWm4YDeJwves,13440
|
|
91
|
-
sourcecode/release_info.py,sha256=
|
|
91
|
+
sourcecode/release_info.py,sha256=6FePTGL0ZSSV_KCYfMUlwT10m7uR0JelH6y1Y-0dPqk,5378
|
|
92
92
|
sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
|
|
93
93
|
sourcecode/remedies.py,sha256=8lgvKkW1WuBG-QfTnA0uBhEKnwWGSvgSMWCfTjzYhEY,6516
|
|
94
94
|
sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
|
|
95
95
|
sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
|
|
96
|
-
sourcecode/repository_ir.py,sha256=
|
|
96
|
+
sourcecode/repository_ir.py,sha256=zCQAZ-SfkbXcgNJJGX5Tv-Z5c2DFTE4GoyzAUitBx48,359936
|
|
97
97
|
sourcecode/ris.py,sha256=SjjDNMcc9Zr9us9G8ikBVkoPXKK3BpM-dBodSGMh_g4,24783
|
|
98
98
|
sourcecode/risk.py,sha256=UgTVdMc8XBArO6D9e2Xx9I-kKqx-PW4W0hSe1Jy9qjU,54491
|
|
99
99
|
sourcecode/rule_catalog.py,sha256=xmphuULUCtg_GWTbriwY25D-N2IaeMXXvB5CNU1xSuA,4925
|
|
@@ -103,7 +103,7 @@ sourcecode/sarif.py,sha256=3hQEegUxIZbojFdY59oB-yKsK-rnafHdTNmYsEP2--A,25984
|
|
|
103
103
|
sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
|
|
104
104
|
sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
|
|
105
105
|
sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
|
|
106
|
-
sourcecode/security_config_scan.py,sha256=
|
|
106
|
+
sourcecode/security_config_scan.py,sha256=m146K51cA9g9wPRwloLSalHAk0qaEybGj_h0bSO4acQ,27916
|
|
107
107
|
sourcecode/security_posture.py,sha256=CjJ2Qm87HhKXepnaWx1ncJLW-SxI18udCmUDIkCyAsY,50270
|
|
108
108
|
sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
|
|
109
109
|
sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
|
|
@@ -118,19 +118,20 @@ sourcecode/spring_impact.py,sha256=keWoE1tN5B_SzJx9F-53RKEL0b2edhJdZMTfocmJZLY,8
|
|
|
118
118
|
sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
|
|
119
119
|
sourcecode/spring_profiles.py,sha256=-kwrCK0O-MRjrCp6SA1d31t--o4Tg87iqPVrTwxoIUs,19605
|
|
120
120
|
sourcecode/spring_properties.py,sha256=kPTk5qAJxdbHc1hclhHAP0O-hCIzBn8PTh3lzl-I4a4,8400
|
|
121
|
-
sourcecode/spring_security_audit.py,sha256=
|
|
121
|
+
sourcecode/spring_security_audit.py,sha256=8IFHdouvurxyK2n60PPxflhJjJ9u-hl8EC7iFns2ST0,46899
|
|
122
122
|
sourcecode/spring_semantic.py,sha256=UrmLg_4gmBVmMqvSnUMgSReVEERVGv3X7LYDBEs6k_o,16598
|
|
123
123
|
sourcecode/spring_tx_analyzer.py,sha256=_wqjRktqdPS6PiXXxtTkCD1t6BaE2uhqx6ioYY-ULAw,44153
|
|
124
124
|
sourcecode/summarizer.py,sha256=0aD4x3vgPngqBCEBKGuES1J2Vk5f7mqCm_ZWErwm3js,27025
|
|
125
125
|
sourcecode/target_admission.py,sha256=wFZ4pzlxhiF6Q6s2lEAZEzcCfj1Y6xNvujjt8MdO0Qo,7154
|
|
126
126
|
sourcecode/test_gap_ranking.py,sha256=hl-tTyQUXZGJycG1b6npbLeE_DaVHaOsvPwG9qqC5y8,15711
|
|
127
127
|
sourcecode/test_sources.py,sha256=6qXI47rVt_DFEbziFUyU-_KIUIGt8YQ5Uf4-sdiAhKU,9277
|
|
128
|
+
sourcecode/text_input.py,sha256=3pL8DxR_DMI3ece-B70DnppvrEaLh4R17Pp8dK9G_hc,3596
|
|
128
129
|
sourcecode/token_estimate.py,sha256=G-ivzIJfVq5TLQN04dC0WK4LZJjWVYL54Ebcxe4fJ7M,9268
|
|
129
130
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
130
131
|
sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
|
|
131
132
|
sourcecode/validation_inference.py,sha256=-oWJqE6PqkqcZbCFDJeWCQHgI9Dbuv5ggJWD3LE_2IU,20817
|
|
132
133
|
sourcecode/validation_surface.py,sha256=jYL-hkDjaaRKkAt7ZUcbxm3Dydl8o6KVRhTkuPXKPVc,31460
|
|
133
|
-
sourcecode/verify_edit.py,sha256=
|
|
134
|
+
sourcecode/verify_edit.py,sha256=IN-cmLxz-gIosbw6jfxKLDE7d5CdXocNAFkEnln806c,37499
|
|
134
135
|
sourcecode/verify_repo.py,sha256=ltSP1nFc726k2aTHCu5dfhxQgxQwz3nrUNDMDN-aDa8,12158
|
|
135
136
|
sourcecode/verify_rules.py,sha256=YxG5JkDXBVpwbU1_tIKVtU8_eaF3zjpsq05tjpN4Z7Y,19401
|
|
136
137
|
sourcecode/version_check.py,sha256=CHp6ZxTIfo8kyHPCBgJA1uFC0xQCoXMuuOfrW8QTL8o,4942
|
|
@@ -193,8 +194,8 @@ sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8
|
|
|
193
194
|
sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
|
|
194
195
|
sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
|
|
195
196
|
sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
|
|
196
|
-
sourcecode-4.10.
|
|
197
|
-
sourcecode-4.10.
|
|
198
|
-
sourcecode-4.10.
|
|
199
|
-
sourcecode-4.10.
|
|
200
|
-
sourcecode-4.10.
|
|
197
|
+
sourcecode-4.10.5.dist-info/METADATA,sha256=YVaP0d14yBclh0t3bccUj_lbJOuaI7cFJbWzotlts7g,36660
|
|
198
|
+
sourcecode-4.10.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
199
|
+
sourcecode-4.10.5.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
|
|
200
|
+
sourcecode-4.10.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
201
|
+
sourcecode-4.10.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|