sourcecode 5.1.0__py3-none-any.whl → 5.2.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__ = "5.1.0"
7
+ __version__ = "5.2.0"
@@ -235,6 +235,63 @@ def diff_metrics(base: ArchMetrics, head: ArchMetrics) -> dict:
235
235
  }
236
236
 
237
237
 
238
+ #: The one sentence both identical-state payloads are derived from (C3-107).
239
+ #: It is an argument, not an assumption, and it rests on a property this product
240
+ #: publishes and asserts every release: the analysis is a pure function of the
241
+ #: tree state, byte-identical across eleven field rounds and two major jumps.
242
+ IDENTICAL_STATE_BASIS = (
243
+ "The two states are one tree, and the analysis is a deterministic function of "
244
+ "the tree state, so every delta is zero without reading the tree. What is "
245
+ "*not* claimed is anything about the tree itself: the per-side totals were "
246
+ "not measured and are `null`, never 0 — pass two different states, or run "
247
+ "`ask <repo>` on one of them, to measure the repository rather than the diff."
248
+ )
249
+
250
+
251
+ def identical_state_delta() -> dict:
252
+ """The `architectural-delta-v1` payload for two states that are one state.
253
+
254
+ C3-107 closed the second build; the eleventh round measured what was left —
255
+ `delta` 24,6 s and `contract-diff` 59,9 s to conclude that a tree does not
256
+ differ from itself, from a purged cache. Building one side to diff it against
257
+ itself is a measurement of the *repository*, and this command's answer is the
258
+ *difference*: with one tree state the difference is empty by construction.
259
+
260
+ So the deltas are published as the zeroes they provably are, and every figure
261
+ that would require an analysis is `null` with the reason attached — the rule
262
+ this product applies everywhere else (an axis that was not measured is
263
+ `unknown`, never `0`).
264
+ """
265
+ empty_counts = {"base": None, "head": None, "delta": 0}
266
+ return {
267
+ "schema": ARCH_DELTA_SCHEMA,
268
+ "base_cir_hash": None,
269
+ "head_cir_hash": None,
270
+ "totals": {
271
+ "files": dict(empty_counts),
272
+ "symbols": dict(empty_counts),
273
+ "endpoints": dict(empty_counts),
274
+ "dependency_edges": dict(empty_counts),
275
+ "import_cycles": dict(empty_counts),
276
+ },
277
+ "endpoint_surface": {
278
+ "added_count": 0, "removed_count": 0, "added": [], "removed": [],
279
+ },
280
+ "fan_in_shifts": {"changed_count": 0, "shifts": []},
281
+ "import_cycle_shifts": {
282
+ "added_count": 0, "removed_count": 0, "added": [], "removed": [],
283
+ },
284
+ "blast_radius_shifts": {
285
+ "scope": "fan_in_shift_symbols", "changed_count": 0, "shifts": [],
286
+ },
287
+ "analysis": {"performed": False, "basis": IDENTICAL_STATE_BASIS},
288
+ "provenance": (
289
+ "architectural_delta (D1), identical states: no repository was read. "
290
+ + IDENTICAL_STATE_BASIS
291
+ ),
292
+ }
293
+
294
+
238
295
  def _transitive_reach(fqn: str, reverse_graph: dict, node_cap: int) -> int:
