sourcecode 4.11.0__py3-none-any.whl → 4.12.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 +45 -2
- sourcecode/cache_model.py +30 -15
- sourcecode/cli.py +225 -27
- sourcecode/container_wiring.py +21 -0
- sourcecode/mcp/dependency_import.py +87 -0
- sourcecode/mcp/registry.py +6 -1
- sourcecode/mcp/server.py +20 -2
- sourcecode/parse_cache.py +166 -7
- sourcecode/phased_run.py +61 -10
- sourcecode/progress.py +17 -0
- sourcecode/readiness_timeline.py +10 -1
- sourcecode/release_info.py +1 -1
- sourcecode/repository_ir.py +20 -2
- sourcecode/retrieval/steps_impact.py +5 -2
- sourcecode/risk.py +120 -38
- sourcecode/rule_pass.py +112 -0
- sourcecode/spring_impact.py +29 -3
- sourcecode/spring_security_audit.py +49 -2
- sourcecode/spring_tx_analyzer.py +34 -2
- sourcecode/verify_repo.py +59 -9
- {sourcecode-4.11.0.dist-info → sourcecode-4.12.0.dist-info}/METADATA +3 -3
- {sourcecode-4.11.0.dist-info → sourcecode-4.12.0.dist-info}/RECORD +26 -25
- sourcecode/cache.tmp_new +0 -772
- {sourcecode-4.11.0.dist-info → sourcecode-4.12.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.11.0.dist-info → sourcecode-4.12.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.11.0.dist-info → sourcecode-4.12.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -30,6 +30,7 @@ the perf baselines do.
|
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
32
|
import json
|
|
33
|
+
import os
|
|
33
34
|
import subprocess
|
|
34
35
|
from datetime import datetime, timezone
|
|
35
36
|
from pathlib import Path
|
|
@@ -365,21 +366,40 @@ def _baseline_filename(baseline: dict) -> str:
|
|
|
365
366
|
#: a 14 587-entry structural inventory of the code. A self-confined
|
|
366
367
|
#: `.ask/.gitignore` is the honest form: it never touches the user's own
|
|
367
368
|
#: `.gitignore`, and it stops applying the moment they delete the directory.
|
|
369
|
+
#: C3-74 corrected the last line. It used to read *"Delete this file to version
|
|
370
|
+
#: them deliberately"*, and deleting it is the one action that would commit a
|
|
371
|
+
#: baseline carrying `env.hostname_hash`, `cpu` and `cores` — an invitation to do
|
|
372
|
+
#: the thing the file exists to prevent. It now states the consequence instead.
|
|
368
373
|
_ASK_DIR_GITIGNORE = (
|
|
369
374
|
"# Written by `ask` the first time it created this directory.\n"
|
|
370
375
|
"# These are local analysis artefacts: a structural inventory of your code\n"
|
|
371
376
|
"# and, if you asked for it, the machine that measured it. Nothing here\n"
|
|
372
|
-
"# belongs in a commit
|
|
377
|
+
"# belongs in a commit.\n"
|
|
378
|
+
"#\n"
|
|
379
|
+
"# Removing this file makes everything here committable, including any\n"
|
|
380
|
+
"# recorded machine fingerprint (`--record-env`: hostname hash, CPU, cores).\n"
|
|
381
|
+
"# Version these deliberately only if you intend to publish that.\n"
|
|
373
382
|
"*\n"
|
|
374
383
|
)
|
|
375
384
|
|
|
376
385
|
|
|
377
|
-
def protect_ask_dir(out_dir: Path) -> None:
|
|
386
|
+
def protect_ask_dir(out_dir: Path, *, announce: bool = True) -> None:
|
|
378
387
|
"""Give a freshly created `.ask/` its own `.gitignore` (C3-68).
|
|
379
388
|
|
|
380
389
|
Best-effort and never overwriting: a user who edited it decided something,
|
|
381
390
|
and a tool that re-imposes its default on every run is worse than one that
|
|
382
391
|
never wrote it.
|
|
392
|
+
|
|
393
|
+
**Call this where the write happens, never where the path is computed.** This
|
|
394
|
+
function *creates* the directory in order to protect it, so calling it
|
|
395
|
+
speculatively is a write — which is how `migrate-check` came to leave a
|
|
396
|
+
`.ask/.gitignore` inside a repository it had only read (C3-74).
|
|
397
|
+
|
|
398
|
+
`announce` puts one line on stderr the first time the directory is created,
|
|
399
|
+
with its path. A side effect inside somebody else's repository is exactly what
|
|
400
|
+
I-8 says must be declared, and this one hides from the check an auditor makes:
|
|
401
|
+
`git status --porcelain` is clean because the ignore file ignores itself, and
|
|
402
|
+
only `--ignored` reveals `!! <scope>/.ask/`.
|
|
383
403
|
"""
|
|
384
404
|
try:
|
|
385
405
|
ask_dir = Path(out_dir)
|
|
@@ -391,12 +411,35 @@ def protect_ask_dir(out_dir: Path) -> None:
|
|
|
391
411
|
return
|
|
392
412
|
marker = ask_dir / ".gitignore"
|
|
393
413
|
if not marker.exists():
|
|
414
|
+
created = not ask_dir.exists()
|
|
394
415
|
ask_dir.mkdir(parents=True, exist_ok=True)
|
|
395
416
|
marker.write_text(_ASK_DIR_GITIGNORE, encoding="utf-8")
|
|
417
|
+
if announce and created:
|
|
418
|
+
_announce_ask_dir(ask_dir)
|
|
396
419
|
except OSError:
|
|
397
420
|
return
|
|
398
421
|
|
|
399
422
|
|
|
423
|
+
def _announce_ask_dir(ask_dir: Path) -> None:
|
|
424
|
+
"""One line, on stderr, naming what was created and how to move it (C3-74)."""
|
|
425
|
+
import sys
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# No flag is named here on purpose: the commands that write under `.ask/`
|
|
429
|
+
# spell the override differently (`baseline capture --dir`,
|
|
430
|
+
# `migrate-check --history-dir`), and a message that names the wrong one
|
|
431
|
+
# is the class of defect this ledger is about.
|
|
432
|
+
sys.stderr.write(
|
|
433
|
+
f"[ask] created {ask_dir}{os.sep} for this run's local artefacts — "
|
|
434
|
+
f"git-ignored by its own .gitignore, so `git status` will not show it "
|
|
435
|
+
f"(`git status --ignored` will). The command's directory option takes a "
|
|
436
|
+
f"path outside the repository if you want them elsewhere.\n"
|
|
437
|
+
)
|
|
438
|
+
sys.stderr.flush()
|
|
439
|
+
except Exception:
|
|
440
|
+
pass
|
|
441
|
+
|
|
442
|
+
|
|
400
443
|
def write_baseline(baseline: dict, out_dir: Path) -> Path:
|
|
401
444
|
"""Write `baseline` as deterministic JSON under `out_dir`; return the file path."""
|
|
402
445
|
out_dir.mkdir(parents=True, exist_ok=True)
|
sourcecode/cache_model.py
CHANGED
|
@@ -75,10 +75,17 @@ class CommandCache:
|
|
|
75
75
|
warm_seconds: "Optional[float]" = None
|
|
76
76
|
#: The other measured point: this command on the field repository
|
|
77
77
|
#: (3 342 Java files, Windows 11 / PowerShell 5.1 / pipx, cold), from field
|
|
78
|
-
#: evaluations #13 and #
|
|
78
|
+
#: evaluations #13, #14 and #16. Seconds where a run finished; `field_blocked`
|
|
79
79
|
#: where it did not — the harness promoted the process and the session died,
|
|
80
80
|
#: which is a measurement of a different kind and is never written as a
|
|
81
81
|
#: duration. Rows with neither were not attempted there.
|
|
82
|
+
#:
|
|
83
|
+
#: C3-72: where two evaluations of the same command at this size disagree, the
|
|
84
|
+
#: figure here is the **most recent** one, because it describes the build a
|
|
85
|
+
#: reader is about to run — and the previous figure is kept in the comment
|
|
86
|
+
#: beside it rather than dropped, since a cost that moved by 5,8× between two
|
|
87
|
+
#: releases (spring-audit, 408 s in #13 against 2 351 s in #16) is itself the
|
|
88
|
+
#: measurement C3-76 exists for.
|
|
82
89
|
field_seconds: "Optional[float]" = None
|
|
83
90
|
field_blocked: bool = False
|
|
84
91
|
|
|
@@ -93,19 +100,26 @@ REFERENCE_REPOSITORY = (
|
|
|
93
100
|
REFERENCE_JAVA_FILES = 2000
|
|
94
101
|
|
|
95
102
|
#: The other end of the measured range, and the reason this module publishes a
|
|
96
|
-
#: *class* rather than a projected duration (C4-19). Field evaluation #
|
|
103
|
+
#: *class* rather than a projected duration (C4-19). Field evaluation #16, on
|
|
97
104
|
#: Windows 11 / PowerShell 5.1 / pipx / Python 3.10: `spring-audit` on a
|
|
98
|
-
#: 3 342-file repository ran **
|
|
99
|
-
#: the 2 000-file reference
|
|
100
|
-
#:
|
|
101
|
-
#:
|
|
102
|
-
#: be said honestly is the class, the two
|
|
105
|
+
#: 3 342-file repository ran **2 351 s** — 267× the 8.8 s the same command takes
|
|
106
|
+
#: on the 2 000-file reference, for a repository 1,7× the size. Cost does not
|
|
107
|
+
#: scale with file count in any way this product has measured, so a projected
|
|
108
|
+
#: "~15 s on your repository" would be a confident falsehood of exactly the kind
|
|
109
|
+
#: the ledger exists to prevent. What can be said honestly is the class, the two
|
|
110
|
+
#: measured anchors, and how to run it.
|
|
111
|
+
#:
|
|
112
|
+
#: C3-72 replaced evaluation #13's 408 s with #16's 2 351 s here: the same command
|
|
113
|
+
#: on the same repository at the same commit, measured three releases later
|
|
114
|
+
#: (2 557 s in 4.10.6, 2 398 s in 4.10.7, 2 351 s in 4.11.0). The older figure was
|
|
115
|
+
#: the one a budget message quoted as "~600s" for three releases while the
|
|
116
|
+
#: measurement stood four times higher.
|
|
103
117
|
FIELD_ANCHOR = (
|
|
104
|
-
"field evaluation #
|
|
105
|
-
"(Windows, pipx,
|
|
118
|
+
"field evaluation #16: spring-audit on 3 342 Java files took 2 351 s "
|
|
119
|
+
"(Windows, pipx, warm cache) against 8.8 s on the 2 000-file reference"
|
|
106
120
|
)
|
|
107
121
|
FIELD_ANCHOR_JAVA_FILES = 3342
|
|
108
|
-
FIELD_ANCHOR_SECONDS =
|
|
122
|
+
FIELD_ANCHOR_SECONDS = 2351.4
|
|
109
123
|
|
|
110
124
|
|
|
111
125
|
#: Every layer keys on `cache.worktree_signature` — the exact tree state (C1-9).
|
|
@@ -176,14 +190,15 @@ COMMANDS: tuple[CommandCache, ...] = (
|
|
|
176
190
|
"Resolves the conditional bean graph on every run, over the shared CIR a warm "
|
|
177
191
|
"builds — the parse it used to repeat for itself. `--diff` compares two profile "
|
|
178
192
|
"sets over that one IR, so the second side costs the resolution only.",
|
|
179
|
-
"10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6,
|
|
193
|
+
"10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6, field_seconds=38.0),
|
|
180
194
|
CommandCache("risk", ("cir", "parse"), "shared", False,
|
|
181
195
|
"Composes what the audit, impact-chain and the posture already answer, so it "
|
|
182
196
|
"pays each of their costs once over the shared CIR a warm builds — one parse "
|
|
183
197
|
"for the whole composition, and the reachability query is cached per symbol "
|
|
184
198
|
"within the run.",
|
|
185
199
|
"not measured on the battery yet — the composition is bounded by the "
|
|
186
|
-
"`spring-audit` + `impact-chain` costs listed here, not by new analysis",
|
|
200
|
+
"`spring-audit` + `impact-chain` costs listed here, not by new analysis",
|
|
201
|
+
analysis_class="deep", field_seconds=3232.2),
|
|
187
202
|
CommandCache("enrich", ("cir", "parse"), "shared", False,
|
|
188
203
|
"Runs the same composition as `risk` over the repository, then joins a SARIF "
|
|
189
204
|
"log to it. Reading the log is negligible; everything a warm helps with is the "
|
|
@@ -213,14 +228,14 @@ COMMANDS: tuple[CommandCache, ...] = (
|
|
|
213
228
|
"Recomputes the endpoint surface on every run, over a parse a warm has already "
|
|
214
229
|
"paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
|
|
215
230
|
"the shared parse cache, and a warm measurably bought it nothing (C3-6).",
|
|
216
|
-
"3.3 s → 1.4 s (re-measured on 3.7.0; was 2.8 s → 2.9 s)", analysis_class="repo-wide", cold_seconds=3.3, warm_seconds=1.4, field_seconds=
|
|
231
|
+
"3.3 s → 1.4 s (re-measured on 3.7.0; was 2.8 s → 2.9 s)", analysis_class="repo-wide", cold_seconds=3.3, warm_seconds=1.4, field_seconds=5.0),
|
|
217
232
|
CommandCache("spring-audit", ("ris", "parse"), "shared", False,
|
|
218
233
|
"Recomputes every run, but over a parse a warm has already paid for.",
|
|
219
|
-
"8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=
|
|
234
|
+
"8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=2351.4),
|
|
220
235
|
CommandCache("migrate-check", ("cir",), "none", False,
|
|
221
236
|
"Computes its own inventory and shares nothing a warm builds. Only `--blast-radius` "
|
|
222
237
|
"reuses the shared CIR.",
|
|
223
|
-
"4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8,
|
|
238
|
+
"4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8, field_seconds=26.7),
|
|
224
239
|
CommandCache("impact-chain", ("cir", "parse"), "shared", False,
|
|
225
240
|
"The CIR is the expensive half — this is where a warm pays most.",
|
|
226
241
|
"9.9 s → 1.7 s", analysis_class="core", cold_seconds=9.9, warm_seconds=1.7),
|
sourcecode/cli.py
CHANGED
|
@@ -1232,7 +1232,32 @@ def _analysis_budget(command: str) -> dict[str, Any]:
|
|
|
1232
1232
|
}
|
|
1233
1233
|
|
|
1234
1234
|
|
|
1235
|
-
def
|
|
1235
|
+
def _budget_evidence(command: str, path: "Optional[Path]") -> "tuple[Optional[str], Optional[str]]":
|
|
1236
|
+
"""(measured anchors for this command, size of this repository) — or Nones.
|
|
1237
|
+
|
|
1238
|
+
C3-72: both halves already exist and neither reached the one message an
|
|
1239
|
+
operator reads before deciding to wait. `execution_plan` owns the formatting
|
|
1240
|
+
of the anchors (the same string `--help` and the remedies print) and the
|
|
1241
|
+
cheap, capped scope measurement, so nothing new is computed or claimed here.
|
|
1242
|
+
"""
|
|
1243
|
+
anchors: Optional[str] = None
|
|
1244
|
+
scope: Optional[str] = None
|
|
1245
|
+
try:
|
|
1246
|
+
from sourcecode import execution_plan as _plan
|
|
1247
|
+
|
|
1248
|
+
anchors = _plan._reference(_plan.cost_row(command))
|
|
1249
|
+
if path is not None:
|
|
1250
|
+
measured = _plan.scope_for(Path(path))
|
|
1251
|
+
if measured is not None and measured.measured:
|
|
1252
|
+
scope = measured.describe()
|
|
1253
|
+
except Exception:
|
|
1254
|
+
pass # a message about cost must never be the reason a run fails
|
|
1255
|
+
return anchors, scope
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def _enforce_analysis_budget(
|
|
1259
|
+
command: str, budget: dict[str, Any], path: "Optional[Path]" = None
|
|
1260
|
+
) -> None:
|
|
1236
1261
|
"""Warn when the operator's budget is under the class floor — never refuse.
|
|
1237
1262
|
|
|
1238
1263
|
C3-55: below the floor was rejected and above it admitted a run that killed
|
|
@@ -1254,11 +1279,13 @@ def _enforce_analysis_budget(command: str, budget: dict[str, Any]) -> None:
|
|
|
1254
1279
|
return
|
|
1255
1280
|
from sourcecode.phased_run import budget_warning
|
|
1256
1281
|
|
|
1282
|
+
_anchors, _scope = _budget_evidence(command, path)
|
|
1257
1283
|
try:
|
|
1258
1284
|
sys.stderr.write(
|
|
1259
1285
|
budget_warning(
|
|
1260
1286
|
command, str(budget.get("class") or "repo-wide"),
|
|
1261
1287
|
float(configured), recommended,
|
|
1288
|
+
anchors=_anchors, scope=_scope,
|
|
1262
1289
|
)
|
|
1263
1290
|
)
|
|
1264
1291
|
sys.stderr.flush()
|
|
@@ -1352,7 +1379,7 @@ def _expensive_analysis_scope(command: str, path: Path, phase: str):
|
|
|
1352
1379
|
lock_path = _analysis_lock_path(path)
|
|
1353
1380
|
_emit_analysis_contention_warning(command, path, phase)
|
|
1354
1381
|
budget = _analysis_budget(command)
|
|
1355
|
-
_enforce_analysis_budget(command, budget)
|
|
1382
|
+
_enforce_analysis_budget(command, budget, path)
|
|
1356
1383
|
run_record = RunRecord.start(path, command, phase, budget=budget)
|
|
1357
1384
|
run_token = set_active_run(run_record)
|
|
1358
1385
|
token = f"{os.getpid()}:{time.time_ns()}"
|
|
@@ -1418,6 +1445,53 @@ def _work_sink(prog: Any) -> Any:
|
|
|
1418
1445
|
return sink
|
|
1419
1446
|
|
|
1420
1447
|
|
|
1448
|
+
def _rule_pass_progress(
|
|
1449
|
+
prog: Any,
|
|
1450
|
+
stage: str,
|
|
1451
|
+
unit: str = "rule families",
|
|
1452
|
+
*,
|
|
1453
|
+
stop_when: Any = None,
|
|
1454
|
+
) -> Any:
|
|
1455
|
+
"""The rule-pass observer an auditor reports through, or None (C3-73).
|
|
1456
|
+
|
|
1457
|
+
The per-file passes are counted (C3-56) and the rule pass — which is most of a
|
|
1458
|
+
repository-wide audit's wall time — was not, so the field watched one
|
|
1459
|
+
unchanging line for 39 minutes at exactly the moment the question is *"is this
|
|
1460
|
+
working, or is it hung?"*. The rule families are the population this loop
|
|
1461
|
+
walks, so they are what it counts.
|
|
1462
|
+
|
|
1463
|
+
`estimate=False`: the families are not comparable units. One walks every symbol
|
|
1464
|
+
in the repository, the next reads one annotation, so a rate over the first four
|
|
1465
|
+
says nothing about the fifth. The count answers *"is it moving"*, which is the
|
|
1466
|
+
question; an ETA here would be a number rather than an estimate, and this line
|
|
1467
|
+
exists to be trusted.
|
|
1468
|
+
|
|
1469
|
+
`stop_when` is the operator's budget (C3-71), and it is the reason this returns
|
|
1470
|
+
an observer even when nothing is watching: the heartbeat is disabled off a TTY
|
|
1471
|
+
and in CI, which is **precisely** where a budget matters, so gating the observer
|
|
1472
|
+
on the spinner would have honoured the budget only on a developer's terminal.
|
|
1473
|
+
No heartbeat and no budget is the only case that returns None, and then the
|
|
1474
|
+
loops run exactly as they did before.
|
|
1475
|
+
"""
|
|
1476
|
+
from sourcecode.rule_pass import RulePassProgress
|
|
1477
|
+
|
|
1478
|
+
watching = prog is not None and getattr(prog, "enabled", False)
|
|
1479
|
+
if not watching and stop_when is None:
|
|
1480
|
+
return None
|
|
1481
|
+
|
|
1482
|
+
def sink(done: int, total: int, rule_id: str) -> None:
|
|
1483
|
+
prog.work(
|
|
1484
|
+
done, total,
|
|
1485
|
+
stage=f"{stage} ({rule_id})",
|
|
1486
|
+
unit=unit,
|
|
1487
|
+
estimate=False,
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
return RulePassProgress(
|
|
1491
|
+
sink=sink if watching else None, stop_when=stop_when
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
|
|
1421
1495
|
def _reserialize_as_yaml(content: str) -> str:
|
|
1422
1496
|
"""JSON document → YAML, or the content unchanged when it is not one.
|
|
1423
1497
|
|
|
@@ -7995,29 +8069,64 @@ def spring_audit_cmd(
|
|
|
7995
8069
|
_model = SpringSemanticModel.build(cir)
|
|
7996
8070
|
_run.checkpoint("ir")
|
|
7997
8071
|
# The IR is built; the rule passes are the work now, and they are not
|
|
7998
|
-
# counted per file. Saying so drops the denominator rather
|
|
7999
|
-
# leaving `2766/2766` frozen on the line while they run (C3-56)
|
|
8072
|
+
# counted per file. Saying so drops the per-file denominator rather
|
|
8073
|
+
# than leaving `2766/2766` frozen on the line while they run (C3-56)
|
|
8074
|
+
# — and C3-73 gives the pass the denominator it does have: the rule
|
|
8075
|
+
# families it evaluates, counted as it walks them.
|
|
8000
8076
|
_prog.update(f"applying {scope} rules over {len(file_list)} Java files")
|
|
8001
8077
|
|
|
8002
8078
|
results: list[SpringAuditResult] = []
|
|
8079
|
+
# C3-71: the budget reaches the loop that spends the time. Checking it
|
|
8080
|
+
# only between the three phases meant it could be observed at two
|
|
8081
|
+
# instants in a 39-minute run, and the phase holding ~85 % of the time
|
|
8082
|
+
# was never interrupted — so `ASK_MAX_ANALYSIS_SECONDS=420` produced a
|
|
8083
|
+
# 2 351 s run, exit 0, and no `partial` anywhere. `PhasedRun.exhausted`
|
|
8084
|
+
# stays the one authority on whether the time is spent; what is new is
|
|
8085
|
+
# that the rule pass asks it.
|
|
8086
|
+
_rules_cut: list[str] = []
|
|
8087
|
+
_phases_cut: list[str] = []
|
|
8003
8088
|
if scope in ("all", "tx"):
|
|
8004
8089
|
if _run.exhausted():
|
|
8005
8090
|
_stopped_early = _WHY_BUDGET
|
|
8006
8091
|
else:
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
"tx_audit",
|
|
8010
|
-
SpringAuditResult.merge(list(results), scope=scope).to_compact_dict(),
|
|
8092
|
+
_tx_watch = _rule_pass_progress(
|
|
8093
|
+
_prog, "transaction rules", stop_when=_run.exhausted
|
|
8011
8094
|
)
|
|
8095
|
+
results.append(run_tx_audit(
|
|
8096
|
+
cir, root=target, min_severity=min_severity, model=_model,
|
|
8097
|
+
progress=_tx_watch,
|
|
8098
|
+
))
|
|
8099
|
+
_cut = list(getattr(_tx_watch, "not_run", []) or [])
|
|
8100
|
+
_rules_cut.extend(_cut)
|
|
8101
|
+
if _cut:
|
|
8102
|
+
_stopped_early = _WHY_BUDGET
|
|
8103
|
+
_phases_cut.append("tx_audit")
|
|
8104
|
+
else:
|
|
8105
|
+
_run.checkpoint(
|
|
8106
|
+
"tx_audit",
|
|
8107
|
+
SpringAuditResult.merge(list(results), scope=scope).to_compact_dict(),
|
|
8108
|
+
)
|
|
8012
8109
|
if scope in ("all", "security"):
|
|
8013
8110
|
if _run.exhausted():
|
|
8014
8111
|
_stopped_early = _WHY_BUDGET
|
|
8015
8112
|
else:
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
"security_audit",
|
|
8019
|
-
SpringAuditResult.merge(list(results), scope=scope).to_compact_dict(),
|
|
8113
|
+
_sec_watch = _rule_pass_progress(
|
|
8114
|
+
_prog, "security rules", stop_when=_run.exhausted
|
|
8020
8115
|
)
|
|
8116
|
+
results.append(run_security_audit(
|
|
8117
|
+
cir, root=target, min_severity=min_severity, model=_model,
|
|
8118
|
+
progress=_sec_watch,
|
|
8119
|
+
))
|
|
8120
|
+
_cut = list(getattr(_sec_watch, "not_run", []) or [])
|
|
8121
|
+
_rules_cut.extend(_cut)
|
|
8122
|
+
if _cut:
|
|
8123
|
+
_stopped_early = _WHY_BUDGET
|
|
8124
|
+
_phases_cut.append("security_audit")
|
|
8125
|
+
else:
|
|
8126
|
+
_run.checkpoint(
|
|
8127
|
+
"security_audit",
|
|
8128
|
+
SpringAuditResult.merge(list(results), scope=scope).to_compact_dict(),
|
|
8129
|
+
)
|
|
8021
8130
|
|
|
8022
8131
|
combined = SpringAuditResult.merge(results, scope=scope)
|
|
8023
8132
|
|
|
@@ -8040,17 +8149,43 @@ def spring_audit_cmd(
|
|
|
8040
8149
|
# The answer is a floor over the phases that ran. It says so in
|
|
8041
8150
|
# the payload rather than in prose nobody parses, and in the
|
|
8042
8151
|
# limitation list the reader already consults.
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8152
|
+
if _rules_cut:
|
|
8153
|
+
# C3-71: the phase was stopped *inside*, so saying it "did not
|
|
8154
|
+
# run" would be false — four of its five families answered.
|
|
8155
|
+
# Name what did not run, at the granularity it was cut on.
|
|
8156
|
+
combined.limitations.append(
|
|
8157
|
+
"Partial answer: the analysis budget ran out during the "
|
|
8158
|
+
f"rule pass, after {', '.join(_run.completed)}. The rule "
|
|
8159
|
+
f"families that did not run: {', '.join(_rules_cut)}. Counts "
|
|
8160
|
+
"are a floor over what did run — absence of a finding from a "
|
|
8161
|
+
"family that never ran is not evidence there is none."
|
|
8162
|
+
)
|
|
8163
|
+
else:
|
|
8164
|
+
combined.limitations.append(
|
|
8165
|
+
"Partial answer: the analysis budget ran out after "
|
|
8166
|
+
f"{', '.join(_run.completed)}; "
|
|
8167
|
+
f"{', '.join(_run.pending())} did not run. Counts are a floor "
|
|
8168
|
+
"over the completed phases — absence of a finding from a "
|
|
8169
|
+
"phase that never ran is not evidence there is none."
|
|
8170
|
+
)
|
|
8050
8171
|
|
|
8051
8172
|
data = combined.to_compact_dict() if compact else combined.to_dict()
|
|
8052
8173
|
if _stopped_early:
|
|
8053
8174
|
data["_partial"] = _run.status(_stopped_early)
|
|
8175
|
+
if _rules_cut:
|
|
8176
|
+
# C3-71: a phase stopped *inside* is neither completed nor
|
|
8177
|
+
# pending, and calling it either would misreport the run. The
|
|
8178
|
+
# families it did not reach are named instead.
|
|
8179
|
+
data["_partial"]["rules_not_run"] = _rules_cut
|
|
8180
|
+
# A phase that was stopped inside is in neither list, and
|
|
8181
|
+
# putting it in `phases_pending` alone would claim none of it
|
|
8182
|
+
# ran when most of it did.
|
|
8183
|
+
data["_partial"]["phases_stopped_inside"] = _phases_cut
|
|
8184
|
+
data["_partial"]["how_to_read"] += (
|
|
8185
|
+
" The rule pass was stopped between families: "
|
|
8186
|
+
f"{', '.join(_rules_cut)} did not run, so every count here "
|
|
8187
|
+
"is a floor over the families that did."
|
|
8188
|
+
)
|
|
8054
8189
|
|
|
8055
8190
|
# Non-fatal RIS side-effect — persist summary only (not full findings).
|
|
8056
8191
|
try:
|
|
@@ -8275,6 +8410,15 @@ def verify_cmd(
|
|
|
8275
8410
|
"--ci/--no-ci",
|
|
8276
8411
|
help="Exit non-zero on a failing/unverified report (default on — for pipelines).",
|
|
8277
8412
|
),
|
|
8413
|
+
allow_unverified: bool = typer.Option(
|
|
8414
|
+
False,
|
|
8415
|
+
"--allow-unverified",
|
|
8416
|
+
help=(
|
|
8417
|
+
"Exit 0 when nothing could be verified (no contracts declared yet, or "
|
|
8418
|
+
"the gate could not run). The verdict stays `unverified` in the payload "
|
|
8419
|
+
"— this is the declared form of `|| true`, and violations still block."
|
|
8420
|
+
),
|
|
8421
|
+
),
|
|
8278
8422
|
copy: bool = _copy_option(),
|
|
8279
8423
|
progress_mode: Optional[str] = _progress_option(),
|
|
8280
8424
|
) -> None:
|
|
@@ -8301,6 +8445,14 @@ def verify_cmd(
|
|
|
8301
8445
|
repository could not be analysed — never a silent pass). Same codes as
|
|
8302
8446
|
`verify-edit`.
|
|
8303
8447
|
|
|
8448
|
+
\b
|
|
8449
|
+
Adopting the gate (C3-75): the payload names which unverified state it is —
|
|
8450
|
+
`unverified_reason: no_contracts_declared` (this repository has not adopted
|
|
8451
|
+
contracts) or `gate_could_not_run`. A pipeline added before the contracts exist
|
|
8452
|
+
can pass `--allow-unverified` to exit 0 while the verdict stays `unverified`;
|
|
8453
|
+
violations still block. That is the declared version of `|| true`, which is what
|
|
8454
|
+
teams actually reach for and which then hides a real failure forever.
|
|
8455
|
+
|
|
8304
8456
|
\b
|
|
8305
8457
|
Where the repository already keeps an architectural history (`.ask/baselines`),
|
|
8306
8458
|
this run adds the current commit to it and reports what it did under
|
|
@@ -8419,7 +8571,8 @@ def verify_cmd(
|
|
|
8419
8571
|
_prog.start("verifying contracts")
|
|
8420
8572
|
try:
|
|
8421
8573
|
report = _vr.verify_repo(
|
|
8422
|
-
path, fail_on=fail_on, baseline_path=baseline, on_cir=_capture
|
|
8574
|
+
path, fail_on=fail_on, baseline_path=baseline, on_cir=_capture,
|
|
8575
|
+
allow_unverified=allow_unverified,
|
|
8423
8576
|
)
|
|
8424
8577
|
except _vr.VerifyError as exc:
|
|
8425
8578
|
_prog.stop()
|
|
@@ -8571,11 +8724,34 @@ def risk_cmd(
|
|
|
8571
8724
|
# without a single byte on stderr while it ran.
|
|
8572
8725
|
phase = "composing risk factors"
|
|
8573
8726
|
risk_limit = 100000 if table and (rule_filter or band_filter or top_n) else limit
|
|
8727
|
+
# C3-77: 3 232 s in the field, all-or-nothing, while `spring-audit` beside it
|
|
8728
|
+
# already kept what it measured (C3-54 part 3). The phases are declared here so
|
|
8729
|
+
# a run that is killed — by an agent harness, a CI timeout, a closed laptop —
|
|
8730
|
+
# leaves the composition it already paid for, with `partial: true` and the
|
|
8731
|
+
# phase it reached. Same mechanism, same durable write (fsync + atomic rename),
|
|
8732
|
+
# same invariant: never a document that looks complete.
|
|
8733
|
+
_run = PhasedRun(
|
|
8734
|
+
command="risk",
|
|
8735
|
+
root=path,
|
|
8736
|
+
phases=["audit", "compose"],
|
|
8737
|
+
output_path=output_path,
|
|
8738
|
+
budget_seconds=_analysis_budget("risk").get("configured_max_seconds"),
|
|
8739
|
+
writer=_safe_write_file,
|
|
8740
|
+
)
|
|
8574
8741
|
with _expensive_analysis_scope("risk", path, phase):
|
|
8575
8742
|
_prog = Progress()
|
|
8576
8743
|
_prog.start(phase)
|
|
8577
8744
|
try:
|
|
8578
|
-
data = build_risk(
|
|
8745
|
+
data = build_risk(
|
|
8746
|
+
path, limit=risk_limit, min_band=min_band,
|
|
8747
|
+
profiles=_profile_set(profile),
|
|
8748
|
+
# C3-73: 54 minutes with one line on stderr in the field. The
|
|
8749
|
+
# factory hands the composer an observer per counted stage.
|
|
8750
|
+
progress=lambda stage, unit="rule families": _rule_pass_progress(
|
|
8751
|
+
_prog, stage, unit
|
|
8752
|
+
),
|
|
8753
|
+
checkpoint=_run.checkpoint,
|
|
8754
|
+
)
|
|
8579
8755
|
finally:
|
|
8580
8756
|
_prog.stop()
|
|
8581
8757
|
if table:
|
|
@@ -8593,6 +8769,9 @@ def risk_cmd(
|
|
|
8593
8769
|
f"{data['total_defects']} defects composed)"
|
|
8594
8770
|
),
|
|
8595
8771
|
)
|
|
8772
|
+
# The complete answer is on disk; the checkpoint that stood in for it is not
|
|
8773
|
+
# needed and must not outlive it (C3-54 part 3's invariant, C3-77's reuse).
|
|
8774
|
+
_run.discard_checkpoint()
|
|
8596
8775
|
|
|
8597
8776
|
|
|
8598
8777
|
@app.command("enrich")
|
|
@@ -9657,6 +9836,14 @@ def migrate_check_cmd(
|
|
|
9657
9836
|
first→last movement — no improving/degrading label, and readiness_score is
|
|
9658
9837
|
flagged not-comparable when the applicable dimension set changed.
|
|
9659
9838
|
|
|
9839
|
+
\b
|
|
9840
|
+
What this command writes (C3-74):
|
|
9841
|
+
Nothing, unless you pass --snapshot. Then, and only then, it creates
|
|
9842
|
+
<repo>/.ask/readiness-history (or --history-dir), announces the directory on
|
|
9843
|
+
stderr the first time, and protects it with its own .gitignore. Point
|
|
9844
|
+
--history-dir outside the repository for a read-only checkout or an audit
|
|
9845
|
+
where the tree must not change.
|
|
9846
|
+
|
|
9660
9847
|
\b
|
|
9661
9848
|
Java LTS/licensing inventory:
|
|
9662
9849
|
The JSON report includes java_lts_inventory: Java 8/11/17/21/25 evidence
|
|
@@ -9691,12 +9878,12 @@ def migrate_check_cmd(
|
|
|
9691
9878
|
raise typer.Exit(code=1)
|
|
9692
9879
|
|
|
9693
9880
|
_history_dir = history_dir.resolve() if history_dir else (target / ".ask" / "readiness-history")
|
|
9694
|
-
# C3-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9881
|
+
# C3-74: the protection belongs at the write, not at the argument parse.
|
|
9882
|
+
# `protect_ask_dir` *creates* `.ask/` so it can drop its ignore file in it, and
|
|
9883
|
+
# calling it here ran it on every invocation — so a read-only question left a
|
|
9884
|
+
# 283-byte artefact inside the audited repository, and the field bisected
|
|
9885
|
+
# eleven commands to find that this was the only one doing it. The write path
|
|
9886
|
+
# (`--snapshot`) calls it below, where something is actually being written.
|
|
9700
9887
|
|
|
9701
9888
|
# --trend reads the stored series and reports movement over time; it does not scan.
|
|
9702
9889
|
if trend:
|
|
@@ -12798,6 +12985,17 @@ def cache_status_cmd(
|
|
|
12798
12985
|
_answer.say(
|
|
12799
12986
|
f"{_label + ':':<13}{_store.get('entries', 0)} entries, {_mb} MB{_scope}"
|
|
12800
12987
|
)
|
|
12988
|
+
# C3-70: one total hid three copies of the same repository, one per
|
|
12989
|
+
# release, for three releases. The composition is printed where the
|
|
12990
|
+
# total is, because that is where it was read.
|
|
12991
|
+
_stale_entries = _store.get("stale_entries")
|
|
12992
|
+
if _stale_entries:
|
|
12993
|
+
_stale_mb = round(int(_store.get("stale_bytes") or 0) / (1024 * 1024), 2)
|
|
12994
|
+
_answer.say(
|
|
12995
|
+
f"{'':<13}of which {_stale_entries} entries ({_stale_mb} MB) were "
|
|
12996
|
+
"written by another build and can never be read again — the next "
|
|
12997
|
+
"analysis retires them"
|
|
12998
|
+
)
|
|
12801
12999
|
# RIS section
|
|
12802
13000
|
if stats.get("ris_exists"):
|
|
12803
13001
|
_stale_tag = " [STALE]" if stats.get("ris_is_stale") else ""
|
sourcecode/container_wiring.py
CHANGED
|
@@ -306,6 +306,27 @@ def warning_line(wired: list[ContainerWiring], measured_fan_in: Optional[int] =
|
|
|
306
306
|
)
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
#: Why the numeric score is `null` rather than `0.0` on this population (C1-41).
|
|
310
|
+
#:
|
|
311
|
+
#: The level is floored here because a fan-in-derived band cannot describe a
|
|
312
|
+
#: component the framework invokes. The *number* was left on that same arithmetic
|
|
313
|
+
#: for two releases, so the field measured `risk_level=high` beside
|
|
314
|
+
#: `risk_score=0.0` on the aspect gating 76 % of a repository's handlers — the
|
|
315
|
+
#: worst possible value for its most critical symbol, from the same object whose
|
|
316
|
+
#: band said `high`. `0.0` is a measurement claim; what was not measured is `null`
|
|
317
|
+
#: (C1-39, I-3). One authority for the sentence, because both blast-radius
|
|
318
|
+
#: derivations publish it: `spring_impact` (impact-chain) and `repository_ir`
|
|
319
|
+
#: (impact / plan / compare).
|
|
320
|
+
SCORE_NULL_NOTE = (
|
|
321
|
+
"null, not 0: the container invokes this component, so its call-graph fan-in "
|
|
322
|
+
"is ~0 by construction and the weighted score derived from it is not a "
|
|
323
|
+
"measurement of this symbol (blind_spots=container_wired, NC-010). "
|
|
324
|
+
"`risk_level` is a floor established without it; rank this symbol by the "
|
|
325
|
+
"surfaces that do measure it — `ask endpoints`, `ask explain <symbol>`, "
|
|
326
|
+
"`ask posture` — not by a number the graph could not produce."
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
309
330
|
def risk_floor_reason(wired: list[ContainerWiring]) -> str:
|
|
310
331
|
"""The `risk_reason` that replaces a fan-in verdict on this population."""
|
|
311
332
|
kinds = sorted({k for w in wired for k in w.kinds})
|