sourcecode 4.12.0__py3-none-any.whl → 4.15.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.
- sourcecode/__init__.py +1 -1
- sourcecode/architectural_baseline.py +5 -0
- sourcecode/cache_model.py +53 -1
- sourcecode/cli.py +245 -7
- sourcecode/parallel.py +241 -0
- sourcecode/parse_cache.py +183 -1
- sourcecode/perf.py +268 -4
- sourcecode/phased_run.py +33 -0
- 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 +165 -0
- sourcecode/runs.py +3 -0
- sourcecode/security_config_scan.py +43 -5
- sourcecode/spring_findings.py +82 -0
- sourcecode/spring_security_audit.py +108 -13
- sourcecode/spring_tx_analyzer.py +88 -14
- sourcecode/verify_edit.py +3 -0
- sourcecode/verify_repo.py +3 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.15.0.dist-info}/METADATA +51 -11
- {sourcecode-4.12.0.dist-info → sourcecode-4.15.0.dist-info}/RECORD +26 -24
- {sourcecode-4.12.0.dist-info → sourcecode-4.15.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.15.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.12.0.dist-info → sourcecode-4.15.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:
|
|
@@ -1218,16 +1261,23 @@ def _analysis_budget(command: str) -> dict[str, Any]:
|
|
|
1218
1261
|
command, ("repo-wide", 600, "run this command in a supervised or nightly job")
|
|
1219
1262
|
)
|
|
1220
1263
|
configured: Optional[float] = None
|
|
1264
|
+
source: Optional[str] = None
|
|
1221
1265
|
raw = os.environ.get("ASK_MAX_ANALYSIS_SECONDS")
|
|
1222
1266
|
if raw:
|
|
1223
1267
|
try:
|
|
1224
1268
|
configured = float(raw)
|
|
1225
1269
|
except ValueError:
|
|
1226
1270
|
configured = None
|
|
1271
|
+
else:
|
|
1272
|
+
# C3-83: a budget is inherited from a shell or a CI runner, so the
|
|
1273
|
+
# person reading `total_defects` is often not the person who set it.
|
|
1274
|
+
# Where it came from travels with it, from here to `_partial`.
|
|
1275
|
+
source = f"ASK_MAX_ANALYSIS_SECONDS={raw} (process environment)"
|
|
1227
1276
|
return {
|
|
1228
1277
|
"class": cls,
|
|
1229
1278
|
"recommended_min_seconds": recommended,
|
|
1230
1279
|
"configured_max_seconds": configured,
|
|
1280
|
+
"configured_source": source,
|
|
1231
1281
|
"fallback": fallback,
|
|
1232
1282
|
}
|
|
1233
1283
|
|
|
@@ -1275,9 +1325,23 @@ def _enforce_analysis_budget(
|
|
|
1275
1325
|
"""
|
|
1276
1326
|
configured = budget.get("configured_max_seconds")
|
|
1277
1327
|
recommended = float(budget.get("recommended_min_seconds") or 0)
|
|
1278
|
-
if configured is None
|
|
1328
|
+
if configured is None:
|
|
1329
|
+
return
|
|
1330
|
+
source = budget.get("configured_source")
|
|
1331
|
+
from sourcecode.phased_run import budget_provenance, budget_warning
|
|
1332
|
+
|
|
1333
|
+
if float(configured) >= recommended:
|
|
1334
|
+
# C3-83: over the floor there is no advice to give, and there is still a
|
|
1335
|
+
# fact the reader of the answer needs — a limit set in an environment is
|
|
1336
|
+
# invisible to whoever reads `total_defects` afterwards.
|
|
1337
|
+
if not source:
|
|
1338
|
+
return
|
|
1339
|
+
try:
|
|
1340
|
+
sys.stderr.write(budget_provenance(command, float(configured), source))
|
|
1341
|
+
sys.stderr.flush()
|
|
1342
|
+
except Exception:
|
|
1343
|
+
pass
|
|
1279
1344
|
return
|
|
1280
|
-
from sourcecode.phased_run import budget_warning
|
|
1281
1345
|
|
|
1282
1346
|
_anchors, _scope = _budget_evidence(command, path)
|
|
1283
1347
|
try:
|
|
@@ -1285,7 +1349,7 @@ def _enforce_analysis_budget(
|
|
|
1285
1349
|
budget_warning(
|
|
1286
1350
|
command, str(budget.get("class") or "repo-wide"),
|
|
1287
1351
|
float(configured), recommended,
|
|
1288
|
-
anchors=_anchors, scope=_scope,
|
|
1352
|
+
anchors=_anchors, scope=_scope, source=source,
|
|
1289
1353
|
)
|
|
1290
1354
|
)
|
|
1291
1355
|
sys.stderr.flush()
|
|
@@ -1487,8 +1551,15 @@ def _rule_pass_progress(
|
|
|
1487
1551
|
estimate=False,
|
|
1488
1552
|
)
|
|
1489
1553
|
|
|
1554
|
+
def detail_sink(done: int, total: int, label: str, detail_unit: str) -> None:
|
|
1555
|
+
# C3-79: inside a family the units *are* comparable to each other — files
|
|
1556
|
+
# are files — so the ETA is allowed here, unlike the family counter above.
|
|
1557
|
+
prog.work(done, total, stage=f"{stage} ({label})", unit=detail_unit)
|
|
1558
|
+
|
|
1490
1559
|
return RulePassProgress(
|
|
1491
|
-
sink=sink if watching else None,
|
|
1560
|
+
sink=sink if watching else None,
|
|
1561
|
+
detail_sink=detail_sink if watching else None,
|
|
1562
|
+
stop_when=stop_when,
|
|
1492
1563
|
)
|
|
1493
1564
|
|
|
1494
1565
|
|
|
@@ -1910,6 +1981,83 @@ def _output_option(help: "Optional[str]" = None) -> Any:
|
|
|
1910
1981
|
COMMANDS_WITHOUT_OUTPUT: "frozenset[str]" = frozenset()
|
|
1911
1982
|
|
|
1912
1983
|
|
|
1984
|
+
#: The one help string for `--jobs` (F-AQ; the scope sentence is C3-84/CL-18).
|
|
1985
|
+
JOBS_OPTION_HELP = (
|
|
1986
|
+
"Worker processes used to parse files (default: CPU count − 1). "
|
|
1987
|
+
"`--jobs 1` parses in this process. Parallelism only warms the "
|
|
1988
|
+
"content-addressed parse cache — the answer is byte-identical at any value. "
|
|
1989
|
+
"It does NOT parallelise the rule pass, which is where a repository-wide "
|
|
1990
|
+
"audit spends most of its time, and on a warm cache there is nothing left "
|
|
1991
|
+
"to parse, so raising it buys nothing there. Env: ASK_JOBS."
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
def _jobs_option() -> Any:
|
|
1996
|
+
"""The `--jobs/-j` option, declared once (F-AQ).
|
|
1997
|
+
|
|
1998
|
+
Four field evaluations asked for this, and the fourth measured it from the
|
|
1999
|
+
outside rather than inferring it: `threads=1`, `CPU/wall = 0,92…0,95`,
|
|
2000
|
+
sustained for 54 minutes on a 20-core machine.
|
|
2001
|
+
|
|
2002
|
+
The flag is process-global by nature — one invocation analyses one
|
|
2003
|
+
repository — so it is recorded in :mod:`sourcecode.parallel` rather than
|
|
2004
|
+
threaded through every analysis signature. It is published only on commands
|
|
2005
|
+
that parse a repository; `ASK_JOBS` covers the rest.
|
|
2006
|
+
|
|
2007
|
+
**C3-84 / CL-18.** The fifth evaluation read this help, sampled the process
|
|
2008
|
+
and corrected four rounds of its own diagnosis: parallelism exists and covers
|
|
2009
|
+
the phase that is not the cost. `threads=4` for the first seconds (parsing),
|
|
2010
|
+
then `threads=1` with CPU/wall **0,97** for the remaining ~99 % — the rule
|
|
2011
|
+
pass, applying families over a built IR — and on a warm cache, which is the
|
|
2012
|
+
normal case, there is nothing to parse at all. The old sentence was true
|
|
2013
|
+
about what the flag *does* and silent about what it does not, which is the
|
|
2014
|
+
half that decides whether setting it is worth anything. Saying so costs a
|
|
2015
|
+
line; letting an operator raise it and measure no change costs trust. The
|
|
2016
|
+
capability that makes the rule pass parallel is F-AT, and no help text is a
|
|
2017
|
+
substitute for it.
|
|
2018
|
+
"""
|
|
2019
|
+
return typer.Option(None, "--jobs", "-j", min=1, help=JOBS_OPTION_HELP)
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
def _apply_jobs(jobs: "Optional[int]") -> None:
|
|
2023
|
+
"""Put a `--jobs` value in force for this process. Never raises."""
|
|
2024
|
+
try:
|
|
2025
|
+
from sourcecode import parallel
|
|
2026
|
+
|
|
2027
|
+
parallel.set_jobs(jobs)
|
|
2028
|
+
except Exception:
|
|
2029
|
+
pass
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
#: The one help string for `--no-write` (F-AS).
|
|
2033
|
+
NO_WRITE_OPTION_HELP = (
|
|
2034
|
+
"Create nothing inside the analysed repository. A command that cannot answer "
|
|
2035
|
+
"without writing says so instead of writing. Shared caches outside the "
|
|
2036
|
+
"repository are unaffected. Env: ASK_READONLY=1."
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
#: The flag itself, handled before the parser sees it — the `--detach` precedent
|
|
2040
|
+
#: (C3-53), for the same reason and a stronger one. A mode whose value is that an
|
|
2041
|
+
#: auditor can *rely* on it is worthless if it is accepted by five commands and
|
|
2042
|
+
#: rejected by seventy-nine: the one run that refuses the flag is the run that
|
|
2043
|
+
#: writes. Declaring it per command would be eighty-four copies of one decision
|
|
2044
|
+
#: that is not a command's to make.
|
|
2045
|
+
NO_WRITE_FLAG = "--no-write"
|
|
2046
|
+
|
|
2047
|
+
|
|
2048
|
+
def _apply_no_write(argv: "list[str]") -> "list[str]":
|
|
2049
|
+
"""Consume `--no-write` from *argv*, putting the mode in force. Never raises."""
|
|
2050
|
+
if NO_WRITE_FLAG not in argv:
|
|
2051
|
+
return argv
|
|
2052
|
+
try:
|
|
2053
|
+
from sourcecode import readonly
|
|
2054
|
+
|
|
2055
|
+
readonly.set_readonly(True)
|
|
2056
|
+
except Exception:
|
|
2057
|
+
pass
|
|
2058
|
+
return [a for a in argv if a != NO_WRITE_FLAG]
|
|
2059
|
+
|
|
2060
|
+
|
|
1913
2061
|
#: The one help string for `--format`, for the same reason as COPY_OPTION_HELP.
|
|
1914
2062
|
FORMAT_OPTION_HELP = "Output format: json (default) or yaml."
|
|
1915
2063
|
|
|
@@ -2667,6 +2815,7 @@ def main(
|
|
|
2667
2815
|
"--no-cache",
|
|
2668
2816
|
help="Bypass the scan cache and force a fresh analysis.",
|
|
2669
2817
|
),
|
|
2818
|
+
jobs: Optional[int] = _jobs_option(),
|
|
2670
2819
|
) -> None:
|
|
2671
2820
|
"""Analyze a repository and produce structured context for AI coding agents.
|
|
2672
2821
|
|
|
@@ -2677,6 +2826,7 @@ def main(
|
|
|
2677
2826
|
ask /path/to/repo --compact analyze specific path
|
|
2678
2827
|
ask --agent agent-optimized output (full detail)
|
|
2679
2828
|
"""
|
|
2829
|
+
_apply_jobs(jobs)
|
|
2680
2830
|
# First-run telemetry notice (skip for telemetry/version/config subcommands)
|
|
2681
2831
|
if ctx.invoked_subcommand not in ("telemetry", "version", "config"):
|
|
2682
2832
|
_maybe_show_telemetry_notice()
|
|
@@ -4557,6 +4707,7 @@ def prepare_context_cmd(
|
|
|
4557
4707
|
),
|
|
4558
4708
|
),
|
|
4559
4709
|
progress_mode: Optional[str] = _progress_option(),
|
|
4710
|
+
jobs: Optional[int] = _jobs_option(),
|
|
4560
4711
|
) -> None:
|
|
4561
4712
|
"""Task-specific context for AI coding agents.
|
|
4562
4713
|
|
|
@@ -4586,6 +4737,7 @@ def prepare_context_cmd(
|
|
|
4586
4737
|
JSON output includes evidence_manifest: claim IDs for context files and
|
|
4587
4738
|
source-backed rows, plus command inputs, ASK version, git HEAD and freshness.
|
|
4588
4739
|
"""
|
|
4740
|
+
_apply_jobs(jobs)
|
|
4589
4741
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
4590
4742
|
|
|
4591
4743
|
if task_help:
|
|
@@ -5352,6 +5504,7 @@ def repo_ir_cmd(
|
|
|
5352
5504
|
help="Compress output with gzip. Requires --output. Reduces large IR files by ~70-80%.",
|
|
5353
5505
|
),
|
|
5354
5506
|
progress_mode: Optional[str] = _progress_option(),
|
|
5507
|
+
jobs: Optional[int] = _jobs_option(),
|
|
5355
5508
|
) -> None:
|
|
5356
5509
|
"""Deterministic symbol-level IR for Java repositories.
|
|
5357
5510
|
|
|
@@ -5376,6 +5529,7 @@ def repo_ir_cmd(
|
|
|
5376
5529
|
ask repo-ir --max-nodes 200 --max-edges 500
|
|
5377
5530
|
ask repo-ir --output ir.json.gz --gzip
|
|
5378
5531
|
"""
|
|
5532
|
+
_apply_jobs(jobs)
|
|
5379
5533
|
import json as _json
|
|
5380
5534
|
|
|
5381
5535
|
from sourcecode.repository_ir import apply_ir_size_limits, build_repo_ir, find_java_files
|
|
@@ -5868,6 +6022,7 @@ def endpoints_cmd(
|
|
|
5868
6022
|
"(implies --client-usage).",
|
|
5869
6023
|
),
|
|
5870
6024
|
progress_mode: Optional[str] = _progress_option(),
|
|
6025
|
+
jobs: Optional[int] = _jobs_option(),
|
|
5871
6026
|
) -> None:
|
|
5872
6027
|
"""Extract REST API endpoint surface from Java source files.
|
|
5873
6028
|
|
|
@@ -5899,6 +6054,7 @@ def endpoints_cmd(
|
|
|
5899
6054
|
ask endpoints . --limit 10
|
|
5900
6055
|
ask endpoints . --compact
|
|
5901
6056
|
"""
|
|
6057
|
+
_apply_jobs(jobs)
|
|
5902
6058
|
_enforce_format("endpoints", format)
|
|
5903
6059
|
|
|
5904
6060
|
target = _admit_path(path)
|
|
@@ -6418,6 +6574,7 @@ def export_cmd(
|
|
|
6418
6574
|
"default because it dominates the export size (18 MB on a large repo).",
|
|
6419
6575
|
),
|
|
6420
6576
|
progress_mode: Optional[str] = _progress_option(),
|
|
6577
|
+
jobs: Optional[int] = _jobs_option(),
|
|
6421
6578
|
) -> None:
|
|
6422
6579
|
"""Export structured, tool-agnostic codebase views for downstream tooling.
|
|
6423
6580
|
|
|
@@ -6446,6 +6603,7 @@ def export_cmd(
|
|
|
6446
6603
|
The section flags compose; pass several to emit multiple sections in one
|
|
6447
6604
|
document. --c4 assembles the full architecture export on its own.
|
|
6448
6605
|
"""
|
|
6606
|
+
_apply_jobs(jobs)
|
|
6449
6607
|
from sourcecode.repository_ir import build_repo_ir, find_java_files
|
|
6450
6608
|
|
|
6451
6609
|
_enforce_format("export", format)
|
|
@@ -6563,6 +6721,7 @@ def validation_cmd(
|
|
|
6563
6721
|
help=_NO_CACHE_SUBCOMMAND_HELP,
|
|
6564
6722
|
),
|
|
6565
6723
|
progress_mode: Optional[str] = _progress_option(),
|
|
6724
|
+
jobs: Optional[int] = _jobs_option(),
|
|
6566
6725
|
) -> None:
|
|
6567
6726
|
"""Map request-body validation per endpoint (constraints + custom validators).
|
|
6568
6727
|
|
|
@@ -6587,6 +6746,7 @@ def validation_cmd(
|
|
|
6587
6746
|
ask validation . --path-prefix /owners
|
|
6588
6747
|
ask validation . --format yaml
|
|
6589
6748
|
"""
|
|
6749
|
+
_apply_jobs(jobs)
|
|
6590
6750
|
_enforce_format("validation", format)
|
|
6591
6751
|
|
|
6592
6752
|
target = _admit_path(path)
|
|
@@ -7940,6 +8100,7 @@ def spring_audit_cmd(
|
|
|
7940
8100
|
),
|
|
7941
8101
|
detach: bool = _detach_option(),
|
|
7942
8102
|
progress_mode: Optional[str] = _progress_option(),
|
|
8103
|
+
jobs: Optional[int] = _jobs_option(),
|
|
7943
8104
|
) -> None:
|
|
7944
8105
|
"""Spring semantic audit: {{RULE_SUMMARY}}.
|
|
7945
8106
|
|
|
@@ -7993,6 +8154,7 @@ def spring_audit_cmd(
|
|
|
7993
8154
|
{{NON_COVERAGE}}
|
|
7994
8155
|
Silence about those is not evidence that there is nothing to find.
|
|
7995
8156
|
"""
|
|
8157
|
+
_apply_jobs(jobs)
|
|
7996
8158
|
import json as _json
|
|
7997
8159
|
|
|
7998
8160
|
from sourcecode.repository_ir import find_java_files
|
|
@@ -8054,6 +8216,8 @@ def spring_audit_cmd(
|
|
|
8054
8216
|
phases=_phases,
|
|
8055
8217
|
output_path=output_path,
|
|
8056
8218
|
budget_seconds=_analysis_budget("spring-audit").get("configured_max_seconds"),
|
|
8219
|
+
# C3-83: the budget's origin travels with the budget, into `_partial`.
|
|
8220
|
+
budget_source=_analysis_budget("spring-audit").get("configured_source"),
|
|
8057
8221
|
writer=_safe_write_file,
|
|
8058
8222
|
)
|
|
8059
8223
|
_stopped_early: Optional[str] = None
|
|
@@ -8085,6 +8249,8 @@ def spring_audit_cmd(
|
|
|
8085
8249
|
# that the rule pass asks it.
|
|
8086
8250
|
_rules_cut: list[str] = []
|
|
8087
8251
|
_phases_cut: list[str] = []
|
|
8252
|
+
# C3-79: families the budget stopped *inside*, with how far each got.
|
|
8253
|
+
_rules_partial: list[dict] = []
|
|
8088
8254
|
if scope in ("all", "tx"):
|
|
8089
8255
|
if _run.exhausted():
|
|
8090
8256
|
_stopped_early = _WHY_BUDGET
|
|
@@ -8098,7 +8264,11 @@ def spring_audit_cmd(
|
|
|
8098
8264
|
))
|
|
8099
8265
|
_cut = list(getattr(_tx_watch, "not_run", []) or [])
|
|
8100
8266
|
_rules_cut.extend(_cut)
|
|
8101
|
-
|
|
8267
|
+
_partial_families = list(
|
|
8268
|
+
getattr(_tx_watch, "partially_run", []) or []
|
|
8269
|
+
)
|
|
8270
|
+
_rules_partial.extend(_partial_families)
|
|
8271
|
+
if _cut or _partial_families:
|
|
8102
8272
|
_stopped_early = _WHY_BUDGET
|
|
8103
8273
|
_phases_cut.append("tx_audit")
|
|
8104
8274
|
else:
|
|
@@ -8119,7 +8289,11 @@ def spring_audit_cmd(
|
|
|
8119
8289
|
))
|
|
8120
8290
|
_cut = list(getattr(_sec_watch, "not_run", []) or [])
|
|
8121
8291
|
_rules_cut.extend(_cut)
|
|
8122
|
-
|
|
8292
|
+
_partial_families = list(
|
|
8293
|
+
getattr(_sec_watch, "partially_run", []) or []
|
|
8294
|
+
)
|
|
8295
|
+
_rules_partial.extend(_partial_families)
|
|
8296
|
+
if _cut or _partial_families:
|
|
8123
8297
|
_stopped_early = _WHY_BUDGET
|
|
8124
8298
|
_phases_cut.append("security_audit")
|
|
8125
8299
|
else:
|
|
@@ -8130,6 +8304,14 @@ def spring_audit_cmd(
|
|
|
8130
8304
|
|
|
8131
8305
|
combined = SpringAuditResult.merge(results, scope=scope)
|
|
8132
8306
|
|
|
8307
|
+
# C2-31: a phase that never *started* is a cut this envelope cannot
|
|
8308
|
+
# learn from its own scopes — no auditor ran to record it. The run
|
|
8309
|
+
# knows, so it says so here, and `finalize()` applies the same ceiling
|
|
8310
|
+
# and the same floor marks it applies to a scope cut mid-pass.
|
|
8311
|
+
if _stopped_early and not combined.partial:
|
|
8312
|
+
combined.partial = True
|
|
8313
|
+
combined.finalize()
|
|
8314
|
+
|
|
8133
8315
|
if _file_limitations:
|
|
8134
8316
|
combined.limitations.extend(_file_limitations)
|
|
8135
8317
|
|
|
@@ -8172,6 +8354,22 @@ def spring_audit_cmd(
|
|
|
8172
8354
|
data = combined.to_compact_dict() if compact else combined.to_dict()
|
|
8173
8355
|
if _stopped_early:
|
|
8174
8356
|
data["_partial"] = _run.status(_stopped_early)
|
|
8357
|
+
if _rules_partial:
|
|
8358
|
+
# C3-79: neither run nor not-run. How far it got is what a
|
|
8359
|
+
# reader needs to decide whether to re-run with more time, and
|
|
8360
|
+
# the findings it produced before the cut are in the payload.
|
|
8361
|
+
data["_partial"]["rules_partially_run"] = _rules_partial
|
|
8362
|
+
data["_partial"]["how_to_read"] += (
|
|
8363
|
+
" One rule family was stopped part-way through its own "
|
|
8364
|
+
"population: "
|
|
8365
|
+
+ "; ".join(
|
|
8366
|
+
f"{'/'.join(entry['rule_ids'])} after "
|
|
8367
|
+
f"{entry['units_done']} of {entry['units_total']} "
|
|
8368
|
+
f"{entry['unit']}"
|
|
8369
|
+
for entry in _rules_partial
|
|
8370
|
+
)
|
|
8371
|
+
+ ". What it found before the cut is reported and is a floor."
|
|
8372
|
+
)
|
|
8175
8373
|
if _rules_cut:
|
|
8176
8374
|
# C3-71: a phase stopped *inside* is neither completed nor
|
|
8177
8375
|
# pending, and calling it either would misreport the run. The
|
|
@@ -8652,6 +8850,7 @@ def risk_cmd(
|
|
|
8652
8850
|
copy: bool = _copy_option(),
|
|
8653
8851
|
detach: bool = _detach_option(),
|
|
8654
8852
|
progress_mode: Optional[str] = _progress_option(),
|
|
8853
|
+
jobs: Optional[int] = _jobs_option(),
|
|
8655
8854
|
) -> None:
|
|
8656
8855
|
"""[EXPERIMENTAL] What each defect actually costs, once reach and access are in it.
|
|
8657
8856
|
|
|
@@ -8697,6 +8896,7 @@ def risk_cmd(
|
|
|
8697
8896
|
ask risk . --table --rule SEC-008 --band high --top-n 20
|
|
8698
8897
|
ask risk . --limit 10 -o risk.json
|
|
8699
8898
|
"""
|
|
8899
|
+
_apply_jobs(jobs)
|
|
8700
8900
|
from sourcecode.risk import build_risk
|
|
8701
8901
|
|
|
8702
8902
|
path = _admit_path(path)
|
|
@@ -8736,6 +8936,8 @@ def risk_cmd(
|
|
|
8736
8936
|
phases=["audit", "compose"],
|
|
8737
8937
|
output_path=output_path,
|
|
8738
8938
|
budget_seconds=_analysis_budget("risk").get("configured_max_seconds"),
|
|
8939
|
+
# C3-83: the budget's origin travels with the budget, into `_partial`.
|
|
8940
|
+
budget_source=_analysis_budget("risk").get("configured_source"),
|
|
8739
8941
|
writer=_safe_write_file,
|
|
8740
8942
|
)
|
|
8741
8943
|
with _expensive_analysis_scope("risk", path, phase):
|
|
@@ -9373,6 +9575,7 @@ def posture_cmd(
|
|
|
9373
9575
|
format: str = _format_option(help="Output format: json or yaml."),
|
|
9374
9576
|
copy: bool = _copy_option(),
|
|
9375
9577
|
progress_mode: Optional[str] = _progress_option(),
|
|
9578
|
+
jobs: Optional[int] = _jobs_option(),
|
|
9376
9579
|
) -> None:
|
|
9377
9580
|
"""Which beans are wired under a profile set — and which cannot be decided.
|
|
9378
9581
|
|
|
@@ -9410,6 +9613,7 @@ def posture_cmd(
|
|
|
9410
9613
|
ask posture . --resolve-environments
|
|
9411
9614
|
ask posture . --resolve-environments --env-file prod.env
|
|
9412
9615
|
"""
|
|
9616
|
+
_apply_jobs(jobs)
|
|
9413
9617
|
from sourcecode.posture import (
|
|
9414
9618
|
FAIL_ON_CHOICES,
|
|
9415
9619
|
build_posture,
|
|
@@ -9782,6 +9986,7 @@ def migrate_check_cmd(
|
|
|
9782
9986
|
),
|
|
9783
9987
|
detach: bool = _detach_option(),
|
|
9784
9988
|
progress_mode: Optional[str] = _progress_option(),
|
|
9989
|
+
jobs: Optional[int] = _jobs_option(),
|
|
9785
9990
|
) -> None:
|
|
9786
9991
|
"""Spring Boot 2→3 migration readiness + Java LTS/licensing inventory.
|
|
9787
9992
|
|
|
@@ -9851,6 +10056,7 @@ def migrate_check_cmd(
|
|
|
9851
10056
|
blockers; and explicit Oracle JDK/licensing-review signals. This is
|
|
9852
10057
|
inventory evidence, not legal advice, and it never changes readiness_score.
|
|
9853
10058
|
"""
|
|
10059
|
+
_apply_jobs(jobs)
|
|
9854
10060
|
from sourcecode.repository_ir import find_java_files
|
|
9855
10061
|
from sourcecode.migrate_check import run_migrate_check
|
|
9856
10062
|
|
|
@@ -10106,6 +10312,7 @@ def impact_chain_cmd(
|
|
|
10106
10312
|
),
|
|
10107
10313
|
),
|
|
10108
10314
|
progress_mode: Optional[str] = _progress_option(),
|
|
10315
|
+
jobs: Optional[int] = _jobs_option(),
|
|
10109
10316
|
) -> None:
|
|
10110
10317
|
"""Spring impact-chain: systemic blast radius of a symbol; TX/SEC findings are opt-in.
|
|
10111
10318
|
|
|
@@ -10139,6 +10346,7 @@ def impact_chain_cmd(
|
|
|
10139
10346
|
ask impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
10140
10347
|
ask impact-chain PaymentService . --depth 6 --output impact.json
|
|
10141
10348
|
"""
|
|
10349
|
+
_apply_jobs(jobs)
|
|
10142
10350
|
import json as _json
|
|
10143
10351
|
|
|
10144
10352
|
from sourcecode.repository_ir import find_java_files
|
|
@@ -12996,6 +13204,18 @@ def cache_status_cmd(
|
|
|
12996
13204
|
"written by another build and can never be read again — the next "
|
|
12997
13205
|
"analysis retires them"
|
|
12998
13206
|
)
|
|
13207
|
+
# F-AR: a store in somebody else's home directory says what bounds it.
|
|
13208
|
+
if "budget_bytes" in _store:
|
|
13209
|
+
_budget_mb = _store.get("budget_mb")
|
|
13210
|
+
_env = _store.get("budget_env")
|
|
13211
|
+
if _store.get("budget_bytes"):
|
|
13212
|
+
_over = " — OVER, the next write evicts" if _store.get("over_budget") else ""
|
|
13213
|
+
_answer.say(
|
|
13214
|
+
f"{'':<13}budget {_budget_mb} MB, {_store.get('policy')} "
|
|
13215
|
+
f"(${_env}){_over}"
|
|
13216
|
+
)
|
|
13217
|
+
else:
|
|
13218
|
+
_answer.say(f"{'':<13}{_store.get('policy')} (${_env})")
|
|
12999
13219
|
# RIS section
|
|
13000
13220
|
if stats.get("ris_exists"):
|
|
13001
13221
|
_stale_tag = " [STALE]" if stats.get("ris_is_stale") else ""
|
|
@@ -13604,6 +13824,24 @@ def main_entry() -> None:
|
|
|
13604
13824
|
try:
|
|
13605
13825
|
# prog_name pins usage/help to the canonical `ask`, whatever the alias.
|
|
13606
13826
|
app(prog_name="ask")
|
|
13827
|
+
except WriteRefused as _refused:
|
|
13828
|
+
# F-AS. "A command that cannot answer without writing says so instead of
|
|
13829
|
+
# writing" — and *says so* means one sentence in the error envelope every
|
|
13830
|
+
# other refusal uses, never a traceback. The write did not happen; that is
|
|
13831
|
+
# the guarantee holding, not a fault.
|
|
13832
|
+
_emit_error_json(
|
|
13833
|
+
INVALID_INPUT_CODE,
|
|
13834
|
+
str(_refused),
|
|
13835
|
+
hint=(
|
|
13836
|
+
"The run was asked to create something inside the repository while "
|
|
13837
|
+
"--no-write (ASK_READONLY=1) was in force. Drop the flag that "
|
|
13838
|
+
"persists — or point its directory option outside the repository — "
|
|
13839
|
+
"and the same answer is produced with the artefact."
|
|
13840
|
+
),
|
|
13841
|
+
expected="A run that creates nothing inside the analysed repository.",
|
|
13842
|
+
path=_refused.path,
|
|
13843
|
+
)
|
|
13844
|
+
raise SystemExit(1)
|
|
13607
13845
|
finally:
|
|
13608
13846
|
# Best-effort "new version available" nudge. Only speaks on an
|
|
13609
13847
|
# interactive terminal; never blocks, raises, or affects exit status.
|