sourcecode 4.18.0__py3-none-any.whl → 5.0.1__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.18.0"
7
+ __version__ = "5.0.1"
sourcecode/cache_model.py CHANGED
@@ -74,11 +74,11 @@ class CommandCache:
74
74
  cold_seconds: "Optional[float]" = None
75
75
  warm_seconds: "Optional[float]" = None
76
76
  #: The other measured point: this command on the field repository
77
- #: (3 342 Java files, Windows 11 / PowerShell 5.1 / pipx, cold), from field
78
- #: evaluations #13, #14 and #16. Seconds where a run finished; `field_blocked`
79
- #: where it did not — the harness promoted the process and the session died,
80
- #: which is a measurement of a different kind and is never written as a
81
- #: duration. Rows with neither were not attempted there.
77
+ #: (3 342 Java files, Windows 11 / PowerShell 5.1 / pipx, warm), from the
78
+ #: field evaluations. Seconds where a run finished; `field_blocked` where it
79
+ #: did not — the harness promoted the process and the session died, which is a
80
+ #: measurement of a different kind and is never written as a duration. Rows
81
+ #: with neither were not attempted there.
82
82
  #:
83
83
  #: C3-72: where two evaluations of the same command at this size disagree, the
84
84
  #: figure here is the **most recent** one, because it describes the build a
@@ -87,7 +87,39 @@ class CommandCache:
87
87
  #: releases (spring-audit, 408 s in #13 against 2 351 s in #16) is itself the
88
88
  #: measurement C3-76 exists for.
89
89
  field_seconds: "Optional[float]" = None
90
+ #: **C3-100.** `field_blocked` is a *measurement outcome*, not a property of the
91
+ #: command: it says "this run did not finish on that build, at that size", and
92
+ #: like every other measurement here it belongs to the build named in
93
+ #: `field_measured_version` and expires with it. It was read as a property for
94
+ #: three releases — `repo-ir` and `verify-edit` carried a 4.10.4 outcome while
95
+ #: eval #23 ran them in 9,8 s and 76,6 s — which is worse than a stale number,
96
+ #: because a stale number is a measurement and *"the field could not run this"*
97
+ #: is an incapacity the product does not have. A blocked row without a build is
98
+ #: rejected by the battery for the same reason a figure without one is.
90
99
  field_blocked: bool = False
100
+ #: The cache state the field figure was taken in. Eval #22 measured a warm
101
+ #: machine; eval #23 purged every cache first (`cache clear --all`,
102
+ #: `context-clear`, `rm -rf parse-cache-v1`) and then ran the whole catalogue
103
+ #: in one round, so its first figures are cold and its later ones are neither —
104
+ #: the shared layers filled as the round went on. Published because *72,3 s
105
+ #: cold* and *72,3 s warm* are two different facts about the same command, and
106
+ #: the row that says which is the only thing standing between the two:
107
+ #: ``warm`` — the caches were warmed before the run
108
+ #: ``cold`` — every cache was purged before the run
109
+ #: ``mixed`` — inside a round that began purged: earlier commands had already
110
+ #: filled some of the shared layers this one reads
111
+ field_cache_state: str = "warm"
112
+ #: **C3-97.** The build the field figure was taken on. An anchor without one is
113
+ #: a measurement that can outlive its build and keep deciding: 4.18.0 deleted
114
+ #: two catastrophic-backtracking patterns (C3-95, C3-96) and every figure taken
115
+ #: before it describes a cost that no longer exists — `spring-audit` at 2 351 s
116
+ #: where the same command on the same commit of the same repository now takes
117
+ #: 8,4 s. Anchors older than the running build are still published, because a
118
+ #: measurement is worth more than silence; what changes is that they are
119
+ #: **labelled** with the build they belong to, so a reader can see the figure is
120
+ #: not this one's. `field_anchor_note()` renders that label and
121
+ #: `execution_plan` appends it to the `here:` line.
122
+ field_measured_version: "Optional[str]" = None
91
123
 
92
124
 
93
125
  #: Where every `measured` figure comes from.
@@ -109,26 +141,37 @@ REFERENCE_JAVA_FILES = 2000
109
141
  REFERENCE_MEASURED_VERSION = "3.2.2"
110
142
 
111
143
  #: The other end of the measured range, and the reason this module publishes a