239
296
  """Size of the transitive reverse-dependency set of `fqn` (its blast radius).
240
297
 
sourcecode/cache_model.py CHANGED
@@ -141,10 +141,11 @@ REFERENCE_JAVA_FILES = 2000
141
141
  REFERENCE_MEASURED_VERSION = "3.2.2"
142
142
 
143
143
  #: The other end of the measured range, and the reason this module publishes a
144
- #: *class* rather than a projected duration (C4-19). Field evaluation #22, on
145
- #: Windows 11 / PowerShell 5.1 / pipx / Python 3.10: `spring-audit` on a
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.
144
+ #: *class* rather than a projected duration (C4-19). Field evaluation #26, on
145
+ #: Windows 11 / PowerShell 5.1 / pipx / Python 3.10, every cache purged first:
146
+ #: `spring-audit` on a 3 342-file repository ran **31,0 s** — the same command
147
+ #: takes 8.8 s on the 2 000-file reference, for a repository 1,7× the size, and it
148
+ #: ran 8,4 s on the same subject two releases earlier.
148
149
  #:
149
150
  #: **The refusal to project stands, and this evaluation strengthened it** (C3-97).
150
151
  #: For eight releases the pair of anchors said *cost does not track file count*;
@@ -159,9 +160,11 @@ REFERENCE_MEASURED_VERSION = "3.2.2"
159
160
  #: each was measured on**, and how to run it.
160
161
  #:
161
162
  #: 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.
163
+ #: 408 s (#13) → 2 351 s (#16, C3-72) → 8,4 s (#22, C3-97) → 31,0 s (#26, C3-110).
164
+ #: The middle figure was quoted as an operational anchor for four releases after
165
+ #: the build that produced it, and told operators to detach commands that finish in
166
+ #: seconds; the last one moved 3,7× in two releases with byte-identical payloads,
167
+ #: which is the movement the release gate exists to catch and did not.
165
168
  #:
166
169
  #: ⚠ `FIELD_ANCHOR`, `FIELD_ANCHOR_SECONDS` and `FIELD_ANCHOR_MEASURED_VERSION`
167
170
  #: are **derived from the `spring-audit` row**, below `COMMANDS`. They used to be
@@ -242,12 +245,12 @@ COMMANDS: tuple[CommandCache, ...] = (
242
245
  "71,7 s again for `--agent` on the state it had just analysed (C3-103).",
243
246
  "--compact 13.3 s cold → 0.3 s warm (cold re-measured on 3.7.0: was 19.3 s, C3-6); "
244
247
  "--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"),
248
+ field_seconds=166.7, field_cache_state="cold", field_measured_version="5.1.0"),
246
249
  CommandCache("posture", ("cir", "parse"), "shared", False,
247
250
  "Resolves the conditional bean graph on every run, over the shared CIR a warm "
248
251
  "builds — the parse it used to repeat for itself. `--diff` compares two profile "
249
252
  "sets over that one IR, so the second side costs the resolution only.",
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"),
253
+ "10.1 s → 1.6 s", analysis_class="repo-wide", cold_seconds=10.1, warm_seconds=1.6, field_seconds=8.3, field_cache_state="mixed", field_measured_version="5.1.0"),
251
254
  CommandCache("risk", ("cir", "parse"), "shared", False,
252
255
  "Composes what the audit, impact-chain and the posture already answer, so it "
253
256
  "pays each of their costs once over the shared CIR a warm builds — one parse "
@@ -255,7 +258,7 @@ COMMANDS: tuple[CommandCache, ...] = (
255
258
  "within the run.",
256
259
  "not measured on the battery yet — the composition is bounded by the "
257
260
  "`spring-audit` + `impact-chain` costs listed here, not by new analysis",
258
- analysis_class="deep", field_seconds=73.3, field_measured_version="4.18.0"),
261
+ analysis_class="deep", field_seconds=77.2, field_cache_state="mixed", field_measured_version="5.1.0"),
259
262
  CommandCache("enrich", ("cir", "parse"), "shared", False,
260
263
  "Runs the same composition as `risk` over the repository, then joins a SARIF "
261
264
  "log to it. Reading the log is negligible; everything a warm helps with is the "
@@ -280,30 +283,31 @@ COMMANDS: tuple[CommandCache, ...] = (
280
283
  "builds. Cost scales with the number of declared types, not with the "
281
284
  "size of the label.",
282
285
  "not measured on the battery yet — one `impact-chain` traversal per "
283
- "declared seed type over a CIR the warm already paid for", analysis_class="repo-wide"),
286
+ "declared seed type over a CIR the warm already paid for", analysis_class="repo-wide",
287
+ field_seconds=28.9, field_cache_state="mixed", field_measured_version="5.1.0"),
284
288
  CommandCache("endpoints", ("ris", "parse"), "shared", False,
285
289
  "Recomputes the endpoint surface on every run, over a parse a warm has already "
286
290
  "paid for. Until 3.7.0 the extractor parsed every file itself instead of reading "
287
291
  "the shared parse cache, and a warm measurably bought it nothing (C3-6).",
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"),
292
+ "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.6, field_cache_state="mixed", field_measured_version="5.1.0"),
289
293
  CommandCache("spring-audit", ("ris", "parse"), "shared", False,
290
294
  "Recomputes every run, but over a parse a warm has already paid for.",
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"),
295
+ "8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7, field_seconds=31.0, field_cache_state="mixed", field_measured_version="5.1.0"),
292
296
  CommandCache("migrate-check", ("cir",), "none", False,
293
297
  "Computes its own inventory and shares nothing a warm builds. Only `--blast-radius` "
294
298
  "reuses the shared CIR.",
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"),
299
+ "4.8 s → 4.8 s", analysis_class="repo-wide", cold_seconds=4.8, warm_seconds=4.8, field_seconds=11.3, field_cache_state="mixed", field_measured_version="5.1.0"),
296
300
  CommandCache("impact-chain", ("cir", "parse"), "shared", False,
297
301
  "The CIR is the expensive half — this is where a warm pays most.",
298
302
  "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"),
303
+ field_seconds=16.1, field_cache_state="mixed", field_measured_version="5.1.0"),
300
304
  CommandCache("impact", ("parse",), "shared", False, "", "4.9 s → 2.8 s", analysis_class="core", cold_seconds=4.9, warm_seconds=2.8,
301
305
  field_seconds=7.7, field_measured_version="4.18.0"),
302
306
  CommandCache("pr-impact", ("parse",), "shared", False,
303
307
  "Diff-dependent: the answer itself is never stored. What it costs follows the "
304
308
  "diff, not the repository: 12,5 s on an ordinary one and 11,8 s on a diff of "
305
309
  "security configuration, measured at field scale.", analysis_class="core",
306
- field_seconds=12.5, field_measured_version="4.18.0"),
310
+ field_seconds=25.7, field_cache_state="mixed", field_measured_version="5.1.0"),
307
311
  CommandCache("verify", (), "none", False, "Runs the contracts against a fresh reading.", analysis_class="core"),
308
312
  CommandCache("verify-edit", ("parse",), "shared", True,
309
313
  "Built for the edit loop: the parse cache is what keeps an unchanged file out of the "
@@ -311,15 +315,17 @@ COMMANDS: tuple[CommandCache, ...] = (
311
315
  "yet: eval #23 measured 76,6 s on a tree with no edits, because the HEAD side is "
312
316
  "still built in a throwaway worktree instead of reusing the shared CIR (C3-102).",
313
317
  "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"),
318
+ field_seconds=119.5, field_cache_state="mixed", field_measured_version="5.1.0"),
315
319
  CommandCache("review-pr", ("cir",), "none", False,
316
320
  "Diff-dependent, and it reuses the CIR only if one exists. On a small diff, loading "
317
321
  "the warmed CIR costs more than the work it saves.",
318
322
  "1.1 s → 2.3 s (slower)", analysis_class="core", cold_seconds=1.1, warm_seconds=2.3),
319
323
  CommandCache("plan", ("parse",), "shared", False, "", "9.3 s → 3.8 s", analysis_class="core", cold_seconds=9.3, warm_seconds=3.8),
320
324
  CommandCache("compare", ("parse",), "shared", False, "", analysis_class="core"),
321
- CommandCache("delta", (), "none", False, "Analyses two states — two checkouts, or two refs materialised into temporary trees; neither is the tree the cache describes.", analysis_class="repo-wide"),
322
- CommandCache("contract-diff", (), "none", False, "Analyses two states (checkouts or refs); neither is the tree the cache describes.", analysis_class="repo-wide"),
325
+ CommandCache("delta", (), "none", False, "Analyses two states — two checkouts, or two refs materialised into temporary trees; neither is the tree the cache describes.", analysis_class="repo-wide",
326
+ field_seconds=24.6, field_cache_state="mixed", field_measured_version="5.1.0"),
327
+ CommandCache("contract-diff", (), "none", False, "Analyses two states (checkouts or refs); neither is the tree the cache describes.", analysis_class="repo-wide",
328
+ field_seconds=59.9, field_cache_state="mixed", field_measured_version="5.1.0"),
323
329
  CommandCache("fix-bug", ("task",), "none", True,
324
330
  "Shorthand for `prepare-context fix-bug`; caches its own answer, which a warm never runs.", analysis_class="core"),
325
331
  CommandCache("rename-class", (), "none", False, "", analysis_class="core"),
@@ -341,26 +347,27 @@ COMMANDS: tuple[CommandCache, ...] = (
341
347
  "no answer hit", analysis_class="repo-wide", cold_seconds=6.5, warm_seconds=0.3),
342
348
  CommandCache("explain", ("cir",), "shared", False, "Serves from the shared CIR a warm builds.",
343
349
  "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"),
350
+ field_seconds=6.3, field_cache_state="mixed", field_measured_version="5.1.0"),
345
351
  CommandCache("export", ("parse",), "shared", False, "", "8.8 s → 3.7 s", analysis_class="repo-wide", cold_seconds=8.8, warm_seconds=3.7),
346
352
  CommandCache("repo-ir", ("parse",), "shared", False,
347
353
  "Carried as *did not finish* from 4.10.4 until eval #23 ran it at field size in "
348
354
  "9,8 s — a run that did not finish once is not a command that cannot finish "
349
355
  "(C3-100, and the same correction `modernize` needed).",
350
356
  "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"),
357
+ field_seconds=19.2, field_cache_state="mixed", field_measured_version="5.1.0"),
352
358
  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"),
359
+ field_seconds=39.5, field_cache_state="mixed", field_measured_version="5.1.0"),
354
360
  CommandCache("modernize", ("parse",), "shared", False,
355
361
  "Blocked in the session that recorded C3-53 and measured since: a run that did "
356
362
  "not finish once is not a command that cannot finish.",
357
363
  "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"),
364
+ field_seconds=17.1, field_cache_state="mixed", field_measured_version="5.1.0"),
359
365
  CommandCache("chunk-file", (), "none", False, "Reads one file; nothing to cache.", analysis_class="core"),
360
366
  CommandCache("cold-start", ("ris",), "answer", True,
361
367
  "Reads the RIS a warm rebuilds — that is all it does. Without one it answers "
362
368
  "`no_ris` instead of a snapshot.",
363
- "0.2 s either way", analysis_class="core", cold_seconds=0.2, warm_seconds=0.2),
369
+ "0.2 s either way", analysis_class="core", cold_seconds=0.2, warm_seconds=0.2,
370
+ field_seconds=0.4, field_cache_state="mixed", field_measured_version="5.1.0"),
364
371
  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"),
365
372
  CommandCache("baseline", ("parse",), "shared", False,
366
373
  "`capture`/`diff`/`trend` over architectural metrics. The field figure is `capture`, "
@@ -405,13 +412,15 @@ def _row(command: str) -> "CommandCache":
405
412
 
406
413
  #: The headline anchor, read off the table rather than written twice (C3-100).
407
414
  #: `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.
415
+ #: (#16), 8,4 s (#22), 31,0 s (#26) — so it is the row the front page quotes, and
416
+ #: quoting it means reading it, not copying it. The figure moving *up* between two
417
+ #: rounds with byte-identical payloads is the reason C3-110 exists: nothing in this
418
+ #: repository measured the direction, and the anchor is the only place it shows.
410
419
  _FIELD_ANCHOR_ROW = _row("spring-audit")
411
420
  FIELD_ANCHOR_SECONDS = _FIELD_ANCHOR_ROW.field_seconds
412
421
  FIELD_ANCHOR_MEASURED_VERSION = _FIELD_ANCHOR_ROW.field_measured_version
413
422
  FIELD_ANCHOR = (
414
- f"field evaluation #22: spring-audit on {_thousands(FIELD_ANCHOR_JAVA_FILES)} "
423
+ f"field evaluation #26: spring-audit on {_thousands(FIELD_ANCHOR_JAVA_FILES)} "
415
424
  f"Java files took {FIELD_ANCHOR_SECONDS:g} s (Windows, pipx, "
416
425
  f"{_FIELD_ANCHOR_ROW.field_cache_state} cache) against "
417
426
  f"{_FIELD_ANCHOR_ROW.cold_seconds:g} s on the "
@@ -532,6 +541,37 @@ def field_anchor_note(row: "Optional[CommandCache]") -> "Optional[str]":
532
541
  return f"on {measured_on}"
533
542
 
534
543
 
544
+ def field_anchor_currency_clause(row: "Optional[CommandCache]") -> "Optional[str]":
545
+ """The same fact as :func:`field_anchor_note`, said where it is *acted on* (C3-110).
546
+
547
+ C3-109 made every field figure carry its build, and the eleventh round confirms
548
+ the label is read and believed — and then reports the gap it does not close:
549
+ `spring-audit` publishes *"8,4 s … (4.18.0)"* while the same command measures
550
+ 31,0 s on the build the reader is running. `(on 4.18.0)` is the right label for
551
+ a **row in a table**, where the reader is comparing figures; it is not enough
552
+ on a line that tells somebody how to run something, because there the figure is
553
+ being used as advice about the build in hand.
554
+
555
+ So the same fact gets a longer form for the surfaces that advise: the build,
556
+ and the admission that nothing has re-measured it here. That admission is the
557
+ registry this product applies everywhere else (`counts_are_floor`,
558
+ `unchanged_for`, `confidence_basis`) and the one place it had not applied to
559
+ itself.
560
+
561
+ ``None`` when the figure was taken on the running build — there is nothing to
562
+ admit — or when there is no figure at all.
563
+ """
564
+ if row is None or (row.field_seconds is None and not row.field_blocked):
565
+ return None
566
+ from sourcecode import __version__
567
+
568
+ measured_on = row.field_measured_version
569
+ if measured_on == __version__:
570
+ return None
571
+ where = f"measured on {measured_on}" if measured_on else "build not recorded"
572
+ return f"{where}; this build has not been re-measured at that scale"
573
+
574
+
535
575
  def cost_sentence() -> str:
536
576
  """The one paragraph the front page says about cost, derived here (CL-21).
