sourcecode 4.12.0__py3-none-any.whl → 4.14.0__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 +5 -0
- sourcecode/cache_model.py +53 -1
- sourcecode/cli.py +203 -4
- sourcecode/parallel.py +241 -0
- sourcecode/parse_cache.py +183 -1
- sourcecode/perf.py +268 -4
- sourcecode/progress.py +99 -46
- sourcecode/readiness_timeline.py +2 -0
- sourcecode/readonly.py +171 -0
- sourcecode/release_info.py +1 -1
- sourcecode/repository_ir.py +24 -1
- sourcecode/rule_pass.py +124 -0
- sourcecode/runs.py +3 -0
- sourcecode/security_config_scan.py +36 -5
- sourcecode/spring_findings.py +82 -0
- sourcecode/spring_security_audit.py +103 -13
- sourcecode/spring_tx_analyzer.py +82 -14
- sourcecode/verify_edit.py +3 -0
- sourcecode/verify_repo.py +3 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.14.0.dist-info}/METADATA +3 -3
- {sourcecode-4.12.0.dist-info → sourcecode-4.14.0.dist-info}/RECORD +25 -23
- {sourcecode-4.12.0.dist-info → sourcecode-4.14.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.14.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.14.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -37,6 +37,7 @@ from pathlib import Path
|
|
|
37
37
|
from typing import TYPE_CHECKING, Any
|
|
38
38
|
|
|
39
39
|
from sourcecode import __version__ as TOOL_VERSION
|
|
40
|
+
from sourcecode import readonly as _readonly
|
|
40
41
|
from sourcecode.architectural_delta import ArchMetrics, diff_metrics, extract_metrics
|
|
41
42
|
from sourcecode.perf import collect_env
|
|
42
43
|
|
|
@@ -412,6 +413,9 @@ def protect_ask_dir(out_dir: Path, *, announce: bool = True) -> None:
|
|
|
412
413
|
marker = ask_dir / ".gitignore"
|
|
413
414
|
if not marker.exists():
|
|
414
415
|
created = not ask_dir.exists()
|
|
416
|
+
# F-AS: the guard raises an OSError, which the `except OSError` below
|
|
417
|
+
# turns into "did not write" — the mode's promise, no new branch.
|
|
418
|
+
_readonly.guard(ask_dir, what="this tool's own directory")
|
|
415
419
|
ask_dir.mkdir(parents=True, exist_ok=True)
|
|
416
420
|
marker.write_text(_ASK_DIR_GITIGNORE, encoding="utf-8")
|
|
417
421
|
if announce and created:
|
|
@@ -442,6 +446,7 @@ def _announce_ask_dir(ask_dir: Path) -> None:
|
|
|
442
446
|
|
|
443
447
|
def write_baseline(baseline: dict, out_dir: Path) -> Path:
|
|
444
448
|
"""Write `baseline` as deterministic JSON under `out_dir`; return the file path."""
|
|
449
|
+
_readonly.guard(out_dir, what="baseline history directory")
|
|
445
450
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
446
451
|
protect_ask_dir(out_dir)
|
|
447
452
|
path = out_dir / _baseline_filename(baseline)
|
sourcecode/cache_model.py
CHANGED
|
@@ -99,6 +99,15 @@ REFERENCE_REPOSITORY = (
|
|
|
99
99
|
#: figures are relative to.
|
|
100
100
|
REFERENCE_JAVA_FILES = 2000
|
|
101
101
|
|
|
102
|
+
#: The build the reference figures were measured on. F-AP's second rider: C3-60
|
|
103
|
+
#: conditioned these figures on the repository in hand; this is the other half —
|
|
104
|
+
#: **keeping them current**, or, when they are not current, saying so. A table
|
|
105
|
+
#: measured nine releases ago and printed without its provenance is a prediction
|
|
106
|
+
#: presented as a measurement, which is the one thing this product does not do.
|
|
107
|
+
#: The nightly gate (`docs/perf/REGRESSION-GATE.md`) is what re-measures them; this
|
|
108
|
+
#: constant is what makes the gap visible until it does.
|
|
109
|
+
REFERENCE_MEASURED_VERSION = "3.2.2"
|
|
110
|
+
|
|
102
111
|
#: The other end of the measured range, and the reason this module publishes a
|
|
103
112
|
#: *class* rather than a projected duration (C4-19). Field evaluation #16, on
|
|
104
113
|
#: Windows 11 / PowerShell 5.1 / pipx / Python 3.10: `spring-audit` on a
|
|
@@ -164,7 +173,9 @@ LAYERS: tuple[Layer, ...] = (
|
|
|
164
173
|
location="~/.sourcecode/parse-cache-v1/",
|
|
165
174
|
invalidated_by=(
|
|
166
175
|
"the file's own bytes and the extractor's source — content-addressed, "
|
|
167
|
-
"so it is never stale and never needs invalidating"
|
|
176
|
+
"so it is never stale and never needs invalidating. Bounded by "
|
|
177
|
+
"`ASK_PARSE_CACHE_MAX_MB` (default 512 MB, least-recently-used, "
|
|
178
|
+
"enforced on write) and reported in `cache status` (F-AR)"
|
|
168
179
|
),
|
|
169
180
|
warmed="filled for every Java file the warm parses",
|
|
170
181
|
),
|
|
@@ -358,6 +369,34 @@ class Conditioning:
|
|
|
358
369
|
)
|
|
359
370
|
|
|
360
371
|
|
|
372
|
+
def reference_currency() -> dict:
|
|
373
|
+
"""How old the reference figures are, in releases the reader can name (F-AP).
|
|
374
|
+
|
|
375
|
+
Returns the build the figures were measured on, the build running now, and
|
|
376
|
+
whether they are the same. The gap is not converted into a count of releases:
|
|
377
|
+
versions are not a distance and this module does not invent one. Naming both
|
|
378
|
+
builds is enough for a reader to see that a figure was measured on a different
|
|
379
|
+
one — which is the whole defect: *"no predice nada a 3 342 ficheros"* applies
|
|
380
|
+
to the size axis, and this is the same failure on the time axis.
|
|
381
|
+
"""
|
|
382
|
+
from sourcecode import __version__
|
|
383
|
+
|
|
384
|
+
current = __version__
|
|
385
|
+
return {
|
|
386
|
+
"measured_on_version": REFERENCE_MEASURED_VERSION,
|
|
387
|
+
"running_version": current,
|
|
388
|
+
"current": REFERENCE_MEASURED_VERSION == current,
|
|
389
|
+
"statement": (
|
|
390
|
+
f"Measured on {REFERENCE_MEASURED_VERSION}; you are running {current}. "
|
|
391
|
+
f"The figures are that build's on that repository — they are not a "
|
|
392
|
+
f"prediction for this one, and the nightly performance gate "
|
|
393
|
+
f"(docs/perf/REGRESSION-GATE.md) is what re-measures them."
|
|
394
|
+
if REFERENCE_MEASURED_VERSION != current
|
|
395
|
+
else f"Measured on {current}, the build you are running."
|
|
396
|
+
),
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
361
400
|
def as_dict(here: "Optional[Conditioning]" = None) -> dict:
|
|
362
401
|
"""The whole model as data — what `ask cache model --json` emits."""
|
|
363
402
|
conditioned: dict = {}
|
|
@@ -386,6 +425,9 @@ def as_dict(here: "Optional[Conditioning]" = None) -> dict:
|
|
|
386
425
|
],
|
|
387
426
|
"reference_repository": REFERENCE_REPOSITORY,
|
|
388
427
|
"reference_java_files": REFERENCE_JAVA_FILES,
|
|
428
|
+
# F-AP rider: the figures carry the build they were measured on, so a
|
|
429
|
+
# table nine releases old cannot read as this build's measurement.
|
|
430
|
+
"reference_currency": reference_currency(),
|
|
389
431
|
# Published beside the reference so no consumer reads the figures as a
|
|
390
432
|
# law: the same command has been measured 46× slower on a repository 1.7×
|
|
391
433
|
# the size, on a different platform (C4-19).
|
|
@@ -427,6 +469,15 @@ def render_markdown() -> str:
|
|
|
427
469
|
out.append("")
|
|
428
470
|
out.append(f"Measured on {REFERENCE_REPOSITORY}, each command in isolation.")
|
|
429
471
|
out.append("")
|
|
472
|
+
# The build, not the gap: this block is a checked-in document, so naming the
|
|
473
|
+
# *running* version here would make the doc drift on every release. `cache
|
|
474
|
+
# model` names both, because there a reader has a running build to compare to.
|
|
475
|
+
out.append(
|
|
476
|
+
f"Measured on release {REFERENCE_MEASURED_VERSION}. Figures are that "
|
|
477
|
+
f"build's on that repository, not a prediction for yours; the nightly "
|
|
478
|
+
f"performance gate (`docs/perf/REGRESSION-GATE.md`) is what re-measures them."
|
|
479
|
+
)
|
|
480
|
+
out.append("")
|
|
430
481
|
out.append("| Command | A warm gives it | Measured (nothing cached → after a warm) | Repeat run cached | Layers | Notes |")
|
|
431
482
|
out.append("|---|---|---|---|---|---|")
|
|
432
483
|
for cmd in COMMANDS:
|
|
@@ -504,6 +555,7 @@ def render_text(here: "Optional[Conditioning]" = None) -> str:
|
|
|
504
555
|
lines.append("")
|
|
505
556
|
lines.append("What a warm gives each command")
|
|
506
557
|
lines.append(f" Timings: {REFERENCE_REPOSITORY}, each command measured in isolation.")
|
|
558
|
+
lines.append(f" {reference_currency()['statement']}")
|
|
507
559
|
width = max(len(c.command) for c in COMMANDS)
|
|
508
560
|
for cmd in COMMANDS:
|
|
509
561
|
repeat = "repeat cached" if cmd.repeat else "recomputes"
|
sourcecode/cli.py
CHANGED
|
@@ -18,6 +18,7 @@ from sourcecode import __version__
|
|
|
18
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.readonly import WriteRefused
|
|
21
22
|
from sourcecode.progress import output_emitted as _progress_output_emitted
|
|
22
23
|
from sourcecode.output_encoding import json_ensure_ascii as _json_ensure_ascii
|
|
23
24
|
from sourcecode.output_encoding import set_ascii_fallback
|
|
@@ -209,6 +210,37 @@ COMMAND_TIERS: "tuple[tuple[str, str, tuple[str, ...]], ...]" = (
|
|
|
209
210
|
)),
|
|
210
211
|
)
|
|
211
212
|
|
|
213
|
+
#: What each command creates inside the repository it is pointed at, and when.
|
|
214
|
+
#: The authority for the `--help` block below, so "which commands write" is a
|
|
215
|
+
#: fact with one home rather than a paragraph someone remembers to update — the
|
|
216
|
+
#: field asked for this by name, beside `--no-write` itself.
|
|
217
|
+
#:
|
|
218
|
+
#: A command absent from this table writes nothing inside the scope. That is a
|
|
219
|
+
#: claim the battery checks, not a hope: `tests/test_no_write_mode.py` fails when
|
|
220
|
+
#: a write seam is added without a row here.
|
|
221
|
+
COMMANDS_THAT_WRITE: "tuple[tuple[str, str], ...]" = (
|
|
222
|
+
("baseline", "`<repo>/.ask/baselines/` (or `--dir`) — one JSON per capture"),
|
|
223
|
+
("migrate-check", "`<repo>/.ask/readiness-history/` — only with `--history`"),
|
|
224
|
+
("verify", "`<repo>/.ask/contracts-baseline.json` — only with `--update-baseline`"),
|
|
225
|
+
("verify-edit", "the repository's git hooks directory — only with `--install-hook`"),
|
|
226
|
+
("*", "`<repo>/.ask/runs/` — only when `ASK_RUNS_IN_REPO=1` is set"),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _writes_help_block() -> str:
|
|
231
|
+
"""The `--no-write` table as `--help` prints it — generated, never typed."""
|
|
232
|
+
lines = [
|
|
233
|
+
"Commands that create files inside the repository (everything else reads only):",
|
|
234
|
+
"",
|
|
235
|
+
]
|
|
236
|
+
width = max(len(name) for name, _ in COMMANDS_THAT_WRITE)
|
|
237
|
+
for name, what in COMMANDS_THAT_WRITE:
|
|
238
|
+
lines.append(f" {name.ljust(width)} {what}")
|
|
239
|
+
lines.append("")
|
|
240
|
+
lines.append(" --no-write (ASK_READONLY=1) refuses all of it and says so on stderr.")
|
|
241
|
+
return "\n".join(lines)
|
|
242
|
+
|
|
243
|
+
|
|
212
244
|
#: Tiers whose membership is short enough to name in `--help`. `supported` is the
|
|
213
245
|
#: remainder by construction, and printing twenty-six names would bury the two lists
|
|
214
246
|
#: a reader acts on.
|
|
@@ -473,6 +505,8 @@ ASK_MAX_ANALYSIS_SECONDS/ASK_PROGRESS when CI needs an explicit budget.
|
|
|
473
505
|
|
|
474
506
|
{_group_help_block()}
|
|
475
507
|
|
|
508
|
+
{_writes_help_block()}
|
|
509
|
+
|
|
476
510
|
[bold]Examples:[/bold]
|
|
477
511
|
ask posture . --diff default:prod -o posture.json
|
|
478
512
|
ask endpoints . -f json | jq '.endpoints | map(select(.method=="POST"))'
|
|
@@ -712,7 +746,7 @@ def _preprocess_args(args: list[str]) -> list[str]:
|
|
|
712
746
|
|
|
713
747
|
def _preprocess_argv() -> None:
|
|
714
748
|
"""Apply _preprocess_args to sys.argv in-place (used by main_entry)."""
|
|
715
|
-
modified = _preprocess_args(sys.argv[1:])
|
|
749
|
+
modified = _preprocess_args(_apply_no_write(sys.argv[1:]))
|
|
716
750
|
sys.argv = sys.argv[:1] + modified
|
|
717
751
|
|
|
718
752
|
|
|
@@ -958,6 +992,15 @@ def _admit_path(
|
|
|
958
992
|
|
|
959
993
|
admission = admit_repo_path(raw)
|
|
960
994
|
if admission.admitted:
|
|
995
|
+
# F-AS: the one gate every command's target passes through is also the
|
|
996
|
+
# one place that knows which tree the no-write guarantee is about. Declared
|
|
997
|
+
# here rather than in each command, so a command cannot forget to.
|
|
998
|
+
try:
|
|
999
|
+
from sourcecode import readonly
|
|
1000
|
+
|
|
1001
|
+
readonly.declare_scope(admission.resolved)
|
|
1002
|
+
except Exception:
|
|
1003
|
+
pass
|
|
961
1004
|
return admission.resolved
|
|
962
1005
|
hint = rejection_hint(admission, usage=usage)
|
|
963
1006
|
if extra_hint:
|
|
@@ -1487,8 +1530,15 @@ def _rule_pass_progress(
|
|
|
1487
1530
|
estimate=False,
|
|
1488
1531
|
)
|
|
1489
1532
|
|
|
1533
|
+
def detail_sink(done: int, total: int, label: str, detail_unit: str) -> None:
|
|
1534
|
+
# C3-79: inside a family the units *are* comparable to each other — files
|
|
1535
|
+
# are files — so the ETA is allowed here, unlike the family counter above.
|
|
1536
|
+
prog.work(done, total, stage=f"{stage} ({label})", unit=detail_unit)
|
|
1537
|
+
|
|
1490
1538
|
return RulePassProgress(
|
|
1491
|
-
sink=sink if watching else None,
|
|
1539
|
+
sink=sink if watching else None,
|
|
1540
|
+
detail_sink=detail_sink if watching else None,
|
|
1541
|
+
stop_when=stop_when,
|
|
1492
1542
|
)
|
|
1493
1543
|
|
|
1494
1544
|
|
|
@@ -1910,6 +1960,69 @@ def _output_option(help: "Optional[str]" = None) -> Any:
|
|
|
1910
1960
|
COMMANDS_WITHOUT_OUTPUT: "frozenset[str]" = frozenset()
|
|
1911
1961
|
|
|
1912
1962
|
|
|
1963
|
+
#: The one help string for `--jobs` (F-AQ).
|
|
1964
|
+
JOBS_OPTION_HELP = (
|
|
1965
|
+
"Worker processes used to parse files (default: CPU count − 1). "
|
|
1966
|
+
"`--jobs 1` parses in this process. Parallelism only warms the "
|
|
1967
|
+
"content-addressed parse cache — the answer is byte-identical at any value. "
|
|
1968
|
+
"Env: ASK_JOBS."
|
|
1969
|
+
)
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
def _jobs_option() -> Any:
|
|
1973
|
+
"""The `--jobs/-j` option, declared once (F-AQ).
|
|
1974
|
+
|
|
1975
|
+
Four field evaluations asked for this, and the fourth measured it from the
|
|
1976
|
+
outside rather than inferring it: `threads=1`, `CPU/wall = 0,92…0,95`,
|
|
1977
|
+
sustained for 54 minutes on a 20-core machine.
|
|
1978
|
+
|
|
1979
|
+
The flag is process-global by nature — one invocation analyses one
|
|
1980
|
+
repository — so it is recorded in :mod:`sourcecode.parallel` rather than
|
|
1981
|
+
threaded through every analysis signature. It is published only on commands
|
|
1982
|
+
that parse a repository; `ASK_JOBS` covers the rest.
|
|
1983
|
+
"""
|
|
1984
|
+
return typer.Option(None, "--jobs", "-j", min=1, help=JOBS_OPTION_HELP)
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def _apply_jobs(jobs: "Optional[int]") -> None:
|
|
1988
|
+
"""Put a `--jobs` value in force for this process. Never raises."""
|
|
1989
|
+
try:
|
|
1990
|
+
from sourcecode import parallel
|
|
1991
|
+
|
|
1992
|
+
parallel.set_jobs(jobs)
|
|
1993
|
+
except Exception:
|
|
1994
|
+
pass
|
|
1995
|
+
|
|
1996
|
+
|
|
1997
|
+
#: The one help string for `--no-write` (F-AS).
|
|
1998
|
+
NO_WRITE_OPTION_HELP = (
|
|
1999
|
+
"Create nothing inside the analysed repository. A command that cannot answer "
|
|
2000
|
+
"without writing says so instead of writing. Shared caches outside the "
|
|
2001
|
+
"repository are unaffected. Env: ASK_READONLY=1."
|
|
2002
|
+
)
|
|
2003
|
+
|
|
2004
|
+
#: The flag itself, handled before the parser sees it — the `--detach` precedent
|
|
2005
|
+
#: (C3-53), for the same reason and a stronger one. A mode whose value is that an
|
|
2006
|
+
#: auditor can *rely* on it is worthless if it is accepted by five commands and
|
|
2007
|
+
#: rejected by seventy-nine: the one run that refuses the flag is the run that
|
|
2008
|
+
#: writes. Declaring it per command would be eighty-four copies of one decision
|
|
2009
|
+
#: that is not a command's to make.
|
|
2010
|
+
NO_WRITE_FLAG = "--no-write"
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
def _apply_no_write(argv: "list[str]") -> "list[str]":
|
|
2014
|
+
"""Consume `--no-write` from *argv*, putting the mode in force. Never raises."""
|
|
2015
|
+
if NO_WRITE_FLAG not in argv:
|
|
2016
|
+
return argv
|
|
2017
|
+
try:
|
|
2018
|
+
from sourcecode import readonly
|
|
2019
|
+
|
|
2020
|
+
readonly.set_readonly(True)
|
|
2021
|
+
except Exception:
|
|
2022
|
+
pass
|
|
2023
|
+
return [a for a in argv if a != NO_WRITE_FLAG]
|
|
2024
|
+
|
|
2025
|
+
|
|
1913
2026
|
#: The one help string for `--format`, for the same reason as COPY_OPTION_HELP.
|
|
1914
2027
|
FORMAT_OPTION_HELP = "Output format: json (default) or yaml."
|
|
1915
2028
|
|
|
@@ -2667,6 +2780,7 @@ def main(
|
|
|
2667
2780
|
"--no-cache",
|
|
2668
2781
|
help="Bypass the scan cache and force a fresh analysis.",
|
|
2669
2782
|
),
|
|
2783
|
+
jobs: Optional[int] = _jobs_option(),
|
|
2670
2784
|
) -> None:
|
|
2671
2785
|
"""Analyze a repository and produce structured context for AI coding agents.
|
|
2672
2786
|
|
|
@@ -2677,6 +2791,7 @@ def main(
|
|
|
2677
2791
|
ask /path/to/repo --compact analyze specific path
|
|
2678
2792
|
ask --agent agent-optimized output (full detail)
|
|
2679
2793
|
"""
|
|
2794
|
+
_apply_jobs(jobs)
|
|
2680
2795
|
# First-run telemetry notice (skip for telemetry/version/config subcommands)
|
|
2681
2796
|
if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
|
|
2682
2797
|
_maybe_show_telemetry_notice()
|
|
@@ -4557,6 +4672,7 @@ def prepare_context_cmd(
|
|
|
4557
4672
|
),
|
|
4558
4673
|
),
|
|
4559
4674
|
progress_mode: Optional[str] = _progress_option(),
|
|
4675
|
+
jobs: Optional[int] = _jobs_option(),
|
|
4560
4676
|
) -> None:
|
|
4561
4677
|
"""Task-specific context for AI coding agents.
|
|
4562
4678
|
|
|
@@ -4586,6 +4702,7 @@ def prepare_context_cmd(
|
|
|
4586
4702
|
JSON output includes evidence_manifest: claim IDs for context files and
|
|
4587
4703
|
source-backed rows, plus command inputs, ASK version, git HEAD and freshness.
|
|
4588
4704
|
"""
|
|
4705
|
+
_apply_jobs(jobs)
|
|
4589
4706
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
4590
4707
|
|
|
4591
4708
|
if task_help:
|
|
@@ -5352,6 +5469,7 @@ def repo_ir_cmd(
|
|
|
5352
5469
|
help="Compress output with gzip. Requires --output. Reduces large IR files by ~70-80%.",
|
|
5353
5470
|
),
|
|
5354
5471
|
progress_mode: Optional[str] = _progress_option(),
|
|
5472
|
+
jobs: Optional[int] = _jobs_option(),
|
|
5355
5473
|
) -> None:
|
|
5356
5474
|
"""Deterministic symbol-level IR for Java repositories.
|
|
5357
5475
|
|
|
@@ -5376,6 +5494,7 @@ def repo_ir_cmd(
|
|
|
5376
5494
|
ask repo-ir --max-nodes 200 --max-edges 500
|
|
5377
5495
|
ask repo-ir --output ir.json.gz --gzip
|
|
5378
5496
|
"""
|
|
5497
|
+
_apply_jobs(jobs)
|
|
5379
5498
|
import json as _json
|
|
5380
5499
|
|
|
5381
5500
|
from sourcecode.repository_ir import apply_ir_size_limits, build_repo_ir, find_java_files
|
|
@@ -5868,6 +5987,7 @@ def endpoints_cmd(
|
|
|
5868
5987
|
"(implies --client-usage).",
|
|
5869
5988
|
),
|
|
5870
5989
|
progress_mode: Optional[str] = _progress_option(),
|
|
5990
|
+
jobs: Optional[int] = _jobs_option(),
|
|
5871
5991
|
) -> None:
|
|
5872
5992
|
"""Extract REST API endpoint surface from Java source files.
|
|
5873
5993
|
|
|
@@ -5899,6 +6019,7 @@ def endpoints_cmd(
|
|
|
5899
6019
|
ask endpoints . --limit 10
|
|
5900
6020
|
ask endpoints . --compact
|
|
5901
6021
|
"""
|
|
6022
|
+
_apply_jobs(jobs)
|
|
5902
6023
|
_enforce_format("endpoints", format)
|
|
5903
6024
|
|
|
5904
6025
|
target = _admit_path(path)
|
|
@@ -6418,6 +6539,7 @@ def export_cmd(
|
|
|
6418
6539
|
"default because it dominates the export size (18 MB on a large repo).",
|
|
6419
6540
|
),
|
|
6420
6541
|
progress_mode: Optional[str] = _progress_option(),
|
|
6542
|
+
jobs: Optional[int] = _jobs_option(),
|
|
6421
6543
|
) -> None:
|
|
6422
6544
|
"""Export structured, tool-agnostic codebase views for downstream tooling.
|
|
6423
6545
|
|
|
@@ -6446,6 +6568,7 @@ def export_cmd(
|
|
|
6446
6568
|
The section flags compose; pass several to emit multiple sections in one
|
|
6447
6569
|
document. --c4 assembles the full architecture export on its own.
|
|
6448
6570
|
"""
|
|
6571
|
+
_apply_jobs(jobs)
|
|
6449
6572
|
from sourcecode.repository_ir import build_repo_ir, find_java_files
|
|
6450
6573
|
|
|
6451
6574
|
_enforce_format("export", format)
|
|
@@ -6563,6 +6686,7 @@ def validation_cmd(
|
|
|
6563
6686
|
help=_NO_CACHE_SUBCOMMAND_HELP,
|
|
6564
6687
|
),
|
|
6565
6688
|
progress_mode: Optional[str] = _progress_option(),
|
|
6689
|
+
jobs: Optional[int] = _jobs_option(),
|
|
6566
6690
|
) -> None:
|
|
6567
6691
|
"""Map request-body validation per endpoint (constraints + custom validators).
|
|
6568
6692
|
|
|
@@ -6587,6 +6711,7 @@ def validation_cmd(
|
|
|
6587
6711
|
ask validation . --path-prefix /owners
|
|
6588
6712
|
ask validation . --format yaml
|
|
6589
6713
|
"""
|
|
6714
|
+
_apply_jobs(jobs)
|
|
6590
6715
|
_enforce_format("validation", format)
|
|
6591
6716
|
|
|
6592
6717
|
target = _admit_path(path)
|
|
@@ -7940,6 +8065,7 @@ def spring_audit_cmd(
|
|
|
7940
8065
|
),
|
|
7941
8066
|
detach: bool = _detach_option(),
|
|
7942
8067
|
progress_mode: Optional[str] = _progress_option(),
|
|
8068
|
+
jobs: Optional[int] = _jobs_option(),
|
|
7943
8069
|
) -> None:
|
|
7944
8070
|
"""Spring semantic audit: {{RULE_SUMMARY}}.
|
|
7945
8071
|
|
|
@@ -7993,6 +8119,7 @@ def spring_audit_cmd(
|
|
|
7993
8119
|
{{NON_COVERAGE}}
|
|
7994
8120
|
Silence about those is not evidence that there is nothing to find.
|
|
7995
8121
|
"""
|
|
8122
|
+
_apply_jobs(jobs)
|
|
7996
8123
|
import json as _json
|
|
7997
8124
|
|
|
7998
8125
|
from sourcecode.repository_ir import find_java_files
|
|
@@ -8085,6 +8212,8 @@ def spring_audit_cmd(
|
|
|
8085
8212
|
# that the rule pass asks it.
|
|
8086
8213
|
_rules_cut: list[str] = []
|
|
8087
8214
|
_phases_cut: list[str] = []
|
|
8215
|
+
# C3-79: families the budget stopped *inside*, with how far each got.
|
|
8216
|
+
_rules_partial: list[dict] = []
|
|
8088
8217
|
if scope in ("all", "tx"):
|
|
8089
8218
|
if _run.exhausted():
|
|
8090
8219
|
_stopped_early = _WHY_BUDGET
|
|
@@ -8098,7 +8227,11 @@ def spring_audit_cmd(
|
|
|
8098
8227
|
))
|
|
8099
8228
|
_cut = list(getattr(_tx_watch, "not_run", []) or [])
|
|
8100
8229
|
_rules_cut.extend(_cut)
|
|
8101
|
-
|
|
8230
|
+
_partial_families = list(
|
|
8231
|
+
getattr(_tx_watch, "partially_run", []) or []
|
|
8232
|
+
)
|
|
8233
|
+
_rules_partial.extend(_partial_families)
|
|
8234
|
+
if _cut or _partial_families:
|
|
8102
8235
|
_stopped_early = _WHY_BUDGET
|
|
8103
8236
|
_phases_cut.append("tx_audit")
|
|
8104
8237
|
else:
|
|
@@ -8119,7 +8252,11 @@ def spring_audit_cmd(
|
|
|
8119
8252
|
))
|
|
8120
8253
|
_cut = list(getattr(_sec_watch, "not_run", []) or [])
|
|
8121
8254
|
_rules_cut.extend(_cut)
|
|
8122
|
-
|
|
8255
|
+
_partial_families = list(
|
|
8256
|
+
getattr(_sec_watch, "partially_run", []) or []
|
|
8257
|
+
)
|
|
8258
|
+
_rules_partial.extend(_partial_families)
|
|
8259
|
+
if _cut or _partial_families:
|
|
8123
8260
|
_stopped_early = _WHY_BUDGET
|
|
8124
8261
|
_phases_cut.append("security_audit")
|
|
8125
8262
|
else:
|
|
@@ -8130,6 +8267,14 @@ def spring_audit_cmd(
|
|
|
8130
8267
|
|
|
8131
8268
|
combined = SpringAuditResult.merge(results, scope=scope)
|
|
8132
8269
|
|
|
8270
|
+
# C2-31: a phase that never *started* is a cut this envelope cannot
|
|
8271
|
+
# learn from its own scopes — no auditor ran to record it. The run
|
|
8272
|
+
# knows, so it says so here, and `finalize()` applies the same ceiling
|
|
8273
|
+
# and the same floor marks it applies to a scope cut mid-pass.
|
|
8274
|
+
if _stopped_early and not combined.partial:
|
|
8275
|
+
combined.partial = True
|
|
8276
|
+
combined.finalize()
|
|
8277
|
+
|
|
8133
8278
|
if _file_limitations:
|
|
8134
8279
|
combined.limitations.extend(_file_limitations)
|
|
8135
8280
|
|
|
@@ -8172,6 +8317,22 @@ def spring_audit_cmd(
|
|
|
8172
8317
|
data = combined.to_compact_dict() if compact else combined.to_dict()
|
|
8173
8318
|
if _stopped_early:
|
|
8174
8319
|
data["_partial"] = _run.status(_stopped_early)
|
|
8320
|
+
if _rules_partial:
|
|
8321
|
+
# C3-79: neither run nor not-run. How far it got is what a
|
|
8322
|
+
# reader needs to decide whether to re-run with more time, and
|
|
8323
|
+
# the findings it produced before the cut are in the payload.
|
|
8324
|
+
data["_partial"]["rules_partially_run"] = _rules_partial
|
|
8325
|
+
data["_partial"]["how_to_read"] += (
|
|
8326
|
+
" One rule family was stopped part-way through its own "
|
|
8327
|
+
"population: "
|
|
8328
|
+
+ "; ".join(
|
|
8329
|
+
f"{'/'.join(entry['rule_ids'])} after "
|
|
8330
|
+
f"{entry['units_done']} of {entry['units_total']} "
|
|
8331
|
+
f"{entry['unit']}"
|
|
8332
|
+
for entry in _rules_partial
|
|
8333
|
+
)
|
|
8334
|
+
+ ". What it found before the cut is reported and is a floor."
|
|
8335
|
+
)
|
|
8175
8336
|
if _rules_cut:
|
|
8176
8337
|
# C3-71: a phase stopped *inside* is neither completed nor
|
|
8177
8338
|
# pending, and calling it either would misreport the run. The
|
|
@@ -8652,6 +8813,7 @@ def risk_cmd(
|
|
|
8652
8813
|
copy: bool = _copy_option(),
|
|
8653
8814
|
detach: bool = _detach_option(),
|
|
8654
8815
|
progress_mode: Optional[str] = _progress_option(),
|
|
8816
|
+
jobs: Optional[int] = _jobs_option(),
|
|
8655
8817
|
) -> None:
|
|
8656
8818
|
"""[EXPERIMENTAL] What each defect actually costs, once reach and access are in it.
|
|
8657
8819
|
|
|
@@ -8697,6 +8859,7 @@ def risk_cmd(
|
|
|
8697
8859
|
ask risk . --table --rule SEC-008 --band high --top-n 20
|
|
8698
8860
|
ask risk . --limit 10 -o risk.json
|
|
8699
8861
|
"""
|
|
8862
|
+
_apply_jobs(jobs)
|
|
8700
8863
|
from sourcecode.risk import build_risk
|
|
8701
8864
|
|
|
8702
8865
|
path = _admit_path(path)
|
|
@@ -9373,6 +9536,7 @@ def posture_cmd(
|
|
|
9373
9536
|
format: str = _format_option(help="Output format: json or yaml."),
|
|
9374
9537
|
copy: bool = _copy_option(),
|
|
9375
9538
|
progress_mode: Optional[str] = _progress_option(),
|
|
9539
|
+
jobs: Optional[int] = _jobs_option(),
|
|
9376
9540
|
) -> None:
|
|
9377
9541
|
"""Which beans are wired under a profile set — and which cannot be decided.
|
|
9378
9542
|
|
|
@@ -9410,6 +9574,7 @@ def posture_cmd(
|
|
|
9410
9574
|
ask posture . --resolve-environments
|
|
9411
9575
|
ask posture . --resolve-environments --env-file prod.env
|
|
9412
9576
|
"""
|
|
9577
|
+
_apply_jobs(jobs)
|
|
9413
9578
|
from sourcecode.posture import (
|
|
9414
9579
|
FAIL_ON_CHOICES,
|
|
9415
9580
|
build_posture,
|
|
@@ -9782,6 +9947,7 @@ def migrate_check_cmd(
|
|
|
9782
9947
|
),
|
|
9783
9948
|
detach: bool = _detach_option(),
|
|
9784
9949
|
progress_mode: Optional[str] = _progress_option(),
|
|
9950
|
+
jobs: Optional[int] = _jobs_option(),
|
|
9785
9951
|
) -> None:
|
|
9786
9952
|
"""Spring Boot 2→3 migration readiness + Java LTS/licensing inventory.
|
|
9787
9953
|
|
|
@@ -9851,6 +10017,7 @@ def migrate_check_cmd(
|
|
|
9851
10017
|
blockers; and explicit Oracle JDK/licensing-review signals. This is
|
|
9852
10018
|
inventory evidence, not legal advice, and it never changes readiness_score.
|
|
9853
10019
|
"""
|
|
10020
|
+
_apply_jobs(jobs)
|
|
9854
10021
|
from sourcecode.repository_ir import find_java_files
|
|
9855
10022
|
from sourcecode.migrate_check import run_migrate_check
|
|
9856
10023
|
|
|
@@ -10106,6 +10273,7 @@ def impact_chain_cmd(
|
|
|
10106
10273
|
),
|
|
10107
10274
|
),
|
|
10108
10275
|
progress_mode: Optional[str] = _progress_option(),
|
|
10276
|
+
jobs: Optional[int] = _jobs_option(),
|
|
10109
10277
|
) -> None:
|
|
10110
10278
|
"""Spring impact-chain: systemic blast radius of a symbol; TX/SEC findings are opt-in.
|
|
10111
10279
|
|
|
@@ -10139,6 +10307,7 @@ def impact_chain_cmd(
|
|
|
10139
10307
|
ask impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
10140
10308
|
ask impact-chain PaymentService . --depth 6 --output impact.json
|
|
10141
10309
|
"""
|
|
10310
|
+
_apply_jobs(jobs)
|
|
10142
10311
|
import json as _json
|
|
10143
10312
|
|
|
10144
10313
|
from sourcecode.repository_ir import find_java_files
|
|
@@ -12996,6 +13165,18 @@ def cache_status_cmd(
|
|
|
12996
13165
|
"written by another build and can never be read again — the next "
|
|
12997
13166
|
"analysis retires them"
|
|
12998
13167
|
)
|
|
13168
|
+
# F-AR: a store in somebody else's home directory says what bounds it.
|
|
13169
|
+
if "budget_bytes" in _store:
|
|
13170
|
+
_budget_mb = _store.get("budget_mb")
|
|
13171
|
+
_env = _store.get("budget_env")
|
|
13172
|
+
if _store.get("budget_bytes"):
|
|
13173
|
+
_over = " — OVER, the next write evicts" if _store.get("over_budget") else ""
|
|
13174
|
+
_answer.say(
|
|
13175
|
+
f"{'':<13}budget {_budget_mb} MB, {_store.get('policy')} "
|
|
13176
|
+
f"(${_env}){_over}"
|
|
13177
|
+
)
|
|
13178
|
+
else:
|
|
13179
|
+
_answer.say(f"{'':<13}{_store.get('policy')} (${_env})")
|
|
12999
13180
|
# RIS section
|
|
13000
13181
|
if stats.get("ris_exists"):
|
|
13001
13182
|
_stale_tag = " [STALE]" if stats.get("ris_is_stale") else ""
|
|
@@ -13604,6 +13785,24 @@ def main_entry() -> None:
|
|
|
13604
13785
|
try:
|
|
13605
13786
|
# prog_name pins usage/help to the canonical `ask`, whatever the alias.
|
|
13606
13787
|
app(prog_name="ask")
|
|
13788
|
+
except WriteRefused as _refused:
|
|
13789
|
+
# F-AS. "A command that cannot answer without writing says so instead of
|
|
13790
|
+
# writing" — and *says so* means one sentence in the error envelope every
|
|
13791
|
+
# other refusal uses, never a traceback. The write did not happen; that is
|
|
13792
|
+
# the guarantee holding, not a fault.
|
|
13793
|
+
_emit_error_json(
|
|
13794
|
+
INVALID_INPUT_CODE,
|
|
13795
|
+
str(_refused),
|
|
13796
|
+
hint=(
|
|
13797
|
+
"The run was asked to create something inside the repository while "
|
|
13798
|
+
"--no-write (ASK_READONLY=1) was in force. Drop the flag that "
|
|
13799
|
+
"persists — or point its directory option outside the repository — "
|
|
13800
|
+
"and the same answer is produced with the artefact."
|
|
13801
|
+
),
|
|
13802
|
+
expected="A run that creates nothing inside the analysed repository.",
|
|
13803
|
+
path=_refused.path,
|
|
13804
|
+
)
|
|
13805
|
+
raise SystemExit(1)
|
|
13607
13806
|
finally:
|
|
13608
13807
|
# Best-effort "new version available" nudge. Only speaks on an
|
|
13609
13808
|
# interactive terminal; never blocks, raises, or affects exit status.
|