112
- #: *class* rather than a projected duration (C4-19). Field evaluation #16, on
144
+ #: *class* rather than a projected duration (C4-19). Field evaluation #22, on
113
145
  #: Windows 11 / PowerShell 5.1 / pipx / Python 3.10: `spring-audit` on a
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.
146
+ #: 3 342-file repository ran **8,4 s** — the same command takes 8.8 s on the
147
+ #: 2 000-file reference, for a repository 1,7× the size.
120
148
  #:
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.
126
- FIELD_ANCHOR = (
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"
129
- )
149
+ #: **The refusal to project stands, and this evaluation strengthened it** (C3-97).
150
+ #: For eight releases the pair of anchors said *cost does not track file count*;
151
+ #: what the series actually shows is that **cost tracks the build**. The identical
152
+ #: command on the identical commit of the identical repository measured 2 557 s
153
+ #: (4.10.6), 2 398 s (4.10.7), 2 351 s (4.11.0), 2 377 s (4.12.0), 1 371 s
154
+ #: (4.16.0) and 8,4 s (4.18.0) — a 280× move produced by deleting two
155
+ #: catastrophic-backtracking patterns (C3-95, C3-96), with every payload
156
+ #: byte-identical throughout. A projection would therefore have to predict our own
157
+ #: next release, which is the confident falsehood the ledger exists to prevent.
158
+ #: What can be said honestly is the class, the two measured anchors, **the build
159
+ #: each was measured on**, and how to run it.
160
+ #:
161
+ #: History of the anchor, kept because it is the measurement C3-76 exists for:
162
+ #: 408 s (#13) → 2 351 s (#16, C3-72) → 8,4 s (#22, C3-97). The middle figure was
163
+ #: quoted as an operational anchor for four releases after the build that produced
164
+ #: it, and told operators to detach commands that finish in seconds.
165
+ #:
166
+ #: ⚠ `FIELD_ANCHOR`, `FIELD_ANCHOR_SECONDS` and `FIELD_ANCHOR_MEASURED_VERSION`
167
+ #: are **derived from the `spring-audit` row**, below `COMMANDS`. They used to be
168
+ #: written out here as well, which made the anchor two facts kept in step by hand:
169
+ #: C3-97 refreshed both, and C3-100 is what it costs when the next refresh reaches
170
+ #: only one of the copies. One authority per fact — the table — and the headline
171
+ #: reads itself off it.
172
+ #:
173
+ #: The size of the field repository every `field_seconds` below was taken on.
130
174
  FIELD_ANCHOR_JAVA_FILES = 3342
131
- FIELD_ANCHOR_SECONDS = 2351.4
132
175
 
133
176
 
134
177
  #: Every layer keys on `cache.worktree_signature` — the exact tree state (C1-9).
