sourcecode 4.11.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 +50 -2
- sourcecode/cache_model.py +83 -16
- sourcecode/cli.py +425 -28
- 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/parallel.py +241 -0
- sourcecode/parse_cache.py +349 -8
- sourcecode/perf.py +268 -4
- sourcecode/phased_run.py +61 -10
- sourcecode/progress.py +116 -46
- sourcecode/readiness_timeline.py +12 -1
- sourcecode/readonly.py +171 -0
- sourcecode/release_info.py +1 -1
- sourcecode/repository_ir.py +44 -3
- sourcecode/retrieval/steps_impact.py +5 -2
- sourcecode/risk.py +120 -38
- sourcecode/rule_pass.py +236 -0
- sourcecode/runs.py +3 -0
- sourcecode/security_config_scan.py +36 -5
- sourcecode/spring_findings.py +82 -0
- sourcecode/spring_impact.py +29 -3
- sourcecode/spring_security_audit.py +146 -9
- sourcecode/spring_tx_analyzer.py +110 -10
- sourcecode/verify_edit.py +3 -0
- sourcecode/verify_repo.py +62 -9
- {sourcecode-4.11.0.dist-info → sourcecode-4.14.0.dist-info}/METADATA +3 -3
- {sourcecode-4.11.0.dist-info → sourcecode-4.14.0.dist-info}/RECORD +33 -30
- sourcecode/cache.tmp_new +0 -772
- {sourcecode-4.11.0.dist-info → sourcecode-4.14.0.dist-info}/WHEEL +0 -0
- {sourcecode-4.11.0.dist-info → sourcecode-4.14.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.11.0.dist-info → sourcecode-4.14.0.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -30,12 +30,14 @@ 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
|
|
36
37
|
from typing import TYPE_CHECKING, Any
|
|
37
38
|
|
|
38
39
|
from sourcecode import __version__ as TOOL_VERSION
|
|
40
|
+
from sourcecode import readonly as _readonly
|
|
39
41
|
from sourcecode.architectural_delta import ArchMetrics, diff_metrics, extract_metrics
|
|
40
42
|
from sourcecode.perf import collect_env
|
|
41
43
|
|
|
@@ -365,21 +367,40 @@ def _baseline_filename(baseline: dict) -> str:
|
|
|
365
367
|
#: a 14 587-entry structural inventory of the code. A self-confined
|
|
366
368
|
#: `.ask/.gitignore` is the honest form: it never touches the user's own
|
|
367
369
|
#: `.gitignore`, and it stops applying the moment they delete the directory.
|
|
370
|
+
#: C3-74 corrected the last line. It used to read *"Delete this file to version
|
|
371
|
+
#: them deliberately"*, and deleting it is the one action that would commit a
|
|
372
|
+
#: baseline carrying `env.hostname_hash`, `cpu` and `cores` — an invitation to do
|
|
373
|
+
#: the thing the file exists to prevent. It now states the consequence instead.
|
|
368
374
|
_ASK_DIR_GITIGNORE = (
|
|
369
375
|
"# Written by `ask` the first time it created this directory.\n"
|
|
370
376
|
"# These are local analysis artefacts: a structural inventory of your code\n"
|
|
371
377
|
"# and, if you asked for it, the machine that measured it. Nothing here\n"
|
|
372
|
-
"# belongs in a commit
|
|
378
|
+
"# belongs in a commit.\n"
|
|
379
|
+
"#\n"
|
|
380
|
+
"# Removing this file makes everything here committable, including any\n"
|
|
381
|
+
"# recorded machine fingerprint (`--record-env`: hostname hash, CPU, cores).\n"
|
|
382
|
+
"# Version these deliberately only if you intend to publish that.\n"
|
|
373
383
|
"*\n"
|
|
374
384
|
)
|
|
375
385
|
|
|
376
386
|
|
|
377
|
-
def protect_ask_dir(out_dir: Path) -> None:
|
|
387
|
+
def protect_ask_dir(out_dir: Path, *, announce: bool = True) -> None:
|
|
378
388
|
"""Give a freshly created `.ask/` its own `.gitignore` (C3-68).
|
|
379
389
|
|
|
380
390
|
Best-effort and never overwriting: a user who edited it decided something,
|
|
381
391
|
and a tool that re-imposes its default on every run is worse than one that
|
|
382
392
|
never wrote it.
|
|
393
|
+
|
|
394
|
+
**Call this where the write happens, never where the path is computed.** This
|
|
395
|
+
function *creates* the directory in order to protect it, so calling it
|
|
396
|
+
speculatively is a write — which is how `migrate-check` came to leave a
|
|
397
|
+
`.ask/.gitignore` inside a repository it had only read (C3-74).
|
|
398
|
+
|
|
399
|
+
`announce` puts one line on stderr the first time the directory is created,
|
|
400
|
+
with its path. A side effect inside somebody else's repository is exactly what
|
|
401
|
+
I-8 says must be declared, and this one hides from the check an auditor makes:
|
|
402
|
+
`git status --porcelain` is clean because the ignore file ignores itself, and
|
|
403
|
+
only `--ignored` reveals `!! <scope>/.ask/`.
|
|
383
404
|
"""
|
|
384
405
|
try:
|
|
385
406
|
ask_dir = Path(out_dir)
|
|
@@ -391,14 +412,41 @@ def protect_ask_dir(out_dir: Path) -> None:
|
|
|
391
412
|
return
|
|
392
413
|
marker = ask_dir / ".gitignore"
|
|
393
414
|
if not marker.exists():
|
|
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")
|
|
394
419
|
ask_dir.mkdir(parents=True, exist_ok=True)
|
|
395
420
|
marker.write_text(_ASK_DIR_GITIGNORE, encoding="utf-8")
|
|
421
|
+
if announce and created:
|
|
422
|
+
_announce_ask_dir(ask_dir)
|
|
396
423
|
except OSError:
|
|
397
424
|
return
|
|
398
425
|
|
|
399
426
|
|
|
427
|
+
def _announce_ask_dir(ask_dir: Path) -> None:
|
|
428
|
+
"""One line, on stderr, naming what was created and how to move it (C3-74)."""
|
|
429
|
+
import sys
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
# No flag is named here on purpose: the commands that write under `.ask/`
|
|
433
|
+
# spell the override differently (`baseline capture --dir`,
|
|
434
|
+
# `migrate-check --history-dir`), and a message that names the wrong one
|
|
435
|
+
# is the class of defect this ledger is about.
|
|
436
|
+
sys.stderr.write(
|
|
437
|
+
f"[ask] created {ask_dir}{os.sep} for this run's local artefacts — "
|
|
438
|
+
f"git-ignored by its own .gitignore, so `git status` will not show it "
|
|
439
|
+
f"(`git status --ignored` will). The command's directory option takes a "
|
|
440
|
+
f"path outside the repository if you want them elsewhere.\n"
|
|
441
|
+
)
|
|
442
|
+
sys.stderr.flush()
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
|
|
446
|
+
|
|
400
447
|
def write_baseline(baseline: dict, out_dir: Path) -> Path:
|
|
401
448
|
"""Write `baseline` as deterministic JSON under `out_dir`; return the file path."""
|
|
449
|
+
_readonly.guard(out_dir, what="baseline history directory")
|
|
402
450
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
403
451
|
protect_ask_dir(out_dir)
|
|
404
452
|
path = out_dir / _baseline_filename(baseline)
|
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
|
|
|
@@ -92,20 +99,36 @@ REFERENCE_REPOSITORY = (
|
|
|
92
99
|
#: figures are relative to.
|
|
93
100
|
REFERENCE_JAVA_FILES = 2000
|
|
94
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
|
+
|
|
95
111
|
#: 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 #
|
|
112
|
+
#: *class* rather than a projected duration (C4-19). Field evaluation #16, on
|
|
97
113
|
#: 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
|
|
114
|
+
#: 3 342-file repository ran **2 351 s** — 267× the 8.8 s the same command takes
|
|
115
|
+
#: on the 2 000-file reference, for a repository 1,7× the size. Cost does not
|
|
116
|
+
#: scale with file count in any way this product has measured, so a projected
|
|
117
|
+
#: "~15 s on your repository" would be a confident falsehood of exactly the kind
|
|
118
|
+
#: the ledger exists to prevent. What can be said honestly is the class, the two
|
|
119
|
+
#: measured anchors, and how to run it.
|
|
120
|
+
#:
|
|
121
|
+
#: C3-72 replaced evaluation #13's 408 s with #16's 2 351 s here: the same command
|
|
122
|
+
#: on the same repository at the same commit, measured three releases later
|
|
123
|
+
#: (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
|
|
124
|
+
#: the one a budget message quoted as "~600s" for three releases while the
|
|
125
|
+
#: measurement stood four times higher.
|
|
103
126
|
FIELD_ANCHOR = (
|
|
104
|
-
"field evaluation #
|
|
105
|
-
"(Windows, pipx,
|
|
127
|
+
"field evaluation #16: spring-audit on 3 342 Java files took 2 351 s "
|
|
128
|
+
"(Windows, pipx, warm cache) against 8.8 s on the 2 000-file reference"
|
|
106
129
|
)
|
|
107
130
|
FIELD_ANCHOR_JAVA_FILES = 3342
|
|
108
|
-
FIELD_ANCHOR_SECONDS =
|
|
131
|
+
FIELD_ANCHOR_SECONDS = 2351.4
|
|
109
132
|
|
|
110
133
|
|
|
111
134
|
#: Every layer keys on `cache.worktree_signature` — the exact tree state (C1-9).
|
|
@@ -150,7 +173,9 @@ LAYERS: tuple[Layer, ...] = (
|
|
|
150
173
|
location="~/.sourcecode/parse-cache-v1/",
|
|
151
174
|
invalidated_by=(
|
|
152
175
|
"the file's own bytes and the extractor's source — content-addressed, "
|
|
153
|
-
"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)"
|
|
154
179
|
),
|
|
155
180
|
warmed="filled for every Java file the warm parses",
|
|
156
181
|
),
|
|
@@ -176,14 +201,15 @@ COMMANDS: tuple[CommandCache, ...] = (
|
|
|
176
201
|
"Resolves the conditional bean graph on every run, over the shared CIR a warm "
|
|
177
202
|
"builds — the parse it used to repeat for itself. `--diff` compares two profile "
|
|
178
203
|
"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,
|
|
204
|
+
"10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6, field_seconds=38.0),
|
|
180
205
|
CommandCache("risk", ("cir", "parse"), "shared", False,
|
|
181
206
|
"Composes what the audit, impact-chain and the posture already answer, so it "
|
|
182
207
|
"pays each of their costs once over the shared CIR a warm builds — one parse "
|
|
183
208
|
"for the whole composition, and the reachability query is cached per symbol "
|
|
184
209
|
"within the run.",
|
|
185
210
|
"not measured on the battery yet — the composition is bounded by the "
|
|
186
|
-
"`spring-audit` + `impact-chain` costs listed here, not by new analysis",
|
|
211
|
+
"`spring-audit` + `impact-chain` costs listed here, not by new analysis",
|
|
212
|
+
analysis_class="deep", field_seconds=3232.2),
|
|
187
213
|
CommandCache("enrich", ("cir", "parse"), "shared", False,
|
|
188
214
|
"Runs the same composition as `risk` over the repository, then joins a SARIF "
|
|
189
215
|
"log to it. Reading the log is negligible; everything a warm helps with is the "
|
|
@@ -213,14 +239,14 @@ COMMANDS: tuple[CommandCache, ...] = (
|
|
|
213
239
|
"Recomputes the endpoint surface on every run, over a parse a warm has already "
|
|
214
240
|
"paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
|
|
215
241
|
"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=
|
|
242
|
+
"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
243
|
CommandCache("spring-audit", ("ris", "parse"), "shared", False,
|
|
218
244
|
"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=
|
|
245
|
+
"8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=2351.4),
|
|
220
246
|
CommandCache("migrate-check", ("cir",), "none", False,
|
|
221
247
|
"Computes its own inventory and shares nothing a warm builds. Only `--blast-radius` "
|
|
222
248
|
"reuses the shared CIR.",
|
|
223
|
-
"4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8,
|
|
249
|
+
"4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8, field_seconds=26.7),
|
|
224
250
|
CommandCache("impact-chain", ("cir", "parse"), "shared", False,
|
|
225
251
|
"The CIR is the expensive half — this is where a warm pays most.",
|
|
226
252
|
"9.9 s → 1.7 s", analysis_class="core", cold_seconds=9.9, warm_seconds=1.7),
|
|
@@ -343,6 +369,34 @@ class Conditioning:
|
|
|
343
369
|
)
|
|
344
370
|
|
|
345
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
|
+
|
|
346
400
|
def as_dict(here: "Optional[Conditioning]" = None) -> dict:
|
|
347
401
|
"""The whole model as data — what `ask cache model --json` emits."""
|
|
348
402
|
conditioned: dict = {}
|
|
@@ -371,6 +425,9 @@ def as_dict(here: "Optional[Conditioning]" = None) -> dict:
|
|
|
371
425
|
],
|
|
372
426
|
"reference_repository": REFERENCE_REPOSITORY,
|
|
373
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(),
|
|
374
431
|
# Published beside the reference so no consumer reads the figures as a
|
|
375
432
|
# law: the same command has been measured 46× slower on a repository 1.7×
|
|
376
433
|
# the size, on a different platform (C4-19).
|
|
@@ -412,6 +469,15 @@ def render_markdown() -> str:
|
|
|
412
469
|
out.append("")
|
|
413
470
|
out.append(f"Measured on {REFERENCE_REPOSITORY}, each command in isolation.")
|
|
414
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("")
|
|
415
481
|
out.append("| Command | A warm gives it | Measured (nothing cached → after a warm) | Repeat run cached | Layers | Notes |")
|
|
416
482
|
out.append("|---|---|---|---|---|---|")
|
|
417
483
|
for cmd in COMMANDS:
|
|
@@ -489,6 +555,7 @@ def render_text(here: "Optional[Conditioning]" = None) -> str:
|
|
|
489
555
|
lines.append("")
|
|
490
556
|
lines.append("What a warm gives each command")
|
|
491
557
|
lines.append(f" Timings: {REFERENCE_REPOSITORY}, each command measured in isolation.")
|
|
558
|
+
lines.append(f" {reference_currency()['statement']}")
|
|
492
559
|
width = max(len(c.command) for c in COMMANDS)
|
|
493
560
|
for cmd in COMMANDS:
|
|
494
561
|
repeat = "repeat cached" if cmd.repeat else "recomputes"
|