537
577
 
@@ -544,15 +584,38 @@ def cost_sentence() -> str:
544
584
  with. It is generated from the anchors instead, so refreshing them refreshes
545
585
  the prose.
546
586
  """
587
+ from sourcecode import __version__
588
+
547
589
  audit = next((r for r in COMMANDS if r.command == "spring-audit"), None)
548
590
  seconds = FIELD_ANCHOR_SECONDS if audit is None or audit.field_seconds is None else audit.field_seconds
591
+ # C3-110: the figure is history the moment it is published, and this sentence
592
+ # is the surface a reader budgets against. The field measured 31,0 s where this
593
+ # line said 8,4 s, and the line was not wrong — it named 4.18.0 — it just left
594
+ # the reader to notice that 4.18.0 is not the build they are running. Said
595
+ # outright, and paired with the one figure this project *can* refresh: the gate
596
+ # measurement of the build in hand, when there is one.
597
+ stale = (
598
+ ""
599
+ if FIELD_ANCHOR_MEASURED_VERSION == __version__
600
+ else f", and {__version__} has not been re-measured at that scale"
601
+ )
602
+ gate = ""
603
+ _gate_cell = gate_anchor("spring-audit")
604
+ _gate_state = gate_currency()
605
+ if _gate_cell and _gate_state.get("current"):
606
+ gate = (
607
+ f" On this build the release gate measured the same command at "
608
+ f"{_gate_cell['seconds']:g} s on a "
609
+ f"{_thousands(int(_gate_cell['java_files']))}-file repository "
610
+ f"({_gate_cell['cache_state']} cache, {_gate_cell['host_class']})."
611
+ )
549
612
  return (
550
613
  f"Cost tracks the command class and the build, not the file count: "
551
614
  f"per-symbol queries are the fast path, inventory commands scale with "
552
615
  f"files, and a repository-wide audit measured {seconds:g} s on a "
553
616
  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; "
617
+ f"({FIELD_ANCHOR_MEASURED_VERSION}{stale}).{gate} `ask cache model` prints "
618
+ f"the figure and the build behind it for every command; "
556
619
  f"ASK_MAX_ANALYSIS_SECONDS/ASK_PROGRESS bound and narrate a run where CI "
557
620
  f"wants an explicit budget."
558
621
  )
sourcecode/cli.py CHANGED
@@ -403,7 +403,7 @@ def _start_here_block(scope: "Optional[Any]" = None) -> str:
403
403
  not would be worse than the one this replaces.
404
404
  """
405
405
  from sourcecode.execution_plan import (
406
- BUCKET_ORDER, BUCKET_TITLE, DETACH, RUNS_NOW, UNKNOWN, plan,
406
+ BUCKET_ORDER, BUCKET_TITLE, BUDGETED, DETACH, RUNS_NOW, UNKNOWN, plan,
407
407
  )
408
408
 
409
409
  if scope is None or not scope.measured:
@@ -430,7 +430,7 @@ def _start_here_block(scope: "Optional[Any]" = None) -> str:
430
430
  " [dim]…each of these answers into a file and survives the shell:"
431
431
  " add --output <file> --detach[/dim]"
432
432
  )
433
- if not (grouped.get(RUNS_NOW) or grouped.get(UNKNOWN)):
433
+ if not (grouped.get(RUNS_NOW) or grouped.get(BUDGETED) or grouped.get(UNKNOWN)):
434
434
  lines.append(
435
435
  " [dim]Nothing here runs in seconds on a repository this size. "
436
436
  "`ask cache warm .` first, or start with `ask endpoints . -o endpoints.json`.[/dim]"
@@ -2559,6 +2559,20 @@ _IR_BYTES_PER_FILE_HIGH: int = 30_300
2559
2559
  _IR_BYTES_PER_FILE_SAMPLES: int = 5
2560
2560
  _IR_SIZE_WARN_MB: int = 10 # advise above this; below it the write is not worth a sentence
2561
2561
 
2562
+ #: **C3-111.** The ceiling on the `-o` path, which had none. `--output FILE skips
2563
+ #: the payload budget entirely` is right about a *budget* — a file has no context
2564
+ #: window — and it was read as *no limit of any kind*: the field's run wrote
2565
+ #: **101 254 852 B**, byte-identical across three releases, with no line anywhere,
2566
+ #: because the pre-build band travels through `_notice`, which is silent off-TTY
2567
+ #: by contract (C3-26) and every `-o` run is a scripted one.
2568
+ #:
2569
+ #: 50 MB is where a projection or compression stops being optional: nothing in
2570
+ #: this catalogue reads a file that size in one piece, and `--gzip` cuts it by
2571
+ #: ~70-80 %. The guard fires *before* the build, so refusing costs the reader a
2572
+ #: third of a second rather than the whole analysis, and `--force` keeps the door
2573
+ #: open for somebody who means it.
2574
+ _IR_FILE_CEILING_MB: int = 50
2575
+
2562
2576
  #: What `--no-cache` means on a subcommand (C3-5). The old text — "this command
2563
2577
  #: always reads fresh source (no snapshot cache)" — was false: `cache model`, the
2564
2578
  #: authority, records these commands reading the `ris`, `cir` and `parse` layers.
@@ -2579,6 +2593,21 @@ _NO_CACHE_SUBCOMMAND_HELP = (
2579
2593
  )
2580
2594
 
2581
2595
 
2596
+ def _ir_size_estimate_mb(file_count: int) -> "tuple[float, float, float]":
2597
+ """The `repo-ir` size band for *file_count*, as ``(low, high, midpoint)`` MB.
2598
+
2599
+ One authority for the estimate (C3-111): the advisory printed it and the
2600
+ guard needed it, and two derivations of one band drift the way the band and
2601
+ the field's measurement drifted in C3-105. The midpoint is what the ceiling
2602
+ is compared against — the low end would have let the field's 101 MB through
2603
+ (34 MB estimated) and the high end refuses repositories whose real output is
2604
+ half of it.
2605
+ """
2606
+ low = file_count * _IR_BYTES_PER_FILE_LOW / (1024 * 1024)
2607
+ high = file_count * _IR_BYTES_PER_FILE_HIGH / (1024 * 1024)
2608
+ return low, high, (low + high) / 2
2609
+
2610
+
2582
2611
  def _ir_size_advisory_message(file_count: int) -> "Optional[str]":
2583
2612
  """Return the `repo-ir` output-size advisory, or ``None`` below the threshold.
2584
2613
 
@@ -2604,8 +2633,7 @@ def _ir_size_advisory_message(file_count: int) -> "Optional[str]":
2604
2633
  Pure and TTY-agnostic: the caller owns the terminal gate, so this never
2605
2634
  reaches stdout or a pipeline (C3-26).
2606
2635
  """
2607
- est_low_mb = file_count * _IR_BYTES_PER_FILE_LOW / (1024 * 1024)
2608
- est_high_mb = file_count * _IR_BYTES_PER_FILE_HIGH / (1024 * 1024)
2636
+ est_low_mb, est_high_mb, _ = _ir_size_estimate_mb(file_count)
2609
2637
  if est_high_mb <= _IR_SIZE_WARN_MB:
2610
2638
  return None
2611
2639
  return (
@@ -5809,6 +5837,39 @@ def repo_ir_cmd(
5809
5837
  if _notice_msg:
5810
5838
  _notice(_notice_msg)
5811
5839
 
5840
+ # C3-111: and the notice above is the half that does not reach a script.
5841
+ # `-o` is a scripted run by construction, `_notice` is silent off-TTY by
5842
+ # contract (C3-26), and the write had no ceiling of any kind — which is
5843
+ # how 101 254 852 B arrived three releases running with no line anywhere.
5844
+ # An error envelope reaches every consumer, and this one is raised before
5845
+ # the analysis rather than after the file is on disk.
5846
+ _est_low, _est_high, _est_mid = _ir_size_estimate_mb(len(file_list))
5847
+ _bounded = summary_only or max_nodes is not None or max_edges is not None
5848
+ if _est_mid > _IR_FILE_CEILING_MB and not force and not _bounded:
5849
+ _emit_error_json(
5850
+ "OUTPUT_TOO_LARGE",
5851
+ f"Estimated output is {_est_low:.0f}-{_est_high:.0f}MB for "
5852
+ f"{len(file_list)} files, above the {_IR_FILE_CEILING_MB}MB "
5853
+ f"ceiling for a written IR.",
5854
+ hint=(
5855
+ "Choose the size deliberately: --summary-only (~5K tokens), "
5856
+ "--max-nodes N --max-edges N, --gzip (70-80% smaller), or "
5857
+ "--force to write it in full."
5858
+ ),
5859
+ expected=(
5860
+ f"An estimate under {_IR_FILE_CEILING_MB}MB, or one of "
5861
+ f"--summary-only / --max-nodes / --max-edges / --gzip / --force"
5862
+ ),
5863
+ estimate_mb={"low": round(_est_low, 1), "high": round(_est_high, 1)},
5864
+ estimate_basis=(
5865
+ f"{_IR_BYTES_PER_FILE_LOW}-{_IR_BYTES_PER_FILE_HIGH} bytes per "
5866
+ f"Java file, sampled across {_IR_BYTES_PER_FILE_SAMPLES} "
5867
+ f"repositories; the top of the band under-states, since its "
5868
+ f"sample counted tests among its files and this run does not"
5869
+ ),
5870
+ )
5871
+ raise typer.Exit(1)
5872
+
5812
5873
  _ir_phase = f"extracting IR ({len(file_list)} files)"
5813
5874
  if since:
5814
5875
  _ir_phase += f" since {since}"
@@ -7103,12 +7164,13 @@ def _two_states(
7103
7164
  f"--base-ref and --head-ref resolve to the same commit ({base_sha})",
7104
7165
  )
7105
7166
  if _identical:
7106
- # C3-107, the first half: two refs that name one commit are one
7107
- # tree, so it is read out once. The second `git archive` copied
7108
- # the same bytes to a second directory for a build that is going
7109
- # to be reused rather than repeated.
7110
- with materialize_ref(target, base_ref) as tree:
7111
- yield tree, tree, block
7167
+ # C3-107: two refs that name one commit are one tree. The first
7168
+ # half of the row stopped the second `git archive`; the second
7169
+ # half stops the first one too, because a caller that reads
7170
+ # `identical_states` analyses neither side — there is no
7171
+ # difference to measure, and materialising a tree nobody opens is
7172
+ # the whole cost on a large repository.
7173
+ yield root, root, block
7112
7174
  return
7113
7175
  with materialize_ref(target, base_ref) as base_tree, \
7114
7176
  materialize_ref(target, head_ref) as head_tree:
@@ -7233,7 +7295,10 @@ def delta_cmd(
7233
7295
  _enforce_format("delta", format)
7234
7296
 
7235
7297
  from sourcecode.context_graph import ContextGraph
7236
- from sourcecode.architectural_delta import architectural_delta
7298
+ from sourcecode.architectural_delta import (
7299
+ architectural_delta,
7300
+ identical_state_delta as _identical_state_delta,
7301
+ )
7237
7302
 
7238
7303
  _prog = Progress()
7239
7304
  with _two_states(
@@ -7242,15 +7307,18 @@ def delta_cmd(
7242
7307
  # C3-107: `_two_states` has already decided this, from the commit on the
7243
7308
  # ref path and from the tree signature on the checkout path.
7244
7309
  _same = bool(_comparison.get("identical_states"))
7245
- _prog.start("building base IR")
7246
- _base_cir = ContextGraph.build_from_root(_base_root).cir
7247
7310
  if _same:
7248
- _head_cir = _base_cir
7311
+ # C3-107, second half: one tree state has no difference to measure, so
7312
+ # nothing is read. Eval #26 measured 24,6 s here to conclude that a
7313
+ # tree does not differ from itself.
7314
+ data = _identical_state_delta()
7249
7315
  else:
7316
+ _prog.start("building base IR")
7317
+ _base_cir = ContextGraph.build_from_root(_base_root).cir
7250
7318
  _prog.update("building head IR")
7251
7319
  _head_cir = ContextGraph.build_from_root(_head_root).cir
7252
- _prog.update("diffing snapshots")
7253
- data = architectural_delta(_base_cir, _head_cir)
7320
+ _prog.update("diffing snapshots")
7321
+ data = architectural_delta(_base_cir, _head_cir)
7254
7322
  data["comparison"] = _comparison
7255
7323
  _prog.finish()
7256
7324
 
@@ -7328,7 +7396,10 @@ def contract_diff_cmd(
7328
7396
  _enforce_format("contract-diff", format)
7329
7397
 
7330
7398
  from sourcecode.context_graph import ContextGraph
7331
- from sourcecode.contract_diff import contract_diff as _contract_diff
7399
+ from sourcecode.contract_diff import (
7400
+ contract_diff as _contract_diff,
7401
+ identical_state_contract_diff as _identical_state_contract_diff,
7402
+ )
7332
7403
  from sourcecode.constraint_diff import constraint_findings as _constraint_findings
7333
7404
  from sourcecode.validation_surface import build_validation_surface
7334
7405
 
@@ -7339,24 +7410,26 @@ def contract_diff_cmd(
7339
7410
  # C3-107: `_two_states` has already decided this, from the commit on the
7340
7411
  # ref path and from the tree signature on the checkout path.
7341
7412
  _same = bool(_comparison.get("identical_states"))
7342
- _prog.start("building base IR")
7343
- _base_cir = ContextGraph.build_from_root(_base_root).cir
7344
7413
  if _same:
7345
- _head_cir = _base_cir
7414
+ # C3-107, second half. This is the command the round measured at
7415
+ # **59,9 s** on one tree — two CIRs and two validation surfaces, all
7416
+ # four of them projections of the same bytes.
7417
+ data = _identical_state_contract_diff()
7346
7418
  else:
7419
+ _prog.start("building base IR")
7420
+ _base_cir = ContextGraph.build_from_root(_base_root).cir
7347
7421
  _prog.update("building head IR")
7348
7422
  _head_cir = ContextGraph.build_from_root(_head_root).cir
7349
- # D2-b: proven request-constraint loosenings fill the non_breaking bucket. The
7350
- # validation surface needs the repo roots (it discovers specs/DTOs), not just the
7351
- # CIR — build it per side and diff the constraint contracts.
7352
- _prog.update("projecting validation constraints")
7353
- _base_surface = build_validation_surface(_base_root)
7354
- _cfindings = _constraint_findings(
7355
- _base_surface,
7356
- _base_surface if _same else build_validation_surface(_head_root),
7357
- )
7358
- _prog.update("diffing contracts")
7359
- data = _contract_diff(_base_cir, _head_cir, constraint_findings=_cfindings)
7423
+ # D2-b: proven request-constraint loosenings fill the non_breaking bucket. The
7424
+ # validation surface needs the repo roots (it discovers specs/DTOs), not just the
7425
+ # CIR — build it per side and diff the constraint contracts.
7426
+ _prog.update("projecting validation constraints")
7427
+ _cfindings = _constraint_findings(
7428
+ build_validation_surface(_base_root),
7429
+ build_validation_surface(_head_root),
7430
+ )
7431
+ _prog.update("diffing contracts")
7432
+ data = _contract_diff(_base_cir, _head_cir, constraint_findings=_cfindings)
7360
7433
  data["comparison"] = _comparison
7361
7434
  _prog.finish()
7362
7435
 
@@ -13810,7 +13883,7 @@ def _cache_model_conditioning(path: Path) -> Any:
13810
13883
  try:
13811
13884
  from sourcecode import cache_model as _cmodel
13812
13885
  from sourcecode.execution_plan import (
13813
- DETACH, RUNS_NOW, UNKNOWN, WARM_FIRST, scope_for, verdict,
13886
+ BUDGETED, DETACH, RUNS_NOW, UNKNOWN, WARM_FIRST, scope_for, verdict,
13814
13887
  )
13815
13888
 
13816
13889
  scope = scope_for(path)
@@ -13820,6 +13893,11 @@ def _cache_model_conditioning(path: Path) -> Any:
13820
13893
  phrasing = {
13821
13894
  RUNS_NOW: "runs in the foreground here",
13822
13895
  WARM_FIRST: "run `ask cache warm .` first; it pays once for every command after it",
13896
+ # C3-110: a minute of foreground work is a budget line, not a refusal.
13897
+ # This line said *"not a foreground run here — a nightly job"* about a
13898
+ # command the field had just run in the foreground, off a figure taken
13899
+ # on another build.
13900
+ BUDGETED: "a foreground run to budget for here — keep the answer with `--output <file>`",
13823
13901
  DETACH: "not a foreground run here — `--output <file> --detach`, or a nightly job",
13824
13902
  UNKNOWN: "not measured at this size",
13825
13903
  }
@@ -206,6 +206,33 @@ def diff_contract(
206
206
  }
207
207
 
208
208
 
209
+ def identical_state_contract_diff() -> dict:
210
+ """The `contract-diff-v1` payload for two states that are one state (C3-107).
211
+
212
+ Same argument as `architectural_delta.identical_state_delta`, and the same
213
+ care about what it does not say: `unchanged` here is a statement about the
214
+ *difference* between the two states, which is empty because there is only one
215
+ state. It is not a statement that the contract is sound, and the payload says
216
+ so rather than leaving a reader to infer it from a green word.
217
+ """
218
+ from sourcecode.architectural_delta import IDENTICAL_STATE_BASIS
219
+
220
+ return {
221
+ "schema": CONTRACT_DIFF_SCHEMA,
222
+ "contract_status": "unchanged",
223
+ "summary": {"breaking": 0, "additive": 0, "non_breaking": 0},
224
+ "breaking": [],
225
+ "additive": [],
226
+ "non_breaking": [],
227
+ "analysis": {"performed": False, "basis": IDENTICAL_STATE_BASIS},
228
+ "provenance": (
229
+ "contract_diff (D2), identical states: no repository was read, and "
230
+ "`unchanged` describes the difference between the two states, not the "
231
+ "health of the contract in either. " + IDENTICAL_STATE_BASIS
232
+ ),
233
+ }
234
+
235
+
209
236
  def contract_diff(
210
237
  base_cir: "CanonicalRepositoryIR",
211
238
  head_cir: "CanonicalRepositoryIR",
@@ -38,12 +38,14 @@ from sourcecode.cache_model import (
38
38
  FIELD_ANCHOR_JAVA_FILES,
39
39
  REFERENCE_JAVA_FILES,
40
40
  REFERENCE_REPOSITORY,
41
+ field_anchor_currency_clause,
41
42
  field_anchor_note,
42
43
  )
43
44
 
44
45
  #: How a command can be run on the scope in hand.
45
46
  RUNS_NOW = "runs_now" # foreground, seconds — the fast path
46
47
  WARM_FIRST = "warm_first" # foreground, after `cache warm`, which pays once
48
+ BUDGETED = "budgeted" # foreground, but long enough to plan for (C3-110)
47
49
  DETACH = "detach" # `--output` + `--detach`, or a nightly job
48
50
  UNKNOWN = "unknown" # not measured on this axis; never guessed
49
51
 
@@ -257,6 +259,17 @@ class Verdict:
257
259
  #: session (C3-53). Commands measured under it at field size are still offered.
258
260
  FOREGROUND_SECONDS = 30.0
259
261
 
262
+ #: **C3-110.** Above this, and only above it, the product says *"not a foreground
263
+ #: run"*. Between the two thresholds a command is offered with its measured cost
264
+ #: attached, because those two sentences are different claims and the product was
265
+ #: making the stronger one on the weaker evidence: `risk` carried *"not a
266
+ #: foreground run here — `--output <file> --detach`, or a nightly job"* off a
267
+ #: 73,3 s anchor taken on another build, and the field — who had just run it in
268
+ #: 77,2 s in the foreground — read that as the product refusing work it does
269
+ #: nightly. Their number, and they are the ones holding the stopwatch: *"umbral
270
+ #: para `not a foreground run`: no emitirlo por debajo de ~120 s"*.
271
+ DETACH_SECONDS = 120.0
272
+
260
273
 
261
274
  def _reference(row) -> Optional[str]:
262
275
  if row is None:
@@ -360,8 +373,13 @@ def verdict(command: str, scope: Scope) -> Verdict:
360
373
  # row this matters most on — 72,3 s from a purged cache against 0,3 s
361
374
  # warm on the reference — and the honest thing to publish is the
362
375
  # measurement's conditions, not a warm figure nobody took at this size.
376
+ # C3-110: on the surfaces that *advise*, the build the figure belongs
377
+ # to is not a parenthetical — it is the reason the advice may be wrong
378
+ # here, so it is said in full.
379
+ currency = field_anchor_currency_clause(row)
363
380
  qualifiers = ", ".join(
364
- [f"{row.field_cache_state} cache"] + ([note] if note else [])
381
+ [f"{row.field_cache_state} cache"]
382
+ + ([currency] if currency else ([note] if note else []))
365
383
  )
366
384
  dated = f" ({qualifiers})"
367
385
  if row.field_seconds <= FOREGROUND_SECONDS:
@@ -370,6 +388,19 @@ def verdict(command: str, scope: Scope) -> Verdict:
370
388
  f"measured at {row.field_seconds:g} s on a repository this size{dated}",
371
389
  cls, reference,
372
390
  )
391
+ if row.field_seconds <= DETACH_SECONDS:
392
+ # C3-110: a minute is a cost, not a prohibition. The figure decides
393
+ # as it always did; what changes is that it stops being read as a
394
+ # refusal on the strength of a measurement from another build.
395
+ return Verdict(
396
+ command, BUDGETED,
397
+ (
398
+ f"measured at {row.field_seconds:g} s on a repository this "
399
+ f"size{dated} — a foreground run to budget for, or "
400
+ f"`--output <file>` to keep the answer"
401
+ ),
402
+ cls, reference,
403
+ )
373
404
  return Verdict(
374
405
  command, DETACH,
375
406
  (
@@ -454,6 +485,10 @@ def advice(invocation: str, root: "Optional[Path]") -> str:
454
485
  found = verdict(invocation, scope)
455
486
  if found.how == DETACH:
456
487
  return f"Not a foreground run on this repository ({scope.describe()}): {found.why}."
488
+ if found.how == BUDGETED:
489
+ # C3-110: stated as a cost, never as a refusal — the distinction the
490
+ # threshold above exists to keep.
491
+ return f"A run to budget for on this repository ({scope.describe()}): {found.why}."
457
492
  if found.how == WARM_FIRST:
458
493
  return (
459
494
  f"The cache for this repository is cold ({scope.describe()}); "
@@ -464,11 +499,15 @@ def advice(invocation: str, root: "Optional[Path]") -> str:
464
499
 
465
500
  #: The order the front page lists the buckets in: what the reader can do now
466
501
  #: first, what costs one warm next, what needs a plan last.
467
- BUCKET_ORDER = (RUNS_NOW, WARM_FIRST, DETACH, UNKNOWN)
502
+ BUCKET_ORDER = (RUNS_NOW, WARM_FIRST, BUDGETED, DETACH, UNKNOWN)
468
503
 
469
504
  BUCKET_TITLE = {
470
505
  RUNS_NOW: "Runs now",
471
506
  WARM_FIRST: "Warm the cache first — `ask cache warm .` pays once",
507
+ BUDGETED: (
508
+ f"Foreground, with a budget — measured between {FOREGROUND_SECONDS:g} s "
509
+ f"and {DETACH_SECONDS:g} s at this size"
510
+ ),
472
511
  DETACH: "Too big for this session — `--output <file> --detach`, or nightly",
473
512
  UNKNOWN: "Cost not measured on this repository",
474
513
  }
sourcecode/perf.py CHANGED
@@ -452,24 +452,121 @@ RELEASE_GATE_MIN_RUNS = 5
452
452
  #: The gated population: repository-wide commands that take only a path, so the
453
453
  #: gate never depends on a symbol that exists in one revision and not the next.
454
454
  #: `""` is the root analysis (`ask <repo>`).
455
+ #:
456
+ #: **C3-110.** Six commands for five releases, against the eighteen the field
457
+ #: measures every round — so `validation` could oscillate 2,9× four times, `risk`
458
+ #: could cross two orders of magnitude, and the gate could stay green throughout
459
+ #: because neither was in it. Every path-only command the field times is here now;
460
+ #: what is left out is left out for a reason, and the reason is published below.
455
461
  RELEASE_GATE_COMMANDS: tuple[str, ...] = (
456
462
  "", "endpoints", "spring-audit", "migrate-check", "validation", "posture",
463
+ "risk", "modernize", "repo-ir", "data-exposure",
457
464
  )
458
465
 
466
+ #: Extra arguments a gated command needs to be measured at all — one authority,
467
+ #: read by the harness and published by `--print-config`, because a flag typed
468
+ #: into a workflow is a second copy of a decision.
469
+ #:
470
+ #: `repo-ir` writes its whole IR by default, which C3-111 now refuses above 50 MB
471
+ #: on the gate repository. `--summary-only` measures the half that is analysis
472
+ #: (build the IR) without the half that is serialisation of ~80 MB, and it is the
473
+ #: form the harness already uses to count symbols.
474
+ RELEASE_GATE_COMMAND_ARGS: dict[str, tuple[str, ...]] = {
475
+ "repo-ir": ("--summary-only",),
476
+ }
477
+
459
478
  #: What the gate does **not** cover, and why — published rather than omitted, so
460
479
  #: "the gate is green" is never read as "every command was measured".
461
480
  RELEASE_GATE_EXCLUSIONS: tuple[tuple[str, str], ...] = (
462
- ("risk", "68 s on the gate repository since C3-96 deleted the catastrophic "
463
- "backtracking that made it 54 minutes (F-AQ) — no longer excluded for "
464
- "cost, only until a baseline cell exists for it; the first capture "
465
- "that includes it makes it gated (C3-99)"),
466
481
  ("impact-chain", "takes a symbol, not a repository — its cost is the audit "
467
482
  "`risk` and `spring-audit` already gate"),
483
+ ("explain", "takes a symbol, same reason"),
468
484
  ("verify-edit", "measures an edit loop, which needs a working-tree mutation "
469
- "the gate would have to fabricate"),
485
+ "the gate would have to fabricate. Its clean-tree path builds "
486
+ "no model at all since C3-102, so the figure a gate could take "
487
+ "here would not be the figure a user pays"),
488
+ ("delta / contract-diff", "take two tree states; on one state they read "
489
+ "nothing at all (C3-107), and on two they measure "
490
+ "the same build the gated commands already cover"),
491
+ ("pr-impact / review-pr", "diff-dependent — their cost follows a diff the "
492
+ "gate would have to invent, and an invented diff "
493
+ "measures the invention"),
470
494
  )
471
495
 
472
496
 
497
+ #: **C3-110.** The second comparison, and the one that would have caught the
498
+ #: series the field prints. A release compared only against the release before it
499
+ #: passes every step of a staircase: `validation` went 13,8 → 34,1 → 39,5 s in
500
+ #: three releases and no single step is 20 %. The reference build is the best
501
+ #: measurement this product has taken, kept as its own baseline directory, and a
502
+ #: release states its distance from it.
503
+ #:
504
+ #: Drift is **measured and published, never deciding** — the same separation
505
+ #: C3-108 drew for cold cells, and for a stronger reason: some of the distance
506
+ #: from the best build is deliberate (work that was added), and a gate that
507
+ #: cannot tell deliberate from accidental must not hold the release. What it must
508
+ #: do is make the number impossible to not know.
509
+ RELEASE_GATE_DRIFT_BASELINE = "reference-best"
510
+ #: The distance at which drift is reported as `drifted` rather than `within`.
511
+ #: Same 20 % as the release threshold: one number for "this is a lot", so a reader
512
+ #: never has to hold two.
513
+ RELEASE_GATE_DRIFT_THRESHOLD = RELEASE_GATE_THRESHOLD
514
+
515
+
516
+ def drift_report(
517
+ reference_cells: Mapping[str, Mapping[str, object]],
518
+ current_cells: Mapping[str, Mapping[str, object]],
519
+ *,
520
+ threshold: float = RELEASE_GATE_DRIFT_THRESHOLD,
521
+ ) -> dict[str, object]:
522
+ """How far this run is from the best build ever measured, cell by cell.
523
+
524
+ Never a verdict: every entry is a ratio and a label, `passed` is not part of
525
+ the shape, and the caller cannot accidentally gate on it. The one thing this
526
+ fails at is silence.
527
+ """
528
+ cells: list[dict[str, object]] = []
529
+ drifted: list[str] = []
530
+ for cid in sorted(set(reference_cells) & set(current_cells)):
531
+ base = reference_cells[cid]
532
+ head = current_cells[cid]
533
+ b_wall = base.get("wall_ms") if isinstance(base.get("wall_ms"), Mapping) else {}
534
+ h_wall = head.get("wall_ms") if isinstance(head.get("wall_ms"), Mapping) else {}
535
+ b50, h50 = b_wall.get("p50"), h_wall.get("p50")
536
+ if not isinstance(b50, (int, float)) or not isinstance(h50, (int, float)) or b50 <= 0:
537
+ cells.append({"cell": cid, "status": "unmeasured"})
538
+ continue
539
+ ratio = float(h50) / float(b50)
540
+ status = "drifted" if (ratio - 1.0) > threshold else "within"
541
+ if status == "drifted":
542
+ drifted.append(cid)
543
+ cells.append({
544
+ "cell": cid,
545
+ "status": status,
546
+ "reference_p50_ms": round(float(b50), 3),
547
+ "current_p50_ms": round(float(h50), 3),
548
+ "ratio": round(ratio, 3),
549
+ })
550
+ unmeasured = sorted(set(reference_cells) - set(current_cells))
551
+ return {
552
+ "schema_version": GATE_SCHEMA_VERSION,
553
+ "comparison": "drift_against_reference_best",
554
+ "reference": RELEASE_GATE_DRIFT_BASELINE,
555
+ "threshold": threshold,
556
+ "cells": cells,
557
+ "drifted": drifted,
558
+ "not_measured_here": unmeasured,
559
+ "basis": (
560
+ "Distance from the best measurement this product has taken, not from "
561
+ "the previous release. A staircase of steps under the release "
562
+ "threshold is invisible to a release-to-release gate and visible "
563
+ "here. Measured and published; never a verdict, because some of the "
564
+ "distance is work that was deliberately added and this comparison "
565
+ "cannot tell which."
566
+ ),
567
+ }
568
+
569
+
473
570
  def cell_id(cell: Mapping[str, object]) -> str:
474
571
  """The identity two runs are matched on: ``<repo>__<command>__<mode>``.
475
572
 
sourcecode/verify_edit.py CHANGED
@@ -56,6 +56,15 @@ class HeadVsWorking:
56
56
  #: True when the working CIR *is* the HEAD CIR because git reported an
57
57
  #: unmodified tree, so it was never built a second time (C3-32).
58
58
  working_cir_reused: bool = False
59
+ #: **C3-102.** False when no model was built at all, which is the only
60
+ #: honest answer to *"what did you analyse?"* on a tree git reports as
61
+ #: unmodified: nothing, because there is nothing to diff. `cir_head` and
62
+ #: `cir_working` are then the cached model if one already existed for this
63
+ #: exact tree state — free — and ``None`` otherwise. Every axis diffs the two
64
+ #: CIRs, so an empty change set has one answer that no analysis can alter;
65
+ #: paying 119,5 s to reach it is what put this command outside the loop it
66
+ #: was built for.
67
+ model_built: bool = True
59
68
 
60
69
 
61
70
  # Git env vars that REDIRECT git at where it operates. When verify-edit runs INSIDE a
@@ -218,34 +227,33 @@ def _build_head_cir_via_worktree(root: Path) -> Any:
218
227
 
219
228
 
220
229
  def _clean_tree_cir(root: Path) -> tuple[Any, bool]:
221
- """The CIR of a tree git reports identical to HEAD — from the shared entry.
222
-
223
- C3-102. C3-32 closed the second half of this: on a clean tree the *working*
224
- CIR is the HEAD CIR, so it is not built twice. What was left is the first
225
- half — the HEAD side still checked out a throwaway detached worktree and
226
- parsed it, which on the field's repository is 76,6 s to answer *"you have
227
- not edited anything"*, and 176,6 s in the round after.
228
-
229
- When git says the tree does not differ from HEAD, the HEAD tree **is** the
230
- worktree: copying it to a temp directory copies bytes that are already on
231
- disk, and the shared repository-wide CIR for exactly this state is what
232
- every other command on this repository has already built or will build.
233
- `context_cache` keys that entry on `worktree_signature`, which on a clean
234
- tree *is* the HEAD sha — the two lanes were separate for a reason that only
235
- holds while the tree is dirty.
230
+ """The cached CIR for a tree git reports identical to HEAD, or ``(None, False)``.
231
+
232
+ **C3-102, second half.** The first half stopped the throwaway worktree: on a
233
+ clean tree the HEAD tree *is* the working tree, so it was read through the
234
+ shared entry instead of being copied and re-parsed. What that left is the
235
+ build itself — 119,5 s on the field's repository, from a purged cache, to
236
+ produce 807 bytes saying *"you have not edited anything"* — and the eleventh
237
+ round is the third to report it.
238
+
239
+ The build is not needed. `verdict()` short-circuits an empty change set
240
+ because every axis is a diff of the two CIRs and here they are the same
241
+ object: no analysis can change the answer, only the time it takes to give it.
242
+ So a clean run takes the model if one is already cached for exactly this tree
243
+ state (free, and it keeps `--compact`-style consumers whole) and builds
244
+ nothing when there is not.
236
245
 
237
246
  The shortcut is reached only through `git_answered` (C3-32's guard): a failed
238
247
  `git diff` also returns an empty change set, and mistaking that for a clean
239
248
  tree would turn an unknown into a confident *"nothing changed"*.
240
249
  """
241
250
  from sourcecode import context_cache as _ctx
242
- from sourcecode.repository_ir import find_java_files
243
251
 
244
- repo_root = _ctx.repo_root_of(root)
245
- cached = _ctx.peek_cir(repo_root)
246
- if cached is not None:
247
- return cached, True
248
- return _ctx.shared_cir(root, find_java_files(root)), False
252
+ try:
253
+ cached = _ctx.peek_cir(_ctx.repo_root_of(root))
254
+ except Exception:
255
+ return None, False
256
+ return (cached, True) if cached is not None else (None, False)
249
257
 
250
258
 
251
259
  def _head_cir(root: Path, head_sha: str) -> tuple[Any, bool]:
@@ -347,11 +355,14 @@ def head_vs_working(path: Path) -> HeadVsWorking:
347
355
  # before the build rather than after it. A clean tree needs no worktree
348
356
  # checkout: the tree on disk is HEAD, and its CIR is the shared one.
349
357
  t1 = time.perf_counter()
358
+ model_built = True
350
359
  if reused:
360
+ # C3-102: nothing to diff, so nothing is built. A model already cached for
361
+ # this exact tree state is taken because it costs a lookup; its absence is
362
+ # not a reason to spend a repository-wide parse on an answer that cannot
363
+ # depend on it.
351
364
  cir_head, hit = _clean_tree_cir(root)
352
- if not hit:
353
- # Keep the edit lane warm: the next run is the one with an edit in it.
354
- _store_head_cir(root, head_sha, cir_head)
365
+ model_built = False
355
366
  else:
356
367
  cir_head, hit = _head_cir(root, head_sha)
357
368
  timings["head_cir_ms"] = (time.perf_counter() - t1) * 1000.0
@@ -369,6 +380,7 @@ def head_vs_working(path: Path) -> HeadVsWorking:
369
380
  timings_ms=timings,
370
381
  changed_build_files=changed_build,
371
382
  working_cir_reused=reused,
383
+ model_built=model_built,
372
384
  )
373
385
 
374
386
 
@@ -424,6 +436,11 @@ class VerifyVerdict:
424
436
  reasons: tuple[str, ...]
425
437
  exit_code: int
426
438
  head_sha: str
439
+ #: **C3-102.** What was read to produce this verdict. Always present, because
440
+ #: its absence would be the answer: a `pass` that analysed nothing and a `pass`
441
+ #: that diffed two models are the same verdict on different evidence, and a
442
+ #: consumer gating on this is owed the difference.
443
+ analysis: dict = field(default_factory=lambda: {"model_built": True})
427
444
 
428
445
  def to_dict(self) -> dict:
429
446
  return {
@@ -436,6 +453,7 @@ class VerifyVerdict:
436
453
  "blast_radius": self.blast_radius,
437
454
  "reasons": list(self.reasons),
438
455
  "exit_code": self.exit_code,
456
+ "analysis": dict(self.analysis),
439
457
  }
440
458
 
441
459
 
@@ -775,6 +793,25 @@ def verdict(hvw: HeadVsWorking, root: Path) -> VerifyVerdict:
775
793
  reasons=(),
776
794
  exit_code=0,
777
795
  head_sha=hvw.head_sha,
796
+ analysis={
797
+ "model_built": hvw.model_built,
798
+ "basis": (
799
+ "git reports no file differing from HEAD, so the working tree "
800
+ "is HEAD and every axis diffs a state against itself"
801
+ ),
802
+ "what_was_read": (
803
+ "the git change set, and a model already cached for this tree "
804
+ "state" if hvw.cir_head is not None else "the git change set"
805
+ ),
806
+ # The claim this verdict does NOT make, said outright: nothing was
807
+ # analysed, so nothing is asserted about the code beyond identity
808
+ # with HEAD.
809
+ "scope_note": (
810
+ "`pass` here means the working tree does not differ from HEAD, "
811
+ "not that HEAD was audited — that is `ask spring-audit` / "
812
+ "`ask verify`."
813
+ ),
814
+ },
778
815
  )
779
816
 
780
817
  axes: dict[str, AxisResult] = {
@@ -815,6 +852,15 @@ def verdict(hvw: HeadVsWorking, root: Path) -> VerifyVerdict:
815
852
  reasons=tuple(reasons),
816
853
  exit_code=code,
817
854
  head_sha=hvw.head_sha,
855
+ analysis={
856
+ "model_built": True,
857
+ "basis": (
858
+ f"{len(hvw.changed_files)} changed .java file(s) and "
859
+ f"{len(hvw.changed_build_files)} changed build file(s): HEAD and "
860
+ f"the working tree were both modelled and diffed"
861
+ ),
862
+ "head_model_cache_hit": hvw.head_cache_hit,
863
+ },
818
864
  )
819
865
 
820
866
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.1.0
3
+ Version: 5.2.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-5.1.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-5.2.0-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -126,7 +126,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
126
126
  # pip / pipx
127
127
  pipx install sourcecode # or: pip install sourcecode
128
128
 
129
- ask version # ask 5.1.0 — and, on a build that has aged,
129
+ ask version # ask 5.2.0 — and, on a build that has aged,
130
130
  # how many releases have probably shipped since
131
131
  ```
132
132
 
@@ -1,15 +1,15 @@
1
- sourcecode/__init__.py,sha256=AQosRL48ISdUoMCE4htHdgvYGVNDHGx2oIMtrSEFoew,308
1
+ sourcecode/__init__.py,sha256=ceBAMIauSu1eUdUEkTRgsDTDWiSm1LutxpbiBjjoS-Y,308
2
2
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
3
3
  sourcecode/archetype.py,sha256=CZvRLpkHot_D8D3JFQVorr-EHDJyh0BbS7RnSTqBigM,40499
4
4
  sourcecode/architectural_baseline.py,sha256=4GiMVBLJVHvRKSWQGf2ZVurI1-R0qk5pmaevgvGSgVA,26325
5
- sourcecode/architectural_delta.py,sha256=E6MkyjWl-1ZgR4KSKnI785gUAfc8nq-lK-jd_rupQBU,12496
5
+ sourcecode/architectural_delta.py,sha256=JhZVmA0gYUryt4x4ieVuxPbcvoBygHgADZaYJ_o4gdk,15142
6
6
  sourcecode/architecture_analyzer.py,sha256=GFc4ek-s1IHWM7pl-0L32WahZ93AmDgrAMcOuBKA5Dk,61463
7
7
  sourcecode/architecture_summary.py,sha256=BVVRHd952cjRhjHnR6CPrvKgaa-tdM16l-pBi1yDCPs,32395
8
8
  sourcecode/ast_extractor.py,sha256=aXJjZ7XjAdxa99hWNm4SyzPqx4gNWiTbUYjNWGNl-Vc,51213
9
9
  sourcecode/audit_report.py,sha256=CLvvlQH2oA6Qj4UADUWhNwSsp1OcyzwXXey5DN_IiWI,8660
10
10
  sourcecode/baseline_autocapture.py,sha256=tmMLexQSeaQRRbqxAd7walvGQqnF0mdpxQmkL40rlhI,15623
11
11
  sourcecode/cache.py,sha256=CK_J8NqyaTNZ57KQT-R4puqI8yYlzG5WL7uLFRbzods,41727
12
- sourcecode/cache_model.py,sha256=w-pmiVRxzNO6BOqYwEQ6fPNXEPDhz0hSmJruX2byNAY,52868
12
+ sourcecode/cache_model.py,sha256=EqsVZ9U2f5Yd1A1RuBp9CBWyMh-mHpFDBOA3aSCmU74,56721
13
13
  sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
14
14
  sourcecode/caller_metrics.py,sha256=--sFGDnIog_YGu9xZHMcB91F5xZakfaAKvy15xU54hg,7904
15
15
  sourcecode/caller_reach.py,sha256=RRF49tv4-QswraJ2vsZ89VAMOj7BmYXlAdXr-tLP_CA,9483
@@ -18,7 +18,7 @@ sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,127
18
18
  sourcecode/change_plan.py,sha256=aX1mp2XnlDN-8R2BcTiu8HlkDwQ_4fN1ZLckj0VMXHM,9945
19
19
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
20
20
  sourcecode/classifier.py,sha256=JBzPwSSrDG-tUHAbcKB678HRbjLpD-ohzbzzO62mgpo,20114
21
- sourcecode/cli.py,sha256=KEYjMKuDdlsrmhfAa3c4MKKPVGywZtPYNNWQriQGrnc,617524
21
+ sourcecode/cli.py,sha256=_dDEE_2pyD4d0VD6WR2KDVKeCVFH6pHToUMEhdd_T4I,622141
22
22
  sourcecode/client_calls.py,sha256=daRTgbXNUOfkzGXJpVb6A737R_Thhka8vw1_YgxCaLg,13548
23
23
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
24
24
  sourcecode/compare.py,sha256=2GdDy0qkDSgmkkpgvoCHitoLj3mt30EeFgJ8PGEXJng,14712
@@ -29,7 +29,7 @@ sourcecode/context_cache.py,sha256=xfS0GpWG8MpHwqgw61tp3IR7jZ2IdG1j-v_HoIiSx7o,3
29
29
  sourcecode/context_graph.py,sha256=a6v2lgl2DxDnPAWE59K3yg6L5YFDak0LIUff3pi9p0c,47327
30
30
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
31
31
  sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
32
- sourcecode/contract_diff.py,sha256=ivgHL1qRKFhriwQ52KWj4oJselqd5icSJW7Z2Ke04po,9429
32
+ sourcecode/contract_diff.py,sha256=skND-oDmLyMDW5Qdvz-yDh7VyisikBCm5fvmLP7Q1xE,10618
33
33
  sourcecode/contract_init.py,sha256=bXRCqWM9tvrDLeT8j7ea6IWf87jB4VJxnlSzGBsRRXo,19582
34
34
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
35
35
  sourcecode/contract_pipeline.py,sha256=4XE-xhab7ysdExXyUOBl2hAH4MABo7moKnT9V4XcEwQ,30243
@@ -52,7 +52,7 @@ sourcecode/envelope.py,sha256=fpF_8znvPqGXKZb0UPYcCYjm27yO7Pr-WsdXNE-tEoY,8911
52
52
  sourcecode/environment_resolution.py,sha256=bfhkM0RGSyLwzFvfi-YhHjU1cy2ufA_WIkXBVeHs9y8,22130
53
53
  sourcecode/error_schema.py,sha256=uwosfNaSujtYm11_732Hu92z5ITV040fQDaIyefSvR4,1683
54
54
  sourcecode/evidence_provider.py,sha256=GSSL44JEaouO5AHks2sB3d1YvC9xIKIld1yBYxZpXxo,4277
55
- sourcecode/execution_plan.py,sha256=c_pYnk-JFY7QNuk_Wc1F0z6tBJswUeu_j5i3QQtakGg,20390
55
+ sourcecode/execution_plan.py,sha256=sV2VrR60XxdzM58SwI577kucLBqCVNQFbDGmA-x9Mvo,22655
56
56
  sourcecode/explain.py,sha256=yqxvKiLF0XMMg11PdgqMFrsim5SXuEKM1U2cLvCLspM,29935
57
57
  sourcecode/file_chunker.py,sha256=3vkM3mDQ5eE_yTPvUgjyjpGFBIjkW6_mrBmIbrylnA8,16444
58
58
  sourcecode/file_classifier.py,sha256=pJCeN9KqWpAwKMCgGP4KDsBjuWMeo4zlbj5fv3hk9dA,15587
@@ -83,7 +83,7 @@ sourcecode/parse_cache.py,sha256=TpIoSeNUba4Aoaz5o5SmgAc0Tz02OKq1Du4Hw7HyJfg,225
83
83
  sourcecode/partial_contract.py,sha256=x_N_0bF3mpNNjXd7JFB1zr6LSIroo2zN5GCz14GXvXQ,3510
84
84
  sourcecode/path_admission.py,sha256=OGNSoluhVh8YYOBZBnACUrkmH4Tp_yxjGLifkcsxQpo,5838
85
85
  sourcecode/path_filters.py,sha256=LmTYq735orssKTIuxTX1mE1FHAXF7dVEJEse35xE1RA,12371
86
- sourcecode/perf.py,sha256=l5YW_BBKMRZb88yjzI9FnXpr21pgmQ7LPt1MZBYhf3k,29345
86
+ sourcecode/perf.py,sha256=Zipl5Wveolrl5xsDzHtlX7q0WYGQLfKlGX1m6__AkWA,34232
87
87
  sourcecode/phased_run.py,sha256=cFd0Lxfc8XL-XkCVCCzcjd9bZbtgkJkGZAHfmJ6dBJ4,16542
88
88
  sourcecode/pipe_contract.py,sha256=PML0Er5d8uDyec4OrdUXuyqHbGPUYndjeaKUjkFz2u4,8369
89
89
  sourcecode/posture.py,sha256=7d3zM4ExrSXVKrWNTlXQ0fUQNqDdl1LOuKwRVaXdRss,76574
@@ -143,7 +143,7 @@ sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
143
143
  sourcecode/type_usage_surface.py,sha256=51IrKRQoIoRnlsiDjHnqpJBn2rc6E59aRhgS0HTzAF0,4428
144
144
  sourcecode/validation_inference.py,sha256=-oWJqE6PqkqcZbCFDJeWCQHgI9Dbuv5ggJWD3LE_2IU,20817
145
145
  sourcecode/validation_surface.py,sha256=jYL-hkDjaaRKkAt7ZUcbxm3Dydl8o6KVRhTkuPXKPVc,31460
146
- sourcecode/verify_edit.py,sha256=djBCZcDMEM71UFhls_J4oImYBu7CUeQOHPKzWrT9c9U,40288
146
+ sourcecode/verify_edit.py,sha256=SUaJeH5Icc54mutQx3-gh7i8U6d50cb_oxYc9SDUnHs,42757
147
147
  sourcecode/verify_repo.py,sha256=aHlNIUfoVVUxTXTNtSFryP4dOFBOvCfdyC31fFRXU6s,15346
148
148
  sourcecode/verify_rules.py,sha256=YxG5JkDXBVpwbU1_tIKVtU8_eaF3zjpsq05tjpN4Z7Y,19401
149
149
  sourcecode/version_check.py,sha256=CHp6ZxTIfo8kyHPCBgJA1uFC0xQCoXMuuOfrW8QTL8o,4942
@@ -207,8 +207,8 @@ sourcecode/telemetry/consent.py,sha256=pQdl-QeLl6Gcibn0eWHSKZrm-HYSsjpVqOnjrgFp8
207
207
  sourcecode/telemetry/events.py,sha256=4_yeO58U-Cwc1Qb27VB0_EjhmroY0k91n3_VGxeALB8,2776
208
208
  sourcecode/telemetry/filters.py,sha256=RzxauTz8HliO4BllQnXEXc7zTeqdCZi5MgqGEDuW7OQ,6570
209
209
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
210
- sourcecode-5.1.0.dist-info/METADATA,sha256=-w-kPbI2gq9YUOyXPsS8f-zgKHUVJBsbC_u-iXcgAZ0,42410
211
- sourcecode-5.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
212
- sourcecode-5.1.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
213
- sourcecode-5.1.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
214
- sourcecode-5.1.0.dist-info/RECORD,,
210
+ sourcecode-5.2.0.dist-info/METADATA,sha256=5c5Xukk2etIZwtrijfL2_Hya7h1a08S-MyOD0s7EX8U,42410
211
+ sourcecode-5.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
212
+ sourcecode-5.2.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
213
+ sourcecode-5.2.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
214
+ sourcecode-5.2.0.dist-info/RECORD,,