@@ -194,14 +237,17 @@ COMMANDS: tuple[CommandCache, ...] = (
194
237
  CommandCache("ask (root)", ("snapshot", "ris", "parse"), "answer", True,
195
238
  "`--compact` is what a warm stores by default; `--agent` needs `cache warm --agent`. "
196
239
  "`--env-map`, `--depth N` and `--exclude` change the *analysis*, so they miss the "
197
- "warmed core and rescan — this is the 171 s the field measured after a 103 s warm.",
240
+ "warmed core and rescan — the 171 s the field measured after a 103 s warm on 4.10.4. "
241
+ "Eval #23 timed the same invocation at 72,3 s from a purged cache, and then paid "
242
+ "71,7 s again for `--agent` on the state it had just analysed (C3-103).",
198
243
  "--compact 13.3 s cold → 0.3 s warm (cold re-measured on 3.7.0: was 19.3 s, C3-6); "
199
- "--agent --full --env-map --depth 20 34.7 s → 33.9 s (no gain)", analysis_class="repo-wide", cold_seconds=13.3, warm_seconds=0.3, field_seconds=115.0),
244
+ "--agent --full --env-map --depth 20 34.7 s → 33.9 s (no gain)", analysis_class="repo-wide", cold_seconds=13.3, warm_seconds=0.3,
245
+ field_seconds=72.3, field_cache_state="cold", field_measured_version="5.0.0"),
200
246
  CommandCache("posture", ("cir", "parse"), "shared", False,
201
247
  "Resolves the conditional bean graph on every run, over the shared CIR a warm "
202
248
  "builds — the parse it used to repeat for itself. `--diff` compares two profile "
203
249
  "sets over that one IR, so the second side costs the resolution only.",
204
- "10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6, field_seconds=38.0),
250
+ "10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6, field_seconds=8.0, field_measured_version="4.18.0"),
205
251
  CommandCache("risk", ("cir", "parse"), "shared", False,
206
252
  "Composes what the audit, impact-chain and the posture already answer, so it "
207
253
  "pays each of their costs once over the shared CIR a warm builds — one parse "
@@ -209,7 +255,7 @@ COMMANDS: tuple[CommandCache, ...] = (
209
255
  "within the run.",
210
256
  "not measured on the battery yet — the composition is bounded by the "
211
257
  "`spring-audit` + `impact-chain` costs listed here, not by new analysis",
212
- analysis_class="deep", field_seconds=3232.2),
258
+ analysis_class="deep", field_seconds=73.3, field_measured_version="4.18.0"),
213
259
  CommandCache("enrich", ("cir", "parse"), "shared", False,
214
260
  "Runs the same composition as `risk` over the repository, then joins a SARIF "
215
261
  "log to it. Reading the log is negligible; everything a warm helps with is the "
@@ -239,25 +285,33 @@ COMMANDS: tuple[CommandCache, ...] = (
239
285
  "Recomputes the endpoint surface on every run, over a parse a warm has already "
240
286
  "paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
241
287
  "the shared parse cache, and a warm measurably bought it nothing (C3-6).",
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),
288
+ "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=3.8, field_measured_version="4.18.0"),
243
289
  CommandCache("spring-audit", ("ris", "parse"), "shared", False,
244
290
  "Recomputes every run, but over a parse a warm has already paid for.",
245
- "8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=2351.4),
291
+ "8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=8.4, field_measured_version="4.18.0"),
246
292
  CommandCache("migrate-check", ("cir",), "none", False,
247
293
  "Computes its own inventory and shares nothing a warm builds. Only `--blast-radius` "
248
294
  "reuses the shared CIR.",
249
- "4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8, field_seconds=26.7),
295
+ "4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8, field_seconds=10.2, field_measured_version="4.18.0"),
250
296
  CommandCache("impact-chain", ("cir", "parse"), "shared", False,
251
297
  "The CIR is the expensive half — this is where a warm pays most.",
252
- "9.9 s → 1.7 s", analysis_class="core", cold_seconds=9.9, warm_seconds=1.7),
253
- CommandCache("impact", ("parse",), "shared", False, "", "4.9 s → 2.8 s", analysis_class="core", cold_seconds=4.9, warm_seconds=2.8),
298
+ "9.9 s → 1.7 s", analysis_class="core", cold_seconds=9.9, warm_seconds=1.7,
299
+ field_seconds=5.5, field_measured_version="4.18.0"),
300
+ CommandCache("impact", ("parse",), "shared", False, "", "4.9 s → 2.8 s", analysis_class="core", cold_seconds=4.9, warm_seconds=2.8,
301
+ field_seconds=7.7, field_measured_version="4.18.0"),
254
302
  CommandCache("pr-impact", ("parse",), "shared", False,
255
- "Diff-dependent: the answer itself is never stored.", analysis_class="core"),
303
+ "Diff-dependent: the answer itself is never stored. What it costs follows the "
304
+ "diff, not the repository: 12,5 s on an ordinary one and 11,8 s on a diff of "
305
+ "security configuration, measured at field scale.", analysis_class="core",
306
+ field_seconds=12.5, field_measured_version="4.18.0"),
256
307
  CommandCache("verify", (), "none", False, "Runs the contracts against a fresh reading.", analysis_class="core"),
257
308
  CommandCache("verify-edit", ("parse",), "shared", True,
258
309
  "Built for the edit loop: the parse cache is what keeps an unchanged file out of the "
259
- "next run. Its own second run is faster again.",
260
- "14.6 s → 9.6 s → 5.6 s on repeat", analysis_class="repo-wide", cold_seconds=14.6, warm_seconds=9.6, field_blocked=True),
310
+ "next run. Its own second run is faster again. At field size that loop is not short "
311
+ "yet: eval #23 measured 76,6 s on a tree with no edits, because the HEAD side is "
312
+ "still built in a throwaway worktree instead of reusing the shared CIR (C3-102).",
313
+ "14.6 s → 9.6 s → 5.6 s on repeat", analysis_class="repo-wide", cold_seconds=14.6, warm_seconds=9.6,
314
+ field_seconds=76.6, field_cache_state="mixed", field_measured_version="5.0.0"),
261
315
  CommandCache("review-pr", ("cir",), "none", False,
262
316
  "Diff-dependent, and it reuses the CIR only if one exists. On a small diff, loading "
263
317
  "the warmed CIR costs more than the work it saves.",
@@ -286,19 +340,33 @@ COMMANDS: tuple[CommandCache, ...] = (
286
340
  "WITHOUT a warm, a second identical run: 10.2 s → 9.4 s (5 486 files) — "
287
341
  "no answer hit", analysis_class="repo-wide", cold_seconds=6.5, warm_seconds=0.3),
288
342
  CommandCache("explain", ("cir",), "shared", False, "Serves from the shared CIR a warm builds.",
289
- "9.8 s → 1.6 s", analysis_class="core", cold_seconds=9.8, warm_seconds=1.6),
343
+ "9.8 s → 1.6 s", analysis_class="core", cold_seconds=9.8, warm_seconds=1.6,
344
+ field_seconds=4.4, field_measured_version="4.18.0"),
290
345
  CommandCache("export", ("parse",), "shared", False, "", "8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7),
291
- CommandCache("repo-ir", ("parse",), "shared", False, "", "5.1 s → 2.9 s", analysis_class="repo-wide", cold_seconds=5.1, warm_seconds=2.9, field_blocked=True),
292
- CommandCache("validation", ("parse",), "shared", False, "", "11.6 s → 6.4 s", analysis_class="repo-wide", cold_seconds=11.6, warm_seconds=6.4),
293
- CommandCache("modernize", ("parse",), "shared", False, "", "5.2 s → 3.0 s", analysis_class="repo-wide", cold_seconds=5.2, warm_seconds=3.0, field_blocked=True),
346
+ CommandCache("repo-ir", ("parse",), "shared", False,
347
+ "Carried as *did not finish* from 4.10.4 until eval #23 ran it at field size in "
348
+ "9,8 s — a run that did not finish once is not a command that cannot finish "
349
+ "(C3-100, and the same correction `modernize` needed).",
350
+ "5.1 s → 2.9 s", analysis_class="repo-wide", cold_seconds=5.1, warm_seconds=2.9,
351
+ field_seconds=9.8, field_cache_state="mixed", field_measured_version="5.0.0"),
352
+ CommandCache("validation", ("parse",), "shared", False, "", "11.6 s → 6.4 s", analysis_class="repo-wide", cold_seconds=11.6, warm_seconds=6.4,
353
+ field_seconds=17.4, field_measured_version="4.18.0"),
354
+ CommandCache("modernize", ("parse",), "shared", False,
355
+ "Blocked in the session that recorded C3-53 and measured since: a run that did "
356
+ "not finish once is not a command that cannot finish.",
357
+ "5.2 s → 3.0 s", analysis_class="repo-wide", cold_seconds=5.2, warm_seconds=3.0,
358
+ field_seconds=9.1, field_measured_version="4.18.0"),
294
359
  CommandCache("chunk-file", (), "none", False, "Reads one file; nothing to cache.", analysis_class="core"),
295
360
  CommandCache("cold-start", ("ris",), "answer", True,
296
361
  "Reads the RIS a warm rebuilds — that is all it does. Without one it answers "
297
362
  "`no_ris` instead of a snapshot.",
298
363
  "0.2 s either way", analysis_class="core", cold_seconds=0.2, warm_seconds=0.2),
299
364
  CommandCache("trend", (), "none", False, "Reads stored baseline artifacts from disk; analyses no source, so no cache layer applies. Same command as `baseline trend`.", analysis_class="none"),
300
- CommandCache("baseline", ("parse",), "shared", False, "`capture`/`diff`/`trend` over architectural metrics.",
301
- "capture 8.7 s → 3.6 s", analysis_class="repo-wide", cold_seconds=8.7, warm_seconds=3.6, field_seconds=11.0),
365
+ CommandCache("baseline", ("parse",), "shared", False,
366
+ "`capture`/`diff`/`trend` over architectural metrics. The field figure is `capture`, "
367
+ "the subcommand that analyses; `trend` reads stored artifacts and has its own row.",
368
+ "capture 8.7 s → 3.6 s", analysis_class="repo-wide", cold_seconds=8.7, warm_seconds=3.6,
369
+ field_seconds=2.2, field_cache_state="mixed", field_measured_version="5.0.0"),
302
370
  CommandCache("retrieve", ("cir", "parse"), "shared", False,
303
371
  "Every query builds or reuses the shared CIR a warm builds.", analysis_class="core"),
304
372
  CommandCache("archetype", ("parse",), "shared", False, "", "9.3 s → 5.6 s", analysis_class="repo-wide", cold_seconds=9.3, warm_seconds=5.6),
@@ -319,6 +387,73 @@ _WARM_LABEL = {
319
387
  "none": "nothing",
320
388
  }
321
389
 
390
+ #: The cache states a field figure may declare. A row outside this set is a
391
+ #: measurement whose conditions nobody can read (C3-100).
392
+ FIELD_CACHE_STATES = ("warm", "cold", "mixed")
393
+
394
+
395
+ def _thousands(n: int) -> str:
396
+ return f"{n:,}".replace(",", " ")
397
+
398
+
399
+ def _row(command: str) -> "CommandCache":
400
+ for entry in COMMANDS:
401
+ if entry.command == command:
402
+ return entry
403
+ raise KeyError(command)
404
+
405
+
406
+ #: The headline anchor, read off the table rather than written twice (C3-100).
407
+ #: `spring-audit` is the command the whole series measured — 408 s (#13), 2 351 s
408
+ #: (#16), 8,4 s (#22) — so it is the row the front page quotes, and quoting it
409
+ #: means reading it, not copying it.
410
+ _FIELD_ANCHOR_ROW = _row("spring-audit")
411
+ FIELD_ANCHOR_SECONDS = _FIELD_ANCHOR_ROW.field_seconds
412
+ FIELD_ANCHOR_MEASURED_VERSION = _FIELD_ANCHOR_ROW.field_measured_version
413
+ FIELD_ANCHOR = (
414
+ f"field evaluation #22: spring-audit on {_thousands(FIELD_ANCHOR_JAVA_FILES)} "
415
+ f"Java files took {FIELD_ANCHOR_SECONDS:g} s (Windows, pipx, "
416
+ f"{_FIELD_ANCHOR_ROW.field_cache_state} cache) against "
417
+ f"{_FIELD_ANCHOR_ROW.cold_seconds:g} s on the "
418
+ f"{_thousands(REFERENCE_JAVA_FILES)}-file reference"
419
+ )
420
+
421
+
422
+ def _version_key(version: "Optional[str]") -> tuple:
423
+ """A comparable key for a release name, for ordering measurements only.
424
+
425
+ Not a distance — `reference_currency` refuses to invent one and this does not
426
+ either. It answers exactly one question: is this row's build older than the
427
+ newest build anything in the table was measured on (C3-100)? Anything
428
+ unparseable sorts oldest, which is the safe direction: it gets labelled.
429
+ """
430
+ if not version:
431
+ return ()
432
+ parts: list[int] = []
433
+ for chunk in version.split("."):
434
+ digits = "".join(c for c in chunk if c.isdigit())
435
+ if not digits:
436
+ break
437
+ parts.append(int(digits))
438
+ return tuple(parts)
439
+
440
+
441
+ def newest_field_measurement_version() -> "Optional[str]":
442
+ """The most recent build any field figure in the table was taken on.
443
+
444
+ The reference point for *"this row is older than the table it sits in"* — the
445
+ state C3-100 lived in for three releases, where a refresh reached five rows
446
+ and left four behind on a build four releases older, with nothing anywhere
447
+ saying so.
448
+ """
449
+ versions = [
450
+ row.field_measured_version
451
+ for row in COMMANDS
452
+ if (row.field_seconds is not None or row.field_blocked)
453
+ and row.field_measured_version
454
+ ]
455
+ return max(versions, key=_version_key) if versions else None
456
+
322
457
 
323
458
  def layer(layer_id: str) -> Layer:
324
459
  for entry in LAYERS:
@@ -363,12 +498,124 @@ class Conditioning:
363
498
  return (
364
499
  f"This repository ({self.scope}) is larger than the repository the "
365
500
  f"figures below were measured on ({REFERENCE_JAVA_FILES} Java files), "
366
- f"and cost has not been observed to scale with file count: "
501
+ f"and what cost tracks here is the build rather than the file count: "
367
502
  f"{FIELD_ANCHOR}. Read each figure as the reference's, not as yours; "
368
503
  f"the `here:` lines say how each command can be run on this one."
369
504
  )
370
505
 
371
506
 
507
+ def field_anchor_note(row: "Optional[CommandCache]") -> "Optional[str]":
508
+ """The build a row's field figure belongs to, when it is not this one (C3-97).
509
+
510
+ ``None`` when there is nothing to qualify — no field figure, or one measured on
511
+ the running build. Otherwise a short parenthetical naming the build, because a
512
+ figure carrying another release's name is a measurement and a figure carrying
513
+ none is a claim about this one.
514
+
515
+ The figure is never suppressed. A measurement from four releases ago still
516
+ beats silence for a reader deciding how to run a command; what it must not do
517
+ is arrive undated, which is how `spring-audit`'s 2 351 s went on recommending
518
+ a nightly job for a command that had come to take 8,4 s.
519
+ """
520
+ if row is None or (row.field_seconds is None and not row.field_blocked):
521
+ return None
522
+ from sourcecode import __version__
523
+
524
+ measured_on = row.field_measured_version
525
+ if measured_on is None:
526
+ return "build not recorded"
527
+ if measured_on == __version__:
528
+ return None
529
+ # Short on purpose: this rides along every `here:` line and every row of
530
+ # `cache model`, and the build is the whole fact. The sentence that explains
531
+ # what an older build means is `field_currency()['statement']`, printed once.
532
+ return f"on {measured_on}"
533
+
534
+
535
+ def cost_sentence() -> str:
536
+ """The one paragraph the front page says about cost, derived here (CL-21).
537
+
538
+ `--help` and the README used to carry their own hand-written version of it —
539
+ *"repo-wide/deep compositions can take minutes … use deep jobs nightly"* —
540
+ which stayed true for as long as the anchors behind it did and then went on
541
+ being printed for two more releases, recommending a nightly job for commands
542
+ that had come to finish in seconds. A sentence maintained beside the table it
543
+ describes is the second copy of a fact, which is the rule this module opens
544
+ with. It is generated from the anchors instead, so refreshing them refreshes
545
+ the prose.
546
+ """
547
+ audit = next((r for r in COMMANDS if r.command == "spring-audit"), None)
548
+ seconds = FIELD_ANCHOR_SECONDS if audit is None or audit.field_seconds is None else audit.field_seconds
549
+ return (
550
+ f"Cost tracks the command class and the build, not the file count: "
551
+ f"per-symbol queries are the fast path, inventory commands scale with "
552
+ f"files, and a repository-wide audit measured {seconds:g} s on a "
553
+ f"{_thousands(FIELD_ANCHOR_JAVA_FILES)}-file repository "
554
+ f"({FIELD_ANCHOR_MEASURED_VERSION}). `ask cache model` prints the figure "
555
+ f"and the build behind it for every command; "
556
+ f"ASK_MAX_ANALYSIS_SECONDS/ASK_PROGRESS bound and narrate a run where CI "
557
+ f"wants an explicit budget."
558
+ )
559
+
560
+
561
+ def field_currency() -> dict:
562
+ """The same statement as `reference_currency`, for the field anchors (C3-97).
563
+
564
+ Published so a consumer can see which side of the pair is current without
565
+ parsing prose, and so the gap is visible the day it opens rather than the day
566
+ an evaluation points at it.
567
+ """
568
+ from sourcecode import __version__
569
+
570
+ current = __version__
571
+ measured = [
572
+ row for row in COMMANDS
573
+ if row.field_seconds is not None or row.field_blocked
574
+ ]
575
+ stale = sorted(
576
+ row.command
577
+ for row in measured
578
+ if row.field_measured_version not in (None, current)
579
+ )
580
+ # C3-100: the other staleness, and the one that hid for three releases — not
581
+ # "older than the build you are running" (every row is, the day after a
582
+ # release) but "older than the newest measurement in this same table", which
583
+ # is a refresh that reached some rows and not others. Named here so the gap is
584
+ # readable without diffing the rows by hand.
585
+ newest = newest_field_measurement_version()
586
+ behind = sorted(
587
+ row.command
588
+ for row in measured
589
+ if _version_key(row.field_measured_version) < _version_key(newest)
590
+ )
591
+ lagging = (
592
+ f" {len(behind)} of the {len(measured)} field figures were taken before "
593
+ f"the newest one in this table ({newest}): "
594
+ f"{', '.join(behind)}."
595
+ if behind
596
+ else ""
597
+ )
598
+ return {
599
+ "anchor": FIELD_ANCHOR,
600
+ "measured_on_version": FIELD_ANCHOR_MEASURED_VERSION,
601
+ "running_version": current,
602
+ "current": FIELD_ANCHOR_MEASURED_VERSION == current,
603
+ "rows_measured_on_an_earlier_build": stale,
604
+ "newest_field_measurement_version": newest,
605
+ "rows_behind_the_newest_field_measurement": behind,
606
+ "statement": (
607
+ (
608
+ f"The field anchors were taken on {FIELD_ANCHOR_MEASURED_VERSION}; you "
609
+ f"are running {current}. Each row states the build its field figure "
610
+ f"belongs to, and no figure is projected onto this one."
611
+ if FIELD_ANCHOR_MEASURED_VERSION != current
612
+ else f"The field anchors were taken on {current}, the build you are running."
613
+ )
614
+ + lagging
615
+ ),
616
+ }
617
+
618
+
372
619
  def reference_currency() -> dict:
373
620
  """How old the reference figures are, in releases the reader can name (F-AP).
374
621
 
@@ -429,9 +676,10 @@ def as_dict(here: "Optional[Conditioning]" = None) -> dict:
429
676
  # table nine releases old cannot read as this build's measurement.
430
677
  "reference_currency": reference_currency(),
431
678
  # Published beside the reference so no consumer reads the figures as a
432
- # law: the same command has been measured 46× slower on a repository 1.7×
433
- # the size, on a different platform (C4-19).
679
+ # law: the same command on the same commit of the same repository has
680
+ # measured 2 351 s and 8,4 s on two of our own builds (C4-19, C3-97).
434
681
  "field_anchor": FIELD_ANCHOR,
682
+ "field_currency": field_currency(),
435
683
  "commands": [
436
684
  {
437
685
  "command": cmd.command,
@@ -453,6 +701,21 @@ def as_dict(here: "Optional[Conditioning]" = None) -> dict:
453
701
  if cmd.cold_seconds is None
454
702
  else {"cold": cmd.cold_seconds, "warm": cmd.warm_seconds}
455
703
  ),
704
+ # C3-97: the field figure and the build it belongs to travel
705
+ # together, or not at all. A consumer that reads the seconds
706
+ # without the build is the defect this key exists to prevent.
707
+ "field_measurement": (
708
+ None
709
+ if cmd.field_seconds is None and not cmd.field_blocked
710
+ else {
711
+ "seconds": cmd.field_seconds,
712
+ "did_not_finish": cmd.field_blocked,
713
+ "java_files": FIELD_ANCHOR_JAVA_FILES,
714
+ "cache_state": cmd.field_cache_state,
715
+ "measured_on_version": cmd.field_measured_version,
716
+ "note": field_anchor_note(cmd),
717
+ }
718
+ ),
456
719
  }
457
720
  for cmd in COMMANDS
458
721
  ],
@@ -478,6 +741,13 @@ def render_markdown() -> str:
478
741
  f"performance gate (`docs/perf/REGRESSION-GATE.md`) is what re-measures them."
479
742
  )
480
743
  out.append("")
744
+ out.append(
745
+ f"The second anchor is the field one, and it carries its own build for the "
746
+ f"same reason (C3-97): {FIELD_ANCHOR}. Where a command was measured on an "
747
+ f"earlier release than the one you are running, `ask cache model` says so "
748
+ f"on that row rather than presenting the figure as this build's."
749
+ )
750
+ out.append("")
481
751
  out.append("| Command | A warm gives it | Measured (nothing cached → after a warm) | Repeat run cached | Layers | Notes |")
482
752
  out.append("|---|---|---|---|---|---|")
483
753
  for cmd in COMMANDS:
@@ -556,12 +826,28 @@ def render_text(here: "Optional[Conditioning]" = None) -> str:
556
826
  lines.append("What a warm gives each command")
557
827
  lines.append(f" Timings: {REFERENCE_REPOSITORY}, each command measured in isolation.")
558
828
  lines.append(f" {reference_currency()['statement']}")
829
+ lines.append(f" {field_currency()['statement']}")
559
830
  width = max(len(c.command) for c in COMMANDS)
560
831
  for cmd in COMMANDS:
561
832
  repeat = "repeat cached" if cmd.repeat else "recomputes"
562
833
  lines.append(f" {cmd.command.ljust(width)} {_WARM_LABEL[cmd.warm]:<16} ({repeat})")
563
834
  if cmd.measured:
564
835
  lines.append(f" {' ' * width} measured: {cmd.measured}")
836
+ if cmd.field_seconds is not None or cmd.field_blocked:
837
+ # C3-97: never the seconds without the build they were taken on.
838
+ # C3-100: nor without the cache state they were taken in.
839
+ note = field_anchor_note(cmd)
840
+ shown = (
841
+ "did not finish in an interactive session"
842
+ if cmd.field_seconds is None
843
+ else f"{cmd.field_seconds:g} s"
844
+ )
845
+ qualifiers = [f"{cmd.field_cache_state} cache"] + ([note] if note else [])
846
+ suffix = f" ({', '.join(qualifiers)})"
847
+ lines.append(
848
+ f" {' ' * width} field: {shown} at "
849
+ f"{_thousands(FIELD_ANCHOR_JAVA_FILES)} Java files{suffix}"
850
+ )
565
851
  if cmd.note:
566
852
  lines.append(f" {' ' * width} {cmd.note}")
567
853
  if here is not None and cmd.command in here.per_command:
sourcecode/cli.py CHANGED
@@ -467,6 +467,25 @@ def _help_scope() -> "Optional[Any]":
467
467
  return None
468
468
 
469
469
 
470
+ def _cost_sentence() -> str:
471
+ """What the front page says about cost, from the module that measured it.
472
+
473
+ CL-21: this paragraph used to be written here, went stale with the anchors it
474
+ paraphrased, and kept telling operators to schedule a nightly job for commands
475
+ that finish in seconds. It is `cache_model`'s sentence now, so it cannot drift
476
+ from the table `ask cache model` prints.
477
+ """
478
+ try:
479
+ from sourcecode.cache_model import cost_sentence
480
+
481
+ return cost_sentence()
482
+ except Exception:
483
+ return (
484
+ "Cost tracks the command class and the build: `ask cache model` prints "
485
+ "what each command measured, and on which build."
486
+ )
487
+
488
+
470
489
  def _build_help_text(scope: "Optional[Any]" = None) -> str:
471
490
  """Build --help text dynamically based on current license state."""
472
491
  try:
@@ -485,10 +504,7 @@ def _build_help_text(scope: "Optional[Any]" = None) -> str:
485
504
  Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
486
505
 
487
506
  Cache warms on first scan; later calls reuse pre-built context instead of rescanning.
488
- Performance depends on command class and repo size: per-symbol queries are the fast path,
489
- inventory commands scale with files, and repo-wide/deep compositions can take
490
- minutes on multi-thousand-endpoint repositories. Use deep jobs nightly or with
491
- ASK_MAX_ANALYSIS_SECONDS/ASK_PROGRESS when CI needs an explicit budget.
507
+ {_cost_sentence()}
492
508
 
493
509
  {_start_here_block(scope)}
494
510
 
sourcecode/detach.py CHANGED
@@ -1,8 +1,12 @@
1
1
  """detach.py — run a repository-wide analysis so it outlives the shell that started it.
2
2
 
3
- C3-53, three reproductions on the field repository: `spring-audit` runs 408 s of
4
- analysis, the agent harness promotes the foreground process to background at
5
- ~600 s, and the session dies. **Backgrounding it did not help, and neither did
3
+ C3-53, three reproductions on the field repository: `spring-audit` ran 408 s of
4
+ analysis **on the build measured then** (4.7.0-class; the same command on the same
5
+ repository measures 8,4 s on 4.18.0 — C3-97), the agent harness promoted the
6
+ foreground process to background at ~600 s, and the session died. The cost that
7
+ provoked this module has since collapsed; what the module answers — *a run that
8
+ must outlive the shell that started it* — has not, because a nightly composition
9
+ or a `risk` on a large tree still crosses that boundary. **Backgrounding it did not help, and neither did
6
10
  `Start-Process -NoNewWindow -PassThru`** — all six detached processes died at
7
11
  the same second the session did.
8
12