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 CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "4.11.0"
7
+ __version__ = "4.14.0"
@@ -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. Delete this file to version them deliberately.\n"
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 #14. Seconds where a run finished; `field_blocked`
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 #13, on
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 **408 s** — 46× the 8.8 s the same command takes on
99
- #: the 2 000-file reference. Cost does not scale with file count in any way this
100
- #: product has measured, so a projected "~15 s on your repository" would be a
101
- #: confident falsehood of exactly the kind the ledger exists to prevent. What can
102
- #: be said honestly is the class, the two measured anchors, and how to run it.
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 #13: spring-audit on 3 342 Java files took 408 s "
105
- "(Windows, pipx, cold) against 8.8 s on the 2 000-file reference"
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 = 408.0
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, field_blocked=True),
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", analysis_class="deep"),
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=4.2),
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=408.0),
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, field_blocked=True),